DDD 개념을 실제 코드에 적용하는 방법은 무엇입니까? 내부 특정 질문


9

나는 DDD를 공부하고 있으며 현재 실제 코드에 개념을 적용하는 방법을 찾기 위해 고심하고 있습니다. 나는 N-tier에 대해 약 10 년의 경험을 가지고 있기 때문에, 어려움을 겪고있는 이유는 나의 정신 모델이 그 디자인과 너무 연관되어 있기 때문일 가능성이 높습니다.

Asp.NET 웹 응용 프로그램을 만들었으며 웹 모니터링 응용 프로그램이라는 간단한 도메인부터 시작합니다. 요구 사항 :

  • 사용자는 모니터링 할 새 웹앱을 등록 할 수 있어야합니다. 웹 앱의 이름은 알기 쉽고 URL을 가리 킵니다.
  • 웹 앱은 주기적으로 상태 (온라인 / 오프라인)를 폴링합니다.
  • 웹앱은 주기적으로 현재 버전을 폴링합니다 (웹앱에는 특정 마크 업으로 시스템 버전을 선언하는 파일 인 "/version.html"이있을 것으로 예상됩니다).

내 의심은 주로 책임 분담, 각 사물 (유효성, 비즈니스 규칙 등)에 적합한 장소를 찾는 것에 관한 것입니다. 아래에서는 코드를 작성하고 질문과 고려 사항이 포함 된 주석을 추가했습니다.

비판하고 조언하십시오 . 미리 감사드립니다!


도메인 모델

모든 비즈니스 규칙을 캡슐화하도록 모델링되었습니다.

// Encapsulates logic for creating and validating Url's.
// Based on "Unbreakable Domain Models", YouTube talk from Mathias Verraes
// See https://youtu.be/ZJ63ltuwMaE
public class Url: ValueObject
{
    private System.Uri _uri;

    public string Url => _uri.ToString();

    public Url(string url)
    {
        _uri = new Uri(url, UriKind.Absolute); // Fails for a malformed URL.
    }
}

// Base class for all Aggregates (root or not).
public abstract class Aggregate
{
    public Guid Id { get; protected set; } = Guid.NewGuid();
    public DateTime CreatedAt { get; protected set; } = DateTime.UtcNow;
}

public class WebApp: Aggregate
{
    public string Name { get; private set; }
    public Url Url { get; private set; }
    public string Version { get; private set; }
    public DateTime? VersionLatestCheck { get; private set; }
    public bool IsAlive { get; private set; }
    public DateTime? IsAliveLatestCheck { get; private set; }

    public WebApp(Guid id, string name, Url url)
    {
        if (/* some business validation fails */)
            throw new InvalidWebAppException(); // Custom exception.

        Id = id;
        Name = name;
        Url = url;
    }

    public void UpdateVersion()
    {
        // Delegates the plumbing of HTTP requests and markup-parsing to infrastructure.
        var versionChecker = Container.Get<IVersionChecker>();
        var version = versionChecker.GetCurrentVersion(this.Url);

        if (version != this.Version)
        {
            var evt = new WebAppVersionUpdated(
                this.Id, 
                this.Name, 
                this.Version /* old version */, 
                version /* new version */);
            this.Version = version;
            this.VersionLatestCheck = DateTime.UtcNow;

            // Now this eems very, very wrong!
            var repository = Container.Get<IWebAppRepository>();
            var updateResult = repository.Update(this);
            if (!updateResult.OK) throw new Exception(updateResult.Errors.ToString());

            _eventDispatcher.Publish(evt);
        }

        /*
         * I feel that the aggregate should be responsible for checking and updating its
         * version, but it seems very wrong to access a Global Container and create the
         * necessary instances this way. Dependency injection should occur via the
         * constructor, and making the aggregate depend on infrastructure also seems wrong.
         * 
         * But if I move such methods to WebAppService, I'm making the aggregate
         * anaemic; It will become just a simple bag of getters and setters.
         *
         * Please advise.
         */
    }

