Jak przybliżyć ten projekt do właściwego DDD?

12

Czytałem o DDD od kilku dni i potrzebuję pomocy w tym przykładowym projekcie. Wszystkie reguły DDD powodują, że jestem bardzo zdezorientowany tym, jak mam cokolwiek zbudować, gdy obiekty domeny nie mogą pokazywać metod w warstwie aplikacji; gdzie jeszcze koordynować zachowanie? Repozytoria nie mogą być wstrzykiwane do podmiotów, a same podmioty muszą zatem działać na stan. Zatem encja musi wiedzieć coś jeszcze z domeny, ale inne obiekty encji też nie mogą być wstrzykiwane? Niektóre z tych rzeczy mają dla mnie sens, ale niektóre nie. Muszę znaleźć dobre przykłady, jak zbudować całą funkcję, ponieważ każdy przykład dotyczy zamówień i produktów, powtarzając inne przykłady w kółko. Uczę się najlepiej, czytając przykłady i próbowałem zbudować funkcję, korzystając z informacji uzyskanych do tej pory o DDD.

Potrzebuję twojej pomocy, aby wskazać, co robię źle i jak to naprawić, najlepiej za pomocą kodu, ponieważ „nie polecam robienia X i Y” jest bardzo trudne do zrozumienia w kontekście, w którym wszystko jest już niejasno zdefiniowane. Jeśli nie mogę wstrzyknąć bytu innemu, łatwiej byłoby zobaczyć, jak to zrobić poprawnie.

W moim przykładzie są użytkownicy i moderatorzy. Moderator może banować użytkowników, ale z regułą biznesową: tylko 3 dziennie. Podjąłem próbę skonfigurowania diagramu klas, aby pokazać relacje (kod poniżej):

wprowadź opis zdjęcia tutaj

interface iUser
{
    public function getUserId();
    public function getUsername();
}

class User implements iUser
{
    protected $_id;
    protected $_username;

    public function __construct(UserId $user_id, Username $username)
    {
        $this->_id          = $user_id;
        $this->_username    = $username;
    }

    public function getUserId()
    {
        return $this->_id;
    }

    public function getUsername()
    {
        return $this->_username;
    }
}

class Moderator extends User
{
    protected $_ban_count;
    protected $_last_ban_date;

    public function __construct(UserBanCount $ban_count, SimpleDate $last_ban_date)
    {
        $this->_ban_count       = $ban_count;
        $this->_last_ban_date   = $last_ban_date;
    }

    public function banUser(iUser &$user, iBannedUser &$banned_user)
    {
        if (! $this->_isAllowedToBan()) {
            throw new DomainException('You are not allowed to ban more users today.');
        }

        if (date('d.m.Y') != $this->_last_ban_date->getValue()) {
            $this->_ban_count = 0;
        }

        $this->_ban_count++;

        $date_banned        = date('d.m.Y');
        $expiration_date    = date('d.m.Y', strtotime('+1 week'));

        $banned_user->add($user->getUserId(), new SimpleDate($date_banned), new SimpleDate($expiration_date));
    }

    protected function _isAllowedToBan()
    {
        if ($this->_ban_count >= 3 AND date('d.m.Y') == $this->_last_ban_date->getValue()) {
            return false;
        }

        return true;
    }
}

interface iBannedUser
{
    public function add(UserId $user_id, SimpleDate $date_banned, SimpleDate $expiration_date);
    public function remove();
}

class BannedUser implements iBannedUser
{
    protected $_user_id;
    protected $_date_banned;
    protected $_expiration_date;

    public function __construct(UserId $user_id, SimpleDate $date_banned, SimpleDate $expiration_date)
    {
        $this->_user_id         = $user_id;
        $this->_date_banned     = $date_banned;
        $this->_expiration_date = $expiration_date;
    }

    public function add(UserId $user_id, SimpleDate $date_banned, SimpleDate $expiration_date)
    {
        $this->_user_id         = $user_id;
        $this->_date_banned     = $date_banned;
        $this->_expiration_date = $expiration_date;
    }

