Come rendere questo design più vicino al DDD corretto?


12

Ho letto su DDD per giorni e ho bisogno di aiuto con questo progetto di esempio. Tutte le regole di DDD mi rendono molto confuso su come dovrei costruire qualcosa quando gli oggetti di dominio non sono autorizzati a mostrare metodi a livello di applicazione; dove altro orchestrare il comportamento? I repository non possono essere iniettati in entità e le entità stesse devono quindi lavorare sullo stato. Quindi un'entità deve sapere qualcos'altro dal dominio, ma non è consentito iniettare altri oggetti entità? Alcune di queste cose hanno senso per me, ma altre no. Devo ancora trovare buoni esempi di come creare un'intera funzionalità poiché ogni esempio riguarda ordini e prodotti, ripetendo gli altri esempi più e più volte. Imparo meglio leggendo esempi e ho provato a creare una funzionalità utilizzando le informazioni che ho acquisito su DDD finora.

Ho bisogno del tuo aiuto per sottolineare cosa faccio di sbagliato e come risolverlo, preferibilmente con il codice come "Non consiglierei di fare X e Y" è molto difficile da capire in un contesto in cui tutto è già vagamente definito. Se non riesco a iniettare un'entità in un'altra, sarebbe più facile vedere come farlo correttamente.

Nel mio esempio ci sono utenti e moderatori. Un moderatore può vietare gli utenti, ma con una regola aziendale: solo 3 al giorno. Ho fatto un tentativo di impostare un diagramma di classe per mostrare le relazioni (codice sotto):

inserisci qui la descrizione dell'immagine

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

L'entità utente dovrebbe avere un 'is_banned'campo che può essere verificato con $user->isBanned();? Come rimuovere un divieto? Non ne ho idea.


Dall'articolo di Wikipedia: "Il design guidato dal dominio non è una tecnologia o una metodologia", quindi la discussione di tale non è appropriata per questo formato. Inoltre, solo tu e i tuoi "esperti" potete decidere se il modello è giusto.

1
@Todd smith ha un grande punto su "Gli oggetti di dominio non sono autorizzati a mostrare metodi a livello di applicazione" . Nota che il primo esempio di codice è la chiave per non iniettare repository negli oggetti di dominio, qualcos'altro li salva e li carica. Non lo fanno da soli. Ciò consente alla logica dell'app di controllare anche le transazioni anziché il dominio / modello / entità / oggetti business / o come si desidera chiamarle.
FastAl,

Risposte:


11

Questa domanda è in qualche modo soggettiva e porta a più di una discussione che a una risposta diretta, che, come ha sottolineato qualcun altro, non è appropriata per il formato stackoverflow. Detto questo, penso che hai solo bisogno di alcuni esempi in codice su come affrontare i problemi, quindi ci proverò, solo per darti alcune idee.

La prima cosa che direi è:

"Gli oggetti di dominio non sono autorizzati a mostrare metodi a livello di applicazione"

Semplicemente non è vero: sarei interessato a sapere da dove l'hai letto. Il livello dell'applicazione è l'orchestratore tra UI, Infrastruttura e Dominio e pertanto deve ovviamente invocare metodi su entità di dominio.

Ho scritto un esempio in codice di come affrontare il tuo problema. Mi scuso per il fatto che sia in C #, ma non conosco PHP - spero che otterrai ancora l'essenza dal punto di vista della struttura.

Forse non avrei dovuto farlo, ma ho leggermente modificato i tuoi oggetti di dominio. Non ho potuto fare a meno di pensare che fosse leggermente imperfetto, in quanto il concetto di "Utente vietato" esiste nel sistema, anche se il divieto è scaduto.

Per cominciare, ecco il servizio applicativo - questo è ciò che l'interfaccia utente chiamerebbe:

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

Abbastanza diretto. Prendi il moderatore che fa il ban, l'utente che il moderatore vuole bandire e chiami il metodo 'Ban' sull'utente, passando il moderatore. Ciò modificherà lo stato sia del moderatore che dell'utente (spiegato di seguito), che deve quindi persistere tramite i rispettivi repository.

La classe utente:

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'invariante per un utente è che non può eseguire determinate azioni quando viene bannato, quindi dobbiamo essere in grado di identificare se un utente è attualmente bannato. A tal fine, l'utente mantiene un elenco di divieti di pubblicazione emessi da moderatori. Il metodo IsBanned () verifica la presenza di ban che non sono ancora scaduti. Quando viene chiamato il metodo Ban (), riceve un moderatore come parametro. Questo quindi chiede al moderatore di emettere un divieto:

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'invariante per il moderatore è che può emettere solo 3 divieti al giorno. Pertanto, quando viene chiamato il metodo IssueBan, verifica che il moderatore non abbia 3 divieti emessi con la data odierna nell'elenco dei divieti emessi. Quindi aggiunge il divieto appena emesso alla sua lista e lo restituisce.

Soggettivo, e sono sicuro che qualcuno non sarà d'accordo con l'approccio, ma spero che ti dia un'idea o come possa stare insieme.


1

Sposta tutta la tua logica che altera lo stato su un livello di servizio (es: ModeratorService) che conosce sia le entità che i repository.

ModeratorService.BanUser(User, UserBanRepository, etc.)
{
    // handle ban logic in the ModeratorService
    // update User object
    // update repository
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.