    public void UpdateIsAlive()
    {
        // Code very similar to UpdateVersion().
    }
}

그리고 Create 및 Deletes를 처리하는 DomainService 클래스는 집계 자체의 문제가 아니라고 생각합니다.

public class WebAppService
{
    private readonly IWebAppRepository _repository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IEventDispatcher _eventDispatcher;

    public WebAppService(
        IWebAppRepository repository, 
        IUnitOfWork unitOfWork, 
        IEventDispatcher eventDispatcher
    ) {
        _repository = repository;
        _unitOfWork = unitOfWork;
        _eventDispatcher = eventDispatcher;
    }

    public OperationResult RegisterWebApp(NewWebAppDto newWebApp)
    {
        var webApp = new WebApp(newWebApp);

        var addResult = _repository.Add(webApp);
        if (!addResult.OK) return addResult.Errors;

        var commitResult = _unitOfWork.Commit();
        if (!commitResult.OK) return commitResult.Errors;

        _eventDispatcher.Publish(new WebAppRegistered(webApp.Id, webApp.Name, webApp.Url);
        return OperationResult.Success;
    }

    public OperationResult RemoveWebApp(Guid webAppId)
    {
        var removeResult = _repository.Remove(webAppId);
        if (!removeResult) return removeResult.Errors;

        _eventDispatcher.Publish(new WebAppRemoved(webAppId);
        return OperationResult.Success;
    }
}

응용 프로그램 계층

아래 클래스는 WebMonitoring 도메인을위한 외부 인터페이스 (웹 인터페이스, 나머지 API 등)를 제공합니다. 현재로서는 셸일 뿐이며 적절한 서비스로 호출을 리디렉션하지만 앞으로 더 많은 로직을 조정하기 위해 앞으로도 계속 확장 될 것입니다 (도메인 모델을 통해 항상 달성 됨).

public class WebMonitoringAppService
{
    private readonly IWebAppQueries _webAppQueries;
    private readonly WebAppService _webAppService;

    /*
     * I'm not exactly reaching for CQRS here, but I like the idea of having a
     * separate class for handling queries right from the beginning, since it will
     * help me fine-tune them as needed, and always keep a clean separation between
     * crud-like queries (needed for domain business rules) and the ones for serving
     * the outside-world.
     */

    public WebMonitoringAppService(
        IWebAppQueries webAppQueries, 
        WebAppService webAppService
    ) {
        _webAppQueries = webAppQueries;
        _webAppService = webAppService;
    }

    public WebAppDetailsDto GetDetails(Guid webAppId)
    {
        return _webAppQueries.GetDetails(webAppId);
    }

    public List<WebAppDetailsDto> ListWebApps()
    {
        return _webAppQueries.ListWebApps(webAppId);
    }

    public OperationResult RegisterWebApp(NewWebAppDto newWebApp)
    {
        return _webAppService.RegisterWebApp(newWebApp);
    }

    public OperationResult RemoveWebApp(Guid webAppId)
    {
        return _webAppService.RemoveWebApp(newWebApp);
    }
}

문제 종결

여기와 다른 질문 에 대한 답변을 수집 한 후 다른 이유로 열었지만 궁극적 으로이 질문 과 같은 시점에 도달하면 이보다 깨끗하고 더 나은 해결책을 찾았습니다.

Github Gist의 솔루션 제안


나는 많이 읽었지만 CQRS와 다른 직교 패턴 및 관행을 적용하는 것을 제외하고는 실제적인 예를 찾지 못했지만 지금이 간단한 것을 찾고 있습니다.
Levidad

1
이 질문은 codereview.stackexchange.com에 더 적합 할 수 있습니다
VoiceOfUnreason

2
저는 n- 계층 앱에 많은 시간을 할애 한 당신을 좋아합니다. 책, 포럼 등에서 만 DDD에 대해 알고 있으므로 의견 만 게시합니다. 유효성 검사에는 입력 유효성 검사와 비즈니스 규칙 유효성 검사의 두 가지 유형이 있습니다. 입력 유효성 검사는 응용 프로그램 계층에, 도메인 유효성 검사는 도메인 계층에 있습니다. WebApp은 애그리 게이트가 아닌 엔터티처럼 보이며 WebAppService는 DomainService보다 응용 프로그램 서비스처럼 보입니다. 또한 귀하의 집계는 인프라 문제인 컨테이너를 참조합니다. 또한 서비스 로케이터처럼 보입니다.
Adrian Iftode

1
예, 관계를 모델링하지 않기 때문입니다. 집계는 도메인 개체 간의 관계를 모델링합니다. WebApp은 원시 데이터와 일부 동작 만 가지고 있으며 다음과 같은 불변을 처리 할 수 ​​있습니다. 현재 버전이 1 일 때 미친 버전 (예 : 버전 3으로
스테핑)

1
ValueObject에 인스턴스 간의 동등성을 구현하는 메소드가있는 한 괜찮습니다. 시나리오에서 버전 값 오브젝트를 작성할 수 있습니다. 시맨틱 버전 관리를 확인하면 불변량 및 동작을 포함하여이 값 객체를 모델링하는 방법에 대한 많은 아이디어를 얻을 수 있습니다. WebApp은 리포지토리와 대화해서는 안됩니다. 실제로 도메인 항목이 포함 된 프로젝트에서 인프라 (리포지토리, 작업 단위)와 직접 또는 간접적으로 (인터페이스를 통해) 관련된 다른 항목을 참조하지 않는 것이 안전하다고 생각합니다.
Adrian Iftode

답변:


1

귀하의 WebApp골재 에 대한 조언이 길어지면 , repository여기 를 당기는 것이 올바른 접근법이 아니라는 것에 전적으로 동의합니다 . 내 경험상 총계는 자신의 상태에 따라 행동이 옳은지 아닌지를 '결정'할 것입니다. 따라서 상태가 아닌 경우 다른 서비스에서 벗어날 수 있습니다. 그러한 검사가 필요한 경우 일반적으로 집계를 호출하는 서비스 (예 :)로 이동합니다 WebAppService.

또한 여러 응용 프로그램이 동시에 집계를 호출하려는 유스 케이스에 도달 할 수 있습니다. 이런 일이 발생하면 오랜 시간이 걸리는 이와 같은 아웃 바운드 통화를 수행하는 동안 다른 용도로 집계를 차단하고 있습니다. 결국 집계 처리 속도가 느려지고 바람직하지 않은 것으로 생각됩니다.

따라서 약간의 유효성 검사를 이동하면 집계가 상당히 얇아지는 것처럼 보일 수도 있지만,으로 이동하는 것이 좋습니다 WebAppService.

또한 WebAppRegistered이벤트 게시를 집계로 옮기는 것이 좋습니다 . 집계는 생성되는 사람이므로 생성 프로세스가 성공하면 해당 지식을 세계에 게시하는 것이 좋습니다.

이것이 @Levidad를 도울 수 있기를 바랍니다!


안녕 스티븐, 입력 주셔서 감사합니다. 나는이 질문 의 동일한 지점에 도달하는 또 다른 질문을 여기에 열었고 마침내이 문제에 대한 Cleaner Solution 시도를 생각해 냈습니다 . 보고 생각을 나누시겠습니까? 나는 그것이 당신의 제안의 방향으로 가고 있다고 생각합니다.
Levidad

물론 레 위드, 내가 볼게요!
Steven

1
방금 'Voice of Unreason'과 'Erik Eidt'에서 답장을 모두 확인했습니다. 둘 다 내가 가지고있는 질문에 대해 언급 할 내용을 따르므로 실제로 가치를 추가 할 수는 없습니다. 그리고 귀하의 질문에 대답하기 위해 : 귀하가 WebApp공유하는 'Cleaner Solution'에서 AR이 되는 방식 은 실제로 집계에 대한 좋은 접근 방식으로 볼 수있는 라인을 따라 있습니다. 이것이 레바 다를 도와 줄 수 있기를 바랍니다!
Steven
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.