¿Cómo acercar este diseño al DDD adecuado?

12

He leído sobre DDD desde hace días y necesito ayuda con este diseño de muestra. Todas las reglas de DDD me confunden mucho sobre cómo se supone que debo construir algo cuando los objetos de dominio no pueden mostrar métodos en la capa de aplicación; ¿Dónde más orquestar el comportamiento? Los repositorios no pueden inyectarse en entidades y las entidades mismas deben trabajar en estado. ¿Entonces una entidad necesita saber algo más del dominio, pero tampoco se permite inyectar otros objetos de entidad? Algunas de estas cosas tienen sentido para mí, pero otras no. Todavía tengo que encontrar buenos ejemplos de cómo construir una función completa, ya que cada ejemplo trata de Pedidos y Productos, repitiendo los otros ejemplos una y otra vez. Aprendo mejor leyendo ejemplos y he intentado crear una función utilizando la información que he obtenido sobre DDD hasta ahora.

Necesito su ayuda para señalar lo que hago mal y cómo solucionarlo, lo más preferiblemente con el código, ya que "No recomendaría hacer X e Y" es muy difícil de entender en un contexto donde todo ya está vagamente definido. Si no puedo inyectar una entidad en otra, sería más fácil ver cómo hacerlo correctamente.

En mi ejemplo hay usuarios y moderadores. Un moderador puede prohibir a los usuarios, pero con una regla comercial: solo 3 por día. Intenté configurar un diagrama de clase para mostrar las relaciones (código a continuación):

ingrese la descripción de la imagen aquí

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

¿La titularidad del usuario debe tener un 'is_banned'campo con el que se pueda verificar $user->isBanned();? ¿Cómo eliminar una prohibición? No tengo idea.

Seralizar
fuente
Del artículo de Wikipedia: "El diseño impulsado por dominios no es una tecnología o una metodología", por lo tanto, la discusión de este tipo es inapropiada para este formato. Además, solo usted y sus 'expertos' pueden decidir si su modelo es el correcto.
1
@Todd smith hace un gran punto sobre "los objetos de dominio no pueden mostrar métodos a la capa de aplicación" . Tenga en cuenta que el primer ejemplo de código que es la clave para no inyectar repositorios en objetos de dominio es que algo más los guarda y los carga. Ellos no hacen eso ellos mismos. Esto permite que la lógica de la aplicación controle también las transacciones, en lugar del dominio / modelo / entidad / objetos comerciales / o como quiera llamarlos.
FastAl

Respuestas:

11

Esta pregunta es algo subjetiva y lleva a una discusión más que a una respuesta directa, que, como alguien más ha señalado, no es apropiada para el formato stackoverflow. Dicho esto, creo que solo necesita algunos ejemplos codificados sobre cómo abordar los problemas, así que lo intentaré, solo para darle algunas ideas.

Lo primero que diría es:

"los objetos de dominio no pueden mostrar métodos a la capa de aplicación"

Eso simplemente no es cierto. Me interesaría saber de dónde has leído esto. La capa de aplicación es el orquestador entre UI, Infraestructura y Dominio y, por lo tanto, obviamente necesita invocar métodos en entidades de dominio.

He escrito un ejemplo codificado de cómo abordaría su problema. Pido disculpas porque está en C #, pero no conozco PHP; espero que aún consigas la esencia desde una perspectiva de estructura.

Quizás no debería haberlo hecho, pero he modificado ligeramente sus objetos de dominio. No pude evitar sentir que era un poco defectuoso, ya que el concepto de 'Usuario Prohibido' existe en el sistema, incluso si la prohibición ha expirado.

Para empezar, aquí está el servicio de aplicación: esto es lo que la UI llamaría:

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

Muy claro. Busca al moderador que hace la prohibición, al usuario que el moderador desea prohibir y llama al método 'Prohibir' al usuario, pasando al moderador. Esto modificará el estado tanto del moderador como del usuario (explicado a continuación), que luego debe persistir a través de sus repositorios correspondientes.

La clase de usuario:

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

Lo invariable para un usuario es que no pueden realizar ciertas acciones cuando están prohibidos, por lo que debemos poder identificar si un usuario está actualmente prohibido. Para lograr esto, el usuario mantiene una lista de prohibiciones de publicación emitidas por moderadores. El método IsBanned () verifica cualquier prohibición de publicación que aún no haya caducado. Cuando se llama al método Ban (), recibe un moderador como parámetro. Esto luego le pide al moderador que emita una prohibición:

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

La invariante para el moderador es que solo puede emitir 3 prohibiciones por día. Por lo tanto, cuando se llama al método IssueBan, verifica que el moderador no tenga 3 prohibiciones emitidas con la fecha de hoy en su lista de prohibiciones emitidas. Luego agrega la prohibición recién emitida a su lista y la devuelve.

Subjetivo, y estoy seguro de que alguien no estará de acuerdo con el enfoque, pero espero que te dé una idea o cómo puede encajar.

David Masters
fuente
1

Mueva toda su lógica que altera el estado a una capa de servicio (por ejemplo: ModeratorService) que conoce tanto Entidades como Repositorios.

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