Сложная ситуация на моем пути к пониманию DDD - PullRequest
0 голосов
/ 17 января 2019

Заранее благодарю за помощь и внимание!

Мой проект предназначен только для учебных целей, и я полностью запутался в DDD и имею следующую ситуацию:

повсеместный язык моего домена, где у меня есть пользователи и документы.Он говорит следующее:

- A user can create a document. One of the main purpose of my project is to provide users an ability to create different documents. I mean that the documents cannot exist without the users. So,I think that the process of a document creation belongs to my domain.
- A user can send a document for approval. It's one more thing that belongs to the domain. An approval process is one of the most important part of the project. It has its steps that other users must confirm.
- A user can approve a step of approval process.
- A user can reject a step of approval process.

Этого достаточно, чтобы понять и ответить на мой вопрос:

Это нормально, что пользователь может содержать такие методы, как: CreateDocument (params), SendDocumentForApproval (docId), ApproveApprovalStepOfDocument (stepId)?

Я смущен этим, потому что в коде это выглядит немного странно.

Например, для процесса создания документа у нас есть что-то вроде этого:

    public async Task<bool> CreateDocumentCommandHandler(CreateDocumentCommand command)
{

    //We have our injected repositories
    User user = await _userRepository.UserOfId(command.UserId);

    Document document = User.CreateDocoment(command.*[Params for the document]);

    _documentRepostiory.Add(document);

     // It raises event before it makes a commit to the database
     // It gets event from an entity. The entity keeps it as readonly collection.
     // Here it raises DocumentCreatedEvent. This event contains logic which concerns
     // creation some additional entities for the document and log actions.
    await _documentRepository.UnitOfWork.SaveEntitiesAsync();
}

Процесс утверждения:

//The first try out to model this process:
public async Task<bool> SendDocumentForApprovalCommandHandler(SendDocumentForApprovalCommand command)
{
    //We have our injected repositories

    User user = await _userRepository.UserOfId(command.UserId);

    //Here I have some problems.
    //Is it okay that the method returns the document?
    //The method that is placed inside the User has this logic:
    //public Document SendDocumentForApproval(int docId)
    //{
    //   Document document = this.GetDocument(docId);
    //   
    //   //Inside this method ChangedStatusToApproving is created
    //   document.SetStatusToApproving();
    //   return document;
    //}
    Document document = User.SendDocumentForApproval(command.DocId);

    _documentRepostiory.Upadate(document);

     // It raises event before it makes a commit to the database
     // It gets event from an entity. The entity keeps it as readonly collection.
     // Here it raises ChangedStatusToApproving. This event contains logic which concerns
     // creation some additional entities for the document and log actions.
    await _documentRepository.UnitOfWork.SaveEntitiesAsync();
}
//Is it okay to do something like the command handler above?

//The second one:
public async Task<bool> SendDocumentForApprovalCommandHandler(SendDocumentForApprovalCommand command)
{
    //We have our injected repositories
    User user = await _userRepository.UserOfId(command.UserId);

    //The same one as we have in the previous method.
    //But here I don't want to put the logic about the changing status of the doucnent inside it.
    Document document = User.SendDocumentForApproval(command.DocId);
    //I see that it breaks the method above (SendDocumentForApproval)
    //Now It doesn't mean anything for our domain, does it?
    //It is only getter like User.GetDocument or we can even do it
    //by using repository - documentRepository.DocumentOfId(docId)
    document.SetStatusToApproving();

    _documentRepostiory.Upadate(document);

    await _documentRepository.UnitOfWork.SaveEntitiesAsync();
}

// So, I think the first one is better, isn't it? It follows the ubiquitous language. 

//And here is the final question: Why can't I do it like this:
public async Task<bool> SendDocumentForApprovalCommandHandler(SendDocumentForApprovalCommand command)
{
    //Here we don't want to use the userRepository. We don't need at all
    //Here as a consequence we also don't need a user entity
    //Everything what we need is:
    Document document = _documentRepository.DocOfId(command.DocId);
    document.ForApproval();

    _documentRepostiory.Upadate(document);

    await _documentRepository.UnitOfWork.SaveEntitiesAsync();
}
//I think that the last approach breaks the ubiquitous language and we're about to having an anemic model.
//But here we have only two queries to the database because we don't need a user.
//Which of the approaches is better? Why? How can I do it more correctly if I want to apply DDD?

Я хочу объяснить мои мысли более подробно.Давайте посмотрим на пользователя.Они управляют документами.Документ не может существовать без пользователя.Означает ли это, что Пользователь является агрегированным корнем, через который нам нужно создавать, обновлять, удалять его агрегаты.

И документ также является агрегированным корнем, поскольку он содержит процесс подтверждения.ApprovalProcess не может существовать без документа.

Значит ли это, что мне нужно сделать что-то вроде этого:

