この設計を適切なDDDに近づける方法は?


12

私は数日間DDDについて読みましたが、このサンプルデザインの支援が必要です。ドメインオブジェクトがアプリケーション層にメソッドを表示することを許可されていない場合、DDDのすべてのルールにより、どのように構築するかについて非常に混乱しています。行動を調整する他の場所は?リポジトリーをエンティティーに注入することは許可されていないため、エンティティー自体が状態で動作する必要があります。次に、エンティティはドメインから何か他のものを知る必要がありますが、他のエンティティオブジェクトも注入することはできませんか?これらのことのいくつかは私には理にかなっていますが、いくつかはそうではありません。すべての例は注文と製品に関するものであり、他の例を繰り返し繰り返しているため、機能全体を構築する方法の良い例をまだ見つけていません。私は例を読むことで最もよく学び、これまでにDDDについて得た情報を使用して機能を構築しようとしました。

私が間違っていることとそれを修正する方法を指摘するためにあなたの助けが必要です。エンティティを別のエンティティに注入できない場合、適切に実行する方法を簡単に確認できます。

私の例では、ユーザーとモデレーターがいます。モデレーターはユーザーを禁止できますが、ビジネスルールがあります:1日あたり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);

Userエンティティには、'is_banned'チェックできるフィールドが必要$user->isBanned();ですか?禁止を解除する方法は?何も思いつきません。


ウィキペディアの記事から:「ドメイン駆動設計は技術や方法論ではありません。」したがって、このような議論はこの形式には不適切です。また、あなたとあなたの「専門家」だけがあなたのモデルが正しいかどうかを決めることができます。

1
@Todd smithは、「ドメインオブジェクトはアプリケーション層にメソッドを表示することを許可されていません」という点で優れています。最初のコードサンプルは、リポジトリをドメインオブジェクトに注入しないための鍵であり、他の何かが保存してロードすることに注意してください。彼らは自分でそれをしません。これにより、ドメイン/モデル/エンティティ/ビジネスオブジェクト/またはそれらを呼び出すものの代わりに、アプリロジックがトランザクションを制御することもできます。
FastAl

回答:


11

この質問はやや主観的であり、直接的な答えよりも議論の余地があります。他の誰かが指摘したように、これはstackoverflow形式には適していません。そうは言っても、問題に取り組む方法についてコード化された例がいくつか必要だと思うので、いくつかのアイデアを与えるために試してみます。

私が最初に言うことは:

「ドメインオブジェクトは、アプリケーション層にメソッドを表示できません」

それは単に真実ではありません-あなたがこれをどこから読んだか知りたいです。アプリケーション層は、UI、インフラストラクチャ、およびドメイン間のオーケストレーターであるため、明らかにドメインエンティティのメソッドを呼び出す必要があります。

私はあなたの問題にどのように取り組むかのコード化された例を書いた。C#であることをおaびしますが、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'メソッドを呼び出してモデレーターを渡します。これにより、モデレーターとユーザー(以下で説明)の両方の状態が変更され、対応するリポジトリーを介して永続化する必要があります。

Userクラス:

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

モデレーターの不変条件は、1日に3回しか禁止できないことです。したがって、IssueBanメソッドが呼び出されると、モデレーターが発行された禁止のリストに今日の日付を持つ3つの発行された禁止がないことを確認します。次に、新しく発行された禁止をリストに追加して返します。

主観的で、誰かがこのアプローチに反対することは間違いないでしょうが、うまくいけばアイデアやそれがどのように適合するかを教えてくれるでしょう。


1

状態を変更するすべてのロジックを、エンティティとリポジトリの両方を知っているサービス層(ModeratorServiceなど)に移動します。

ModeratorService.BanUser(User, UserBanRepository, etc.)
{
    // handle ban logic in the ModeratorService
    // update User object
    // update repository
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.