Làm thế nào để thiết kế này gần với DDD thích hợp?


12

Tôi đã đọc về DDD nhiều ngày nay và cần trợ giúp với thiết kế mẫu này. Tất cả các quy tắc của DDD làm cho tôi rất bối rối về cách tôi phải xây dựng mọi thứ khi các đối tượng miền không được phép hiển thị các phương thức cho lớp ứng dụng; nơi nào khác để phối hợp hành vi? Các kho lưu trữ không được phép đưa vào các thực thể và bản thân các thực thể phải hoạt động theo trạng thái. Sau đó, một thực thể cần phải biết một cái gì đó khác từ miền, nhưng các đối tượng thực thể khác cũng không được phép tiêm? Một số trong những điều này có ý nghĩa với tôi nhưng một số thì không. Tôi vẫn chưa tìm thấy các ví dụ hay về cách xây dựng toàn bộ tính năng vì mọi ví dụ là về Đơn hàng và Sản phẩm, lặp đi lặp lại các ví dụ khác. Tôi học tốt nhất bằng cách đọc các ví dụ và đã cố gắng xây dựng một tính năng bằng cách sử dụng thông tin tôi đã đạt được về DDD cho đến nay.

Tôi cần sự giúp đỡ của bạn để chỉ ra những gì tôi đã làm sai và cách khắc phục nó, tốt nhất là với mã là "Tôi sẽ không áp dụng làm X và Y" là rất khó hiểu trong bối cảnh mà mọi thứ chỉ được định nghĩa một cách mơ hồ. Nếu tôi không thể tiêm một thực thể vào một thực thể khác, sẽ dễ dàng hơn để xem làm thế nào để làm điều đó đúng.

Trong ví dụ của tôi có người dùng và người điều hành. Người điều hành có thể cấm người dùng, nhưng với quy tắc kinh doanh: chỉ 3 mỗi ngày. Tôi đã cố gắng thiết lập một sơ đồ lớp để hiển thị các mối quan hệ (mã bên dưới):

nhập mô tả hình ảnh ở đây

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

Quyền lợi của Người dùng 'is_banned'có nên được kiểm tra $user->isBanned();không? Làm thế nào để loại bỏ lệnh cấm? Tôi không có ý kiến.


Từ bài viết trên Wikipedia: "Thiết kế hướng tên miền không phải là công nghệ hay phương pháp luận." Do đó, việc thảo luận như vậy là không phù hợp với định dạng này. Ngoài ra, chỉ có bạn và 'chuyên gia' của bạn có thể quyết định xem mô hình của bạn có đúng không.

1
@Todd smith đưa ra một điểm tuyệt vời về "các đối tượng miền không được phép hiển thị các phương thức cho lớp ứng dụng" . Lưu ý mẫu mã đầu tiên là chìa khóa để không đưa các kho lưu trữ vào các đối tượng miền là một cái gì đó khác lưu và tải chúng. Họ không tự làm điều đó. Điều này cũng cho phép các giao dịch điều khiển logic ứng dụng, thay vì các đối tượng miền / mô hình / thực thể / doanh nghiệp / hoặc bất cứ điều gì bạn muốn gọi chúng.
FastAl

Câu trả lời:


11

Câu hỏi này hơi chủ quan và dẫn đến nhiều cuộc thảo luận hơn là câu trả lời trực tiếp, như một người khác đã chỉ ra - không phù hợp với định dạng stackoverflow. Điều đó nói rằng, tôi nghĩ rằng bạn chỉ cần một số ví dụ được mã hóa về cách giải quyết vấn đề, vì vậy tôi sẽ cung cấp cho nó một shot, chỉ để cung cấp cho bạn một số ý tưởng.

Điều đầu tiên tôi muốn nói là:

"các đối tượng miền không được phép hiển thị các phương thức cho lớp ứng dụng"

Điều đó chỉ đơn giản là không đúng sự thật - Tôi rất muốn biết bạn đã đọc nó từ đâu. Lớp ứng dụng là bộ điều phối giữa UI, Cơ sở hạ tầng & Miền, và do đó rõ ràng cần phải gọi các phương thức trên các thực thể miền.

Tôi đã viết một ví dụ được mã hóa về cách tôi sẽ giải quyết vấn đề của bạn. Tôi xin lỗi rằng nó ở C #, nhưng tôi không biết PHP - hy vọng bạn vẫn sẽ nhận được ý chính từ góc độ cấu trúc.

Có lẽ tôi không nên làm, nhưng tôi đã sửa đổi một chút các đối tượng miền của bạn. Tôi không thể cảm thấy nó hơi thiếu sót, trong đó khái niệm 'BnedUser' tồn tại trong hệ thống, ngay cả khi lệnh cấm đã hết hạn.

Để bắt đầu, đây là dịch vụ ứng dụng - đây là giao diện người dùng sẽ gọi:

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

Khá thẳng về phía trước. Bạn tìm nạp người điều hành thực hiện lệnh cấm, người dùng mà người điều hành muốn cấm và gọi phương thức 'Ban' trên người dùng, thông qua người điều hành. Điều này sẽ sửa đổi trạng thái của cả người điều hành & người dùng (được giải thích bên dưới), sau đó cần duy trì thông qua các kho tương ứng của họ.

Lớp người dùng:

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

Điều bất biến đối với người dùng là họ không thể thực hiện một số hành động nhất định khi bị cấm, vì vậy chúng tôi cần có thể xác định xem người dùng hiện có bị cấm hay không. Để đạt được điều này, người dùng duy trì một danh sách các lệnh cấm phục vụ đã được ban hành bởi người điều hành. Phương thức IsBned () kiểm tra mọi lệnh cấm phục vụ chưa hết hạn. Khi phương thức Ban () được gọi, nó nhận một bộ điều tiết làm tham số. Điều này sau đó yêu cầu người điều hành ban hành lệnh cấm:

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

Điều bất biến đối với người điều hành là nó chỉ có thể phát hành 3 lượt cấm mỗi ngày. Do đó, khi phương thức IssueBan được gọi, nó kiểm tra xem người điều hành không có 3 lệnh cấm được ban hành với ngày hôm nay trong danh sách các lệnh cấm được ban hành. Sau đó, nó thêm lệnh cấm mới ban hành vào danh sách của nó và trả lại.

Chủ quan, và tôi chắc chắn rằng ai đó sẽ không đồng ý với cách tiếp cận này, nhưng hy vọng nó sẽ cho bạn một ý tưởng hoặc làm thế nào nó có thể phù hợp với nhau.


1

Di chuyển tất cả logic của bạn để thay đổi trạng thái sang lớp dịch vụ (ví dụ: ModeratorService) biết về cả Thực thể và Kho lưu trữ.

ModeratorService.BanUser(User, UserBanRepository, etc.)
{
    // handle ban logic in the ModeratorService
    // update User object
    // update repository
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.