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


Зі статті Вікіпедії: "Дизайн, керований доменом, не є технологією чи методологією", тому обговорення такого питання є недоцільним для цього формату. Крім того, тільки ви та ваші "експерти" можуть вирішити, чи правильно ваша модель.

1
@Todd Сміт робить велике значення на тему "об’єкти домену не дозволяють показувати методи на рівні додатків" . Зверніть увагу, що перший зразок коду є ключем до того, щоб не вводити сховища в об’єкти домену, а щось інше зберігає та завантажує їх. Вони самі цього не роблять. Це дозволяє також керувати транзакціями логіки програми замість домену / моделі / сутності / бізнес-об'єктів / або будь-якого, що ви хочете їх викликати.
FastAl

Відповіді:


11

Це питання є дещо суб'єктивним і призводить до більшої дискусії, ніж до прямої відповіді, що, як зауважив хтось інший, - не підходить для формату stackoverflow. З цього приводу я думаю, що вам просто потрібні кілька зашифрованих прикладів, як вирішувати проблеми, тому я спробую це зробити, лише щоб дати вам кілька ідей.

Перше, що я сказав би:

"доменним об'єктам заборонено показувати методи на рівні програми"

Це просто неправда - мені було б цікаво дізнатися, звідки ви це читали. Прикладний рівень є оркестром між користувальницьким інтерфейсом, інфраструктурою та доменом, тому, очевидно, потрібно використовувати методи доменних об'єктів.

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

Можливо, я не повинен був цього робити, але я трохи змінив об'єкти вашого домену. Я не міг не відчути, що він злегка хибний тим, що в системі існує поняття "заборонений користувач", навіть якщо термін дії заборони минув.

Для початку ось служба прикладних програм - ось що називав би інтерфейс користувача:

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

Досить прямо вперед. Ви забираєте модератора, який виконує заборону, користувача, якого модератор хоче заборонити, і викликаєте метод "Заборонити" для користувача, передаючи модератору. Це змінить стан як модератора, так і користувача (пояснено нижче), який потребує збереження через відповідні сховища.

Клас користувача:

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
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.