Как сделать этот дизайн ближе к правильному 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();? Как снять бан? Я понятия не имею.

Seralize
источник
Из статьи в Википедии: «Доменное проектирование не является технологией или методологией», поэтому обсуждение этого не подходит для этого формата. Кроме того, только вы и ваши «эксперты» могут решить, подходит ли ваша модель.
1
@Todd smith подчеркивает, что «доменным объектам не разрешается показывать методы на уровне приложения» . Обратите внимание на первый пример кода, ключ к тому, чтобы не внедрять репозитории в доменные объекты, что-то еще сохраняет и загружает их. Они сами этого не делают. Это позволяет логике приложения управлять транзакциями также вместо домена / модели / сущности / бизнес-объектов / или как вы хотите их вызывать.
FastAl

Ответы:

11

Этот вопрос несколько субъективен и ведет к большему количеству дискуссий, чем к прямому ответу, который, как указал кто-то другой, не подходит для формата stackoverflow. Тем не менее, я думаю, что вам просто нужны некоторые закодированные примеры того, как решать проблемы, поэтому я попробую, просто чтобы дать вам некоторые идеи.

Первое, что я бы сказал:

«объектам домена не разрешено показывать методы на уровне приложения»

Это просто неправда - мне было бы интересно узнать, откуда вы это прочитали. Прикладной уровень является связующим звеном между пользовательским интерфейсом, инфраструктурой и доменом и поэтому, очевидно, должен вызывать методы на объектах домена.

Я написал зашифрованный пример того, как я буду решать вашу проблему. Я прошу прощения, что это на C #, но я не знаю PHP - надеюсь, вы все равно получите суть структуры.

Возможно, я не должен был этого делать, но я немного изменил ваши доменные объекты. Я не мог удержаться от ощущения, что он был слегка ошибочным, поскольку в системе существует понятие «BannedUser», даже если срок действия запрета истек.

Для начала, вот служба приложений - это то, что пользовательский интерфейс назвал бы:

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
}
Тодд смит
источник