Wie kann man dieses Design näher an die richtige DDD bringen?

12

Ich habe seit Tagen über DDD gelesen und brauche Hilfe bei diesem Beispieldesign. Alle DDD-Regeln verwirren mich sehr, wie ich überhaupt etwas erstellen soll, wenn Domänenobjekte der Anwendungsebene keine Methoden anzeigen dürfen. Wo sonst kann man Verhalten orchestrieren? Repositorys dürfen nicht in Entitäten injiziert werden, und die Entitäten selbst müssen daher am Status arbeiten. Dann muss eine Entität etwas anderes aus der Domäne wissen, aber andere Entitätsobjekte dürfen auch nicht injiziert werden. Einige dieser Dinge ergeben für mich einen Sinn, andere nicht. Ich habe noch keine guten Beispiele dafür gefunden, wie man ein ganzes Feature erstellt, da es in jedem Beispiel um Bestellungen und Produkte geht und die anderen Beispiele immer wieder wiederholt werden. Ich lerne am besten anhand von Beispielen und habe versucht, anhand der Informationen, die ich bisher über DDD erhalten habe, ein Feature zu erstellen.

Ich brauche Ihre Hilfe, um darauf hinzuweisen, was ich falsch mache und wie ich es beheben kann. Am besten mit Code, da "Ich würde es nicht empfehlen, X und Y zu machen" in einem Kontext, in dem alles nur vage definiert ist, sehr schwer zu verstehen ist. Wenn ich eine Entität nicht in eine andere injizieren kann, ist es einfacher zu sehen, wie es richtig gemacht wird.

In meinem Beispiel gibt es Benutzer und Moderatoren. Ein Moderator kann Benutzer sperren, jedoch mit einer Geschäftsregel: nur 3 pro Tag. Ich habe versucht, ein Klassendiagramm zu erstellen, um die Beziehungen anzuzeigen (Code unten):

Bildbeschreibung hier eingeben

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);

Sollte die Benutzerberechtigung ein 'is_banned'Feld haben, mit dem geprüft werden kann $user->isBanned();? Wie hebe ich ein Verbot auf? Ich habe keine Ahnung.

Seralisieren
quelle
Aus dem Wikipedia-Artikel: "Domain-driven Design ist keine Technologie oder Methodik." Daher ist die Diskussion über solche für dieses Format ungeeignet. Außerdem können nur Sie und Ihre Experten entscheiden, ob Ihr Modell richtig ist.
1
@Todd Smith legt großen Wert auf " Domänenobjekte dürfen der Anwendungsebene keine Methoden anzeigen " . Beachten Sie, dass das erste Codebeispiel der Schlüssel ist, um keine Repositorys in Domänenobjekte zu injizieren, etwas anderes speichert und lädt sie. Das machen sie nicht selbst. Auf diese Weise kann die Anwendungslogik auch Transaktionen steuern, anstatt die Domäne / das Modell / die Entität / die Geschäftsobjekte oder was auch immer Sie sie aufrufen möchten.
FastAl

Antworten:

11

Diese Frage ist etwas subjektiv und führt eher zu einer Diskussion als zu einer direkten Antwort, die, wie jemand anderes darauf hingewiesen hat, für das Stackoverflow-Format nicht geeignet ist. Das heißt, ich denke, Sie brauchen nur ein paar codierte Beispiele, wie Sie Probleme angehen können, also werde ich es versuchen, nur um Ihnen ein paar Ideen zu geben.

Das erste, was ich sagen würde, ist:

"Domänenobjekte dürfen der Anwendungsebene keine Methoden anzeigen"

Das stimmt einfach nicht - ich würde mich interessieren, woher Sie das gelesen haben. Die Anwendungsschicht ist der Koordinator zwischen Benutzeroberfläche, Infrastruktur und Domäne und muss daher offensichtlich Methoden für Domänenentitäten aufrufen.

Ich habe ein codiertes Beispiel dafür geschrieben, wie ich Ihr Problem angehen würde. Ich entschuldige mich, dass es in C # ist, aber ich kenne PHP nicht - hoffentlich bekommen Sie das Wesentliche aus der Strukturperspektive heraus.

Vielleicht hätte ich das nicht tun sollen, aber ich habe Ihre Domain-Objekte leicht modifiziert. Ich konnte nicht anders, als den Eindruck zu erwecken, dass das Konzept eines 'BannedUser' im System vorhanden ist, auch wenn das Verbot abgelaufen ist.

Hier ist zunächst der Anwendungsservice - so würde die Benutzeroberfläche lauten:

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);
        }
    }
}

Ziemlich einfach. Sie holen den Moderator, der die Sperre vornimmt, den Benutzer, den der Moderator sperren möchte, und rufen die 'Ban'-Methode für den Benutzer auf und übergeben den Moderator. Dies ändert den Status sowohl des Moderators als auch des Benutzers (siehe unten), der dann über die entsprechenden Repositorys beibehalten werden muss.

Die Benutzerklasse:

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;
    }
}

Die Invariante für einen Benutzer ist, dass er bestimmte Aktionen nicht ausführen kann, wenn er gesperrt ist. Daher müssen wir in der Lage sein, zu identifizieren, ob ein Benutzer derzeit gesperrt ist. Zu diesem Zweck führt der Benutzer eine Liste der von Moderatoren verhängten Serving-Verbote. Die IsBanned () -Methode prüft, ob Serving-Bans noch nicht abgelaufen sind. Wenn die Ban () -Methode aufgerufen wird, erhält sie einen Moderator als Parameter. Dies fordert dann den Moderator auf, ein Verbot zu erlassen:

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;
    }
}

Die Invariante für den Moderator ist, dass er nur 3 Sperren pro Tag ausstellen kann. Wenn die IssueBan-Methode aufgerufen wird, wird daher überprüft, ob der Moderator in seiner Liste der erteilten Verbote keine drei gesperrten Verbote mit dem heutigen Datum hat. Es fügt dann das neu ausgestellte Verbot zu seiner Liste hinzu und gibt es zurück.

Subjektiv, und ich bin sicher, dass jemand mit dem Ansatz nicht einverstanden ist, aber hoffentlich gibt es Ihnen eine Idee oder wie es zusammenpassen kann.

David Masters
quelle
1

Verschieben Sie Ihre gesamte Logik, die den Status ändert, in eine Serviceebene (z. B. ModeratorService), die sowohl Entitäten als auch Repositorys kennt.

ModeratorService.BanUser(User, UserBanRepository, etc.)
{
    // handle ban logic in the ModeratorService
    // update User object
    // update repository
}
Todd Smith
quelle