이 디자인을 적절한 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

답변:


11

이 질문은 다소 주관적이며 다른 사람이 지적했듯이 스택 오버플로 형식에 적합하지 않은 직접 답변보다 더 많은 토론으로 이어집니다. 즉, 문제를 해결하는 방법에 대한 코딩 된 예제가 필요하다고 생각하므로 아이디어를 얻을 수 있습니다.

내가 말한 첫 번째 것은 :

"도메인 개체는 응용 프로그램 계층에 메서드를 표시 할 수 없습니다"

그것은 사실이 아닙니다. 나는 당신이 이것을 어디서 읽었는지 알고 싶습니다. 응용 프로그램 계층은 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.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.