Comment rendre cette conception plus proche du DDD approprié?

12

J'ai lu sur DDD depuis des jours et j'ai besoin d'aide avec cet exemple de conception. Toutes les règles de DDD me rendent très confus quant à la façon dont je suis censé construire quoi que ce soit lorsque les objets de domaine ne sont pas autorisés à montrer des méthodes à la couche application; où d'autre pour orchestrer le comportement? Les référentiels ne sont pas autorisés à être injectés dans des entités et les entités elles-mêmes doivent donc travailler sur l'état. Une entité doit alors connaître autre chose du domaine, mais les autres objets d'entité ne sont pas autorisés à être injectés non plus? Certaines de ces choses ont du sens pour moi, mais d'autres non. Je n'ai pas encore trouvé de bons exemples de la façon de construire une fonctionnalité entière car chaque exemple concerne les commandes et les produits, répétant les autres exemples encore et encore. J'apprends mieux en lisant des exemples et j'ai essayé de créer une fonctionnalité en utilisant les informations que j'ai obtenues sur DDD jusqu'à présent.

J'ai besoin de votre aide pour signaler ce que je fais mal et comment y remédier, de préférence avec du code car "je ne recommanderais pas de faire X et Y" est très difficile à comprendre dans un contexte où tout est déjà défini vaguement. Si je ne peux pas injecter une entité dans une autre, il serait plus facile de voir comment le faire correctement.

Dans mon exemple, il y a des utilisateurs et des modérateurs. Un modérateur peut interdire les utilisateurs, mais avec une règle commerciale: seulement 3 par jour. J'ai tenté de mettre en place un diagramme de classes pour montrer les relations (code ci-dessous):

entrez la description de l'image ici

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

Le droit d'utilisateur doit-il avoir un 'is_banned'champ qui peut être vérifié avec $user->isBanned();? Comment supprimer une interdiction? Je n'ai aucune idée.

Seralize
la source
Extrait de l'article de Wikipédia: "La conception axée sur le domaine n'est pas une technologie ou une méthodologie." Par conséquent, la discussion de ce type n'est pas appropriée pour ce format. De plus, seuls vous et vos «experts» pouvez décider si votre modèle est bon.
1
@Todd smith fait un bon point sur "les objets de domaine ne sont pas autorisés à montrer des méthodes à la couche application" . Notez que le premier exemple de code est la clé pour ne pas injecter de référentiels dans les objets de domaine, quelque chose d'autre les enregistre et les charge. Ils ne le font pas eux-mêmes. Cela permet à la logique d'application de contrôler les transactions, également, au lieu du domaine / modèle / entité / objets métier / ou tout ce que vous voulez les appeler.
FastAl

Réponses:

11

Cette question est quelque peu subjective et conduit à plus de discussion qu'à une réponse directe qui, comme quelqu'un l'a souligné, n'est pas appropriée pour le format stackoverflow. Cela dit, je pense que vous avez juste besoin d'exemples codés sur la façon de résoudre les problèmes, alors je vais essayer, juste pour vous donner quelques idées.

La première chose que je dirais est:

"Les objets de domaine ne sont pas autorisés à afficher des méthodes dans la couche d'application"

Ce n'est tout simplement pas vrai - je serais intéressé de savoir d'où vous avez lu cela. La couche application est l'orchestrateur entre l'interface utilisateur, l'infrastructure et le domaine, et doit donc évidemment invoquer des méthodes sur les entités de domaine.

J'ai écrit un exemple codé de la façon dont je traiterais votre problème. Je m'excuse que c'est en C #, mais je ne connais pas PHP - j'espère que vous aurez toujours l'essentiel du point de vue de la structure.

Peut-être que je n'aurais pas dû le faire, mais j'ai légèrement modifié vos objets de domaine. Je ne pouvais pas m'empêcher de penser qu'il était légèrement défectueux, en ce sens que le concept d'un «utilisateur interdit» existe dans le système, même si l'interdiction a expiré.

Pour commencer, voici le service d'application - voici ce que l'interface utilisateur appellerait:

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

Assez simple. Vous récupérez le modérateur faisant l'interdiction, l'utilisateur que le modérateur veut interdire, et appelez la méthode 'Ban' sur l'utilisateur, en passant le modérateur. Cela modifiera l'état du modérateur et de l'utilisateur (expliqué ci-dessous), qui doit ensuite persister via leurs référentiels correspondants.

La classe Utilisateur:

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

L'invariant pour un utilisateur est qu'il ne peut pas effectuer certaines actions lorsqu'il est banni, nous devons donc être en mesure d'identifier si un utilisateur est actuellement banni. Pour ce faire, l'utilisateur tient à jour une liste des interdictions de service émises par les modérateurs. La méthode IsBanned () vérifie toutes les interdictions de diffusion qui n'ont pas encore expiré. Lorsque la méthode Ban () est appelée, elle reçoit un modérateur comme paramètre. Cela demande ensuite au modérateur d'émettre une interdiction:

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

L'invariant pour le modérateur est qu'il ne peut émettre que 3 interdictions par jour. Ainsi, lorsque la méthode IssueBan est appelée, elle vérifie que le modérateur n'a pas 3 interdictions émises avec la date d'aujourd'hui dans sa liste d'interdictions émises. Il ajoute ensuite l'interdiction nouvellement émise à sa liste et la renvoie.

Subjective, et je suis sûr que quelqu'un sera en désaccord avec l'approche, mais j'espère que cela vous donne une idée ou comment elle peut s'adapter.

David Masters
la source
1

Déplacez toute votre logique qui modifie l'état vers une couche de service (ex: ModeratorService) qui connaît à la fois les entités et les référentiels.

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