public async Task<bool> SendDocumentForApprovalCommandHandler(SendDocumentForApprovalCommand command)
{
    Document document = _documentRepository.DocumentOfId(command.DocId);

    document.SendForApproval();

    _documentRepository.SaveChangesAsync();//Raise a domain event - SentDocumentForApprovalEvent
}

// Here we have a handler for the event SentDocumentForApprovalEvent
public async Task SentDocumentForApprovalEventHandler(SentDocumentForApprovalEvent sentDocumentForApprovalEvent)
{
    //Now I want to create an approval process for the document
    //Can I do the next thing:
    ApprovalProcess process = new ApprovalProcess(sentDocumentForApprovalEvent.DocId);

    _approvalProcessRepository.Add(process);

    _approvalProcessRepository.SaveEntitiesAsync();//Raise a domain event - InitiatedApprovalProcessEvent

    //Or Should I create the approval process through Document?

    //Looks terrible due to we need to call the repostiory amd
    ApprovalProcess process = Document.InitiateApprovalProcess(sentDocumentForApprovalEvent.DocID);//Static method

    _approvalProcessRepository.Add(process);

    _approvalProcessRepository.SaveEntitiesAsync();

    //log
}

// Here we have a handler for the event InitiatedApprovalProcessEvent
public async Task InitiatedApprovalProcesEventHandler(SentDocumentForApprovalEvent sentDocumentForApprovalEvent)
{
    //The same question as we have with handler above.
    //Should I create steps trough the approval process or just with the help of constructor of the step?

    //log
}

Большое спасибо и извините за мой ужасный английский!С наилучшими пожеланиями

Ответы [ 2 ]

0 голосов
/ 19 января 2019

Вы движетесь в правильном направлении, пользователь и документ являются совокупными, поскольку они созданы в отдельных транзакциях.Когда речь заходит о том, кто на кого ссылается, принцип масштабируемости IDDD гласит, что агрегаты должны ссылаться на агрегаты только через их идентификаторы.

Я думаю, что придерживаясь вездесущего языка, ваш код должен выглядеть примерно так

class User {
    private UserId id;
    private String name;    

    User(String name) {
        this.id = new UserId();
        this.name = name;            
    }

    Document createDocument(String name) {
        Document document = new Document(name);        
        document.createdBy(this);
        return document;
    }

    Document approve(Document document) {
        document.approved();
        return document;
    }
}

class Document {
    private DocumentId id;
    private String name;
    private UserId userId;
    private Boolean isApproved;

    Document(String name) {
        this.id = new DocumentId();
        this.name = name;
    }

    void createdBy(UserId userId) {
        this.userId = userId;
    }    

    void approved() {
        this.isApproved = true;
    }

}

// User creation
User user = new User("Someone");
userRepository.save(user);

//Document creation
User user = userRepository.find(new UserId("some-id"))
Document document = user.createDocument("important-document")
documentRepository.save(document)

// Approval
User user = userRepository.find(new UserId("some-id"))
Document document = documentRepository.find(new DocumentId("some-id")) 
document = user.approve(Document)

Я бы настоятельно рекомендовал прочитать серию из трех частей совокупного дизайна бумаги Вона Вернона лучший агрегированный дизайн

0 голосов
/ 17 января 2019

Это нормально, что пользователь может содержать такие методы, как: CreateDocument (params), SendDocumentForApproval (docId), ApproveApprovalStepOfDocument (stepId)?

В большинстве моделей предметной области метод принадлежит сущности, которая управляет состоянием, которое будет изменено.

Document document = User.SendDocumentForApproval(command.DocId);
_documentRepository.Update(document);

Тот факт, что ваш пример обновляет хранилище документов, является большим намеком на то, что это документ, который изменяется, и поэтому мы обычно ожидаем увидеть SendDocumentForApproval в качестве метода для документа.

document.SendDocumentForApproval(command.UserId)
_documentRepository.Update(document);

(Да, код не читается как письменный или разговорный английский.)

Когда создает новый документ ... шаблоны создания странные. Уди Дахан предполагает, что в модели вашего домена всегда должна быть какая-то сущность, отвечающая за создание других сущностей, но я не уверен, что с результатом на самом деле легче работать в долгосрочной перспективе.

Как мы можем смоделировать бизнес-процесс утверждения

Общий ответ: бизнес-процессы - это протоколы , то есть обычно их можно моделировать как конечный автомат. Вот то состояние, в котором мы сейчас находимся, вот какая-то новая информация из внешнего мира, вычисли последствия.

(Зачастую модель данных для процесса просто выглядит как история событий; задача модели domain состоит в том, чтобы затем взять новую информацию и вычислить нужные события для хранить в истории. Вам не нужно делать это таким образом, но есть интересные возможности, когда вы можете).

...