    public function remove()
    {
        $this->_user_id         = '';
        $this->_date_banned     = '';
        $this->_expiration_date = '';
    }
}

// Gathers objects
$user_repo = new UserRepository();
$evil_user = $user_repo->findById(123);

$moderator_repo = new ModeratorRepository();
$moderator = $moderator_repo->findById(1337);

$banned_user_factory = new BannedUserFactory();
$banned_user = $banned_user_factory->build();

// Performs ban
$moderator->banUser($evil_user, $banned_user);

// Saves objects to database
$user_repo->store($evil_user);
$moderator_repo->store($moderator);

$banned_user_repo = new BannedUserRepository();
$banned_user_repo->store($banned_user);

Czy uprawnienie użytkownika powinno zawierać 'is_banned'pole, które można sprawdzić $user->isBanned();? Jak usunąć ban? Nie mam pojęcia.

Seralizować
źródło
Z artykułu w Wikipedii: „Projektowanie oparte na domenie nie jest technologią ani metodologią”. Dlatego dyskusja na ten temat jest nieodpowiednia dla tego formatu. Również tylko Ty i Twoi „eksperci” możesz zdecydować, czy Twój model jest odpowiedni.
1
@Todd Smith podkreśla, że „obiekty domeny nie mogą pokazywać metod w warstwie aplikacji” . Zauważ, że pierwszym przykładem kodu kluczem do nie wstrzykiwania repozytoriów do obiektów domeny jest coś innego, co zapisuje i ładuje. Nie robią tego sami. To pozwala logice aplikacji sterować także transakcjami zamiast domeny / modelu / encji / obiektów biznesowych / lub cokolwiek, co chcesz je nazwać.
FastAl

Odpowiedzi:

11

To pytanie jest nieco subiektywne i prowadzi raczej do dyskusji niż do bezpośredniej odpowiedzi, która, jak zauważył ktoś inny, nie jest odpowiednia dla formatu przepływu stosu. To powiedziawszy, myślę, że potrzebujesz tylko zakodowanych przykładów radzenia sobie z problemami, więc dam ci szansę, aby dać ci kilka pomysłów.

Pierwszą rzeczą, którą powiem to:

„obiekty domeny nie mogą wyświetlać metod w warstwie aplikacji”

To po prostu nieprawda - chciałbym wiedzieć, skąd to przeczytałeś. Warstwa aplikacji jest koordynatorem między interfejsem użytkownika, infrastrukturą i domeną, dlatego oczywiście musi wywoływać metody na jednostkach domeny.

Napisałem zakodowany przykład rozwiązania tego problemu. Przepraszam, że jest w języku C #, ale nie znam PHP - mam nadzieję, że nadal dostaniesz sedno z perspektywy struktury.

Być może nie powinienem był tego robić, ale nieco zmodyfikowałem twoje obiekty domeny. Nie mogłem się oprzeć wrażeniu, że był nieco wadliwy, ponieważ w systemie istnieje koncepcja „BannedUser”, nawet jeśli ban wygasł.

Na początek oto usługa aplikacji - tak właśnie zadzwoni interfejs użytkownika:

public class ModeratorApplicationService
{
    private IUserRepository _userRepository;
    private IModeratorRepository _moderatorRepository;

    public void BanUser(Guid moderatorId, Guid userToBeBannedId)
    {
        Moderator moderator = _moderatorRepository.GetById(moderatorId);
        User userToBeBanned = _userRepository.GetById(userToBeBannedId);

        using (IUnitOfWork unitOfWork = UnitOfWorkFactory.Create())
        {
            userToBeBanned.Ban(moderator);

            _userRepository.Save(userToBeBanned);
            _moderatorRepository.Save(moderator);
        }
    }
}

Całkiem prosto. Pobierasz moderatora wykonującego blokadę, użytkownika, którego moderator chce zablokować, i wywołujesz metodę „Banowania” dla użytkownika, przekazując moderatorowi. Spowoduje to modyfikację stanu zarówno moderatora, jak i użytkownika (wyjaśnione poniżej), które następnie muszą przetrwać za pośrednictwem odpowiednich repozytoriów.

Klasa użytkownika:

