Come rendere questo disegno più vicino al DDD corretto?

12

Ho letto DDD da giorni e ho bisogno di aiuto con questo esempio di design. Tutte le regole della DDD mi rendono molto confuso su come dovrei costruire qualcosa quando gli oggetti del dominio non sono autorizzati a mostrare metodi al livello dell'applicazione; dove altro per 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 è possibile iniettare anche altri oggetti di entità? Alcune di queste cose hanno senso per me, ma alcune no. Devo ancora trovare buoni esempi di come costruire un'intera funzione, dato che ogni esempio riguarda gli ordini e i prodotti, ripetendo gli altri esempi più e più volte. Imparo meglio leggendo esempi e ho cercato di creare una funzione utilizzando le informazioni che ho acquisito riguardo al DDD fino a questo punto.

Ho bisogno del tuo aiuto per sottolineare cosa faccio di sbagliato e come risolverlo, preferibilmente con codice come "Non consiglierei di fare X e Y" è molto difficile da capire in un contesto in cui tutto è solo 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 agli 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):

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

Se l'entitity dell'utente ha un campo 'is_banned' che può essere controllato con $user->isBanned(); ? Come rimuovere un divieto? Non ne ho idea.

    
posta Seralize 20.06.2012 - 23:56
fonte

2 risposte

11

Questa domanda è in qualche modo soggettiva e conduce a una discussione più che a una risposta diretta, che, come qualcuno ha sottolineato, non è appropriata per il formato stackoverflow. Detto questo, penso che tu abbia solo bisogno di alcuni esempi codificati su come affrontare i problemi, quindi ti darò una possibilità, solo per darti qualche idea.

La prima cosa che direi è:

"domain objects are not allowed to show methods to the application layer"

Questo semplicemente non è vero - sarei interessato a sapere da dove hai letto questo. Il livello dell'applicazione è l'orchestratore tra interfaccia utente, infrastruttura e amp; Dominio, e quindi ovviamente ha bisogno di invocare metodi su entità di dominio.

Ho scritto un esempio in codice su come affrontare il tuo problema. Mi scuso per il fatto che sia in C #, ma non conosco il PHP - si spera che otterrete comunque il succo da una prospettiva di struttura.

Forse non avrei dovuto farlo, ma ho leggermente modificato i tuoi oggetti di dominio. Non ho potuto fare a meno di sentire che era leggermente imperfetto, in quanto nel sistema esiste il concetto di "Utente Bannato", 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);
        }
    }
}

Piuttosto semplice. Recuperi il moderatore che fa il divieto, l'utente che il moderatore vuole vietare e chiama il metodo "Ban" sull'utente, passando il moderatore. Questo modificherà lo stato sia del moderatore che dell'amplificatore; utente (spiegato di seguito), che necessita quindi di persistere tramite i repository corrispondenti.

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 possono eseguire determinate azioni quando sono bannati, quindi dobbiamo essere in grado di identificare se un utente è attualmente bannato. Per raggiungere questo obiettivo l'utente mantiene un elenco di divieti di pubblicazione che sono stati emessi dai moderatori. Il metodo IsBanned () verifica la scadenza di eventuali divieti di pubblicazione. Quando viene chiamato il metodo Ban (), riceve un moderatore come parametro. Ciò richiede quindi 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, controlla che il moderatore non abbia 3 divieti emessi con la data odierna nella sua lista di 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 si spera che ti dia un'idea o come possa combaciare.

    
risposta data 21.06.2012 - 12:30
fonte
1

Sposta tutta la tua logica che altera lo stato a 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
}
    
risposta data 21.06.2012 - 00:55
fonte

Leggi altre domande sui tag