Como tornar esse design mais próximo do DDD adequado?

12

Eu li sobre DDD há dias e preciso de ajuda com este design de exemplo. Todas as regras do DDD me deixam muito confuso sobre como eu devo criar alguma coisa quando objetos de domínio não têm permissão para mostrar métodos para a camada de aplicativo; onde mais para orquestrar o comportamento? Repositórios não podem ser injetados em entidades e as próprias entidades devem, portanto, trabalhar no estado. Então, uma entidade precisa saber mais alguma coisa do domínio, mas também não é permitido que outros objetos de entidade sejam injetados? Algumas dessas coisas fazem sentido para mim, mas outras não. Ainda não encontrei bons exemplos de como criar um recurso completo, pois todos os exemplos são sobre pedidos e produtos, repetindo os outros exemplos várias vezes. Aprendo melhor lendo exemplos e tentei criar um recurso usando as informações que adquiri sobre DDD até agora.

Preciso da sua ajuda para apontar o que eu faço de errado e como corrigi-lo, de preferência com código como "Eu não recomendo fazer X e Y" é muito difícil de entender em um contexto em que tudo já está vagamente definido. Se eu não puder injetar uma entidade em outra, seria mais fácil ver como fazê-lo corretamente.

No meu exemplo, existem usuários e moderadores. Um moderador pode banir usuários, mas com uma regra de negócios: apenas 3 por dia. Fiz uma tentativa de configurar um diagrama de classes para mostrar os relacionamentos (código abaixo):

insira a descrição da imagem aqui

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

A entidade Usuário deve ter um 'is_banned'campo que possa ser verificado $user->isBanned();? Como remover uma proibição? Eu não faço ideia.

Seralize
fonte
No artigo da Wikipedia: "O design orientado a domínio não é uma tecnologia ou uma metodologia". Portanto, a discussão sobre isso é inadequada para esse formato. Além disso, somente você e seus 'especialistas' podem decidir se seu modelo está certo.
1
O @Dodd smith enfatiza "objetos de domínio não podem mostrar métodos para a camada de aplicação" . Observe que o primeiro exemplo de código é a chave para não injetar repositórios em objetos de domínio, algo mais os salva e carrega. Eles não fazem isso eles mesmos. Isso permite que a lógica do aplicativo controle também as transações, em vez do domínio / modelo / entidade / objetos de negócios / ou o que você quiser chamá-los.
FastAl

Respostas:

11

Essa pergunta é um tanto subjetiva e leva a mais uma discussão do que uma resposta direta, que, como alguém já apontou - não é apropriada para o formato de fluxo de pilha. Dito isto, acho que você só precisa de alguns exemplos codificados sobre como lidar com problemas, então vou tentar, apenas para lhe dar algumas idéias.

A primeira coisa que eu diria é:

"objetos de domínio não têm permissão para mostrar métodos para a camada de aplicação"

Isso simplesmente não é verdade - eu estaria interessado em saber de onde você leu isso. A camada de aplicativo é o orquestrador entre interface do usuário, infraestrutura e domínio e, portanto, obviamente precisa invocar métodos em entidades de domínio.

Escrevi um exemplo codificado de como resolveria seu problema. Peço desculpas por estar em C #, mas não conheço PHP - espero que você ainda obtenha a essência da perspectiva da estrutura.

Talvez eu não devesse ter feito, mas modifiquei levemente os objetos do seu domínio. Não pude deixar de sentir que era um pouco falho, pois o conceito de 'BannedUser' existe no sistema, mesmo que a proibição tenha expirado.

Para começar, aqui está o serviço de aplicativo - é assim que a interface do usuário chamaria:

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

Bem direto. Você busca o moderador que bane, o usuário que o moderador deseja banir e chama o método 'Ban' no usuário, passando pelo moderador. Isso modificará o estado do moderador e do usuário (explicado abaixo), que precisa persistir nos repositórios correspondentes.

A classe Usuário:

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

A invariante para um usuário é que ele não pode executar determinadas ações quando banido, portanto, precisamos identificar se um usuário está banido no momento. Para isso, o usuário mantém uma lista de proibições de veiculação que foram emitidas por moderadores. O método IsBanned () verifica todas as proibições de veiculação que ainda precisam expirar. Quando o método Ban () é chamado, ele recebe um moderador como parâmetro. Isso então pede ao moderador que proíba:

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

O invariante para o moderador é que ele só pode emitir 3 proibições por dia. Assim, quando o método IssueBan é chamado, ele verifica se o moderador não possui 3 proibições emitidas com a data de hoje em sua lista de proibições emitidas. Em seguida, ele adiciona a proibição recém-emitida à sua lista e a devolve.

Subjetivo, e tenho certeza que alguém discordará da abordagem, mas espero que lhe dê uma idéia ou como ela pode se encaixar.

David Masters
fonte
1

Mova toda a sua lógica que altera o estado para uma camada de serviço (ex: ModeratorService) que conhece Entidades e Repositórios.

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