public class User : IUser
{
    private readonly Guid _userId;
    private readonly string _userName;
    private readonly List<ServingBan> _servingBans = new List<ServingBan>();

    public Guid UserId
    {
        get { return _userId; }
    }

    public string Username
    {
        get { return _userName; }
    }

    public void Ban(Moderator bannedByModerator)
    {
        IssuedBan issuedBan = bannedByModerator.IssueBan(this);

        _servingBans.Add(new ServingBan(bannedByModerator.UserId, issuedBan.BanDate, issuedBan.BanExpiry));
    }

    public bool IsBanned()
    {
        return (_servingBans.FindAll(CurrentBans).Count > 0);
    }

    public User(Guid userId, string userName)
    {
        _userId = userId;
        _userName = userName;
    }

    private bool CurrentBans(ServingBan ban)
    {
        return (ban.BanExpiry > DateTime.Now);
    }

}

public class ServingBan
{
    private readonly DateTime _banDate;
    private readonly DateTime _banExpiry;
    private readonly Guid _bannedByModeratorId;

    public DateTime BanDate
    {
        get { return _banDate;}
    }

    public DateTime BanExpiry
    {
        get { return _banExpiry; }
    }

    public ServingBan(Guid bannedByModeratorId, DateTime banDate, DateTime banExpiry)
    {
        _bannedByModeratorId = bannedByModeratorId;
        _banDate = banDate;
        _banExpiry = banExpiry;
    }
}

Niezmiennością dla użytkownika jest to, że nie może wykonać pewnych działań po zbanowaniu, dlatego musimy być w stanie stwierdzić, czy użytkownik jest obecnie zbanowany. Aby to osiągnąć, użytkownik utrzymuje listę zakazów udostępniania wydanych przez moderatorów. Metoda IsBanned () sprawdza wszelkie zakazy udostępniania, które jeszcze nie wygasły. Gdy wywoływana jest metoda Ban (), odbiera moderator jako parametr. Następnie prosi moderatora o wydanie zakazu:

public class Moderator : User
{
    private readonly List<IssuedBan> _issuedbans = new List<IssuedBan>();

    public bool CanBan()
    {
        return (_issuedbans.FindAll(BansWithTodaysDate).Count < 3);
    }

    public IssuedBan IssueBan(User user)
    {
        if (!CanBan())
            throw new InvalidOperationException("Ban limit for today has been exceeded");

        IssuedBan issuedBan = new IssuedBan(user.UserId, DateTime.Now, DateTime.Now.AddDays(7));

        _issuedbans.Add(issuedBan); 

        return issuedBan;
    }

    private bool BansWithTodaysDate(IssuedBan ban)
    {
        return (ban.BanDate.Date == DateTime.Today.Date);
    }
}

public class IssuedBan
{
    private readonly Guid _bannedUserId;
    private readonly DateTime _banDate;
    private readonly DateTime _banExpiry;

    public DateTime BanDate { get { return _banDate;}}

    public DateTime BanExpiry { get { return _banExpiry;}}

    public IssuedBan(Guid bannedUserId, DateTime banDate, DateTime banExpiry)
    {
        _bannedUserId = bannedUserId;
        _banDate = banDate;
        _banExpiry = banExpiry;
    }
}

Niezmiennikiem dla moderatora jest to, że może on wydać tylko 3 zakazy dziennie. Zatem po wywołaniu metody IssueBan sprawdza, czy moderator nie ma 3 wydanych zakazów z dzisiejszą datą na liście wydanych zakazów. Następnie dodaje nowo wydany zakaz do swojej listy i zwraca go.

Subiektywnie i jestem pewien, że ktoś nie zgodzi się z tym podejściem, ale mam nadzieję, że daje to pomysł lub sposób, w jaki można go ze sobą połączyć.

David Masters
źródło
1

Przenieś całą logikę, która zmienia stan na warstwę usługi (np. ModeratorService), która zna zarówno Encje, jak i Repozytoria.

ModeratorService.BanUser(User, UserBanRepository, etc.)
{
    // handle ban logic in the ModeratorService
    // update User object
    // update repository
}
Todd Smith
źródło