How can the user interface know which commands are allowed to run against Aggregate root?

The user interface is separate from the domain, but the user interface should try its best to never allow the user to issue commands that necessarily fail. Consider the following example (pseudo-code):

DiscussionController
    @Security(is_logged)
    @Method('POST')
    @Route('addPost')
    addPostToDiscussionAction(request)
        discussionService.postToDiscussion(
            new PostToDiscussionCommand(request.discussionId, session.myUserId, request.bodyText)
        )

    @Method('GET')
    @Route('showDiscussion/{discussionId}')
    showDiscussionAction(request)
        discussionWithAllThePosts = discussionFinder.findById(request.discussionId)

        canAddPostToThisDiscussion = ???

        // render the discussion to the user, and use `canAddPostToThisDiscussion` to show/hide the form
        // from which the user can send a request to `addPostToDiscussionAction`.
        renderDiscussion(discussionWithAllThePosts, canAddPostToThisDiscussion)

PostToDiscussionCommand
    constructor(discussionId, authorId, bodyText)

DiscussionApplicationService
    postToDiscussion(command)
        discussion = discussionRepository.get(command.discussionId)
        author = collaboratorService.authorFrom(discussion.Id, command.authorId)
        post = discussion.createPost(postRepository.nextIdentity(), author, command.bodyText)
        postRepository.add(post)

DiscussionAggregate
    // originalPoster is the Author that started the discussion
    constructor(discussionId, originalPoster)

    // if the discussion is closed, you can't create a post.
    // *unless* if you're the author (OP) that started the discussion 
    createPost(postId, author, bodyText)
        if (this.close && !this.originalPoster.equals(author))
            throw "Discussion is closed."

        return new Post(this.discussionId, postId, author, bodyText)

    close()
        if (this.close)
            throw "Discussion already closed."

        this.close = true

    isClosed()
        return this.close
  • The user goes to /showDiscussion/123and he sees the discussion with <form>, from which he can send a new message, but only if the discussion is not closed, or the current user has started the discussion.
  • Or the user goes to /showDiscussion/123where he is presented as the REST-as-in-HATEOAS API. A hypermedia link will be provided /addPostonly if the discussion is not closed, or the authenticated user started the discussion.

?

,

canAddPostToThisDiscussion = !discussionWithAllThePosts.discussion.isClosed
    && discussionWithAllThePosts.discussion.originalPoster.id == session.currentUserId

. , , , . (, RESTBucks). - , , .

, , , , , . , , ? ? ? - ?

+4
4

?

- , , , . Ta Da.

- , .

{
    // Notice that these statements are queries
    State currentState = bookOfRecord.getState()
    State nextState = model.computeNextState(currentState, command)

    // This statement is a command
    bookOfRecord.replace(currentState, nextState)
}

: - ; ( " " ) .

, , , -. , .

- , . .

; , , - , , .

, , , . , , , , .

- , , .

, - " ". ; - - , . , , , ​​.

AggregateRoot {
    final Reference<State> bookOfRecord;
    final Model<State,Command> theModel;

    onCommand(Command command) {
        State currentState = bookOfRecord.getState()
        State nextState = model.computeNextState(currentState, command)

        bookOfRecord.replace(currentState, nextState)
    }            
}

, , " ", , AggregateRoot . , , , .

AggregateRoot {
    final Reference<State> bookOfRecord;

    onCommand(Model<State,Command> theModel, Command command) {
        State currentState = bookOfRecord.getState()
        State nextState = model.computeNextState(currentState, command)

        bookOfRecord.replace(currentState, nextState)
    }            
}

, , , . , - .

, .

+2

knowlegde () , , , .

, . Aggregate, , , (!), , . , , . CQRS , 3- : idempotent, , ( , ), .

, - :

DiscussionController
    @Security(is_logged)
    @Method('POST')
    @Route('addPost')
    addPostToDiscussionAction(request)
        discussionService.postToDiscussion(
            new PostToDiscussionCommand(request.discussionId, session.myUserId, request.bodyText)
        )

    @Method('GET')
    @Route('showDiscussion/{discussionId}')
    showDiscussionAction(request)
        discussionWithAllThePosts = discussionFinder.findById(request.discussionId)

     canAddPostToThisDiscussion = discussionService.canPostToDiscussion(request.discussionId, session.myUserId, "some sample body")

        // render the discussion to the user, and use `canAddPostToThisDiscussion` to show/hide the form
        // from which the user can send a request to `addPostToDiscussionAction`.
        renderDiscussion(discussionWithAllThePosts, canAddPostToThisDiscussion)

DiscussionApplicationService
    postToDiscussion(command)
        discussion = discussionRepository.get(command.discussionId)
        author = collaboratorService.authorFrom(discussion.Id, command.authorId)
        post = discussion.createPost(postRepository.nextIdentity(), author, command.bodyText)
        postRepository.add(post)

    canPostToDiscussion(discussionId, authorId, bodyText)
        discussion = discussionRepository.get(discussionId)
        author = collaboratorService.authorFrom(discussion.Id, authorId)
        try
        {
                post = discussion.createPost(postRepository.nextIdentity(), author, bodyText)
                return true
        }
        catch (exception)
        {
                return false
         }

whyCantPostToDiscussion, .

: postRepository.nextIdentity(), ID, - postRepository.getBiggestIdentity(), .

+2

, , . , , .

. , .

CQRS, " " , AR. , . .

+1

, , - HATEOAS, . api ( 3), , ( ). , . . , , - -, . , API, , .

0
source

Source: https://habr.com/ru/post/1680568/


All Articles