如何使这种设计更接近适当的DDD?


12

我已经读了几天DDD,并且需要有关此样本设计的帮助。当不允许域对象向应用程序层显示方法时,DDD的所有规则使我非常困惑应该如何构建任何内容。还有什么地方可以协调行为?不允许将存储库注入实体,因此实体本身必须在状态下工作。然后,一个实体需要从域中了解其他信息,但是其他实体对象也不被允许注入?这些事情中有些对我有意义,而另一些则没有。我还没有找到很好的例子来说明如何构建整个功能,因为每个例子都与订单和产品有关,一遍又一遍地重复其他例子。通过阅读示例,我学得最好,并且尝试使用到目前为止所获得的有关DDD的信息来构建功能。

我需要您的帮助来指出我做错了什么以及如何解决,最好是使用代码,因为在已经模糊定义所有内容的情况下,很难理解“我不会建议做X和Y”。如果我无法将一个实体注入另一个实体,则更容易了解如何正确地进行操作。

在我的示例中,有用户和主持人。主持人可以禁止用户,但有一条商业规则:每天仅3个。我尝试建立一个类图以显示关系(下面的代码):

在此处输入图片说明

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

用户实体是否应该有一个'is_banned'可以检查的字段$user->isBanned();?如何取消禁令?我不知道。


在Wikipedia文章中:“域驱动的设计不是技术或方法。”因此,这种讨论不适用于这种格式。同样,只有您和您的“专家”才能确定您的模型是否正确。

1
@Todd Smith在“不允许域对象向应用程序层显示方法”上提出了一个重要观点。请注意,第一个代码示例不将存储库注入域对象的关键是,其他存储和加载它们。他们自己不这样做。这使应用程序逻辑也可以控制事务,而不是域/模型/实体/业务对象/或您想调用的任何对象。
FastAl 2012年

Answers:


11

这个问题在某种程度上是主观的,导致更多的讨论,而不是直接的答案,正如其他人指出的那样,这不适合stackoverflow格式。就是说,我认为您只需要一些有关如何解决问题的编码示例,因此,我将简要介绍一下,仅是为您提供一些想法。

我要说的第一件事是:

“不允许域对象向应用程序层显示方法”

那根本不是真的-我很想知道您从哪里读到的。应用程序层是UI,基础结构和域之间的协调器,因此显然需要在域实体上调用方法。

我已经写了一个编码示例来说明如何解决您的问题。抱歉,它在C#中,但我不知道PHP-希望您仍然可以从结构角度理解要点。

也许我不应该做,但是我已经稍微修改了您的域对象。我禁不住觉得它有点瑕疵,因为即使禁令已过期,系统中也存在“ BannedUser”的概念。

首先,这里是应用程序服务-这就是UI所调用的:

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

非常简单。您获取主持人要禁止的主持人,并取而代之,然后调用该用户的“ Ban”方法,并传递主持人。这将同时修改主持人和用户的状态(在下面说明),然后需要通过其相应的存储库保留该状态。

用户类:

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

对于用户而言不变的是,他们在被禁止时无法执行某些操作,因此我们需要能够确定当前是否在禁止用户。为此,用户维护由主持人发布的服务禁令列表。IsBanned()方法检查是否有任何尚未到期的服务禁令。调用Ban()方法时,它将接收主持人作为参数。然后,这要求主持人发布禁令:

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

主持人不变的是,每天只能发布3个禁令。因此,在调用IssueBan方法时,它将检查主持人是否在其已发布禁令列表中没有3个具有今天日期的已发布禁令。然后,它将新发布的禁令添加到列表中并返回。

主观的,我敢肯定有人会不同意这种方法,但希望它能给您一个想法或如何将其组合在一起。


1

将所有更改状态的逻辑移到服务层(例如:ModeratorService),该服务层同时了解实体和存储库。

ModeratorService.BanUser(User, UserBanRepository, etc.)
{
    // handle ban logic in the ModeratorService
    // update User object
    // update repository
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.