UFO ET IT

명령에서 결과 데이터가 필요할 때 명령 쿼리 분리 (CQS)를 어떻게 적용합니까?

ufoet 2020. 12. 1. 20:09
반응형

명령에서 결과 데이터가 필요할 때 명령 쿼리 분리 (CQS)를 어떻게 적용합니까?


wikipedia의 명령 쿼리 분리 정의 에서 다음과 같이 명시되어 있습니다.

보다 공식적으로 메서드는 참조 적으로 투명하고 부작용이없는 경우에만 값을 반환해야합니다.

명령을 실행하는 경우이 정의에 따라 함수가 데이터를 반환 할 수 없기 때문에 명령이 성공했는지 여부를 어떻게 확인하거나보고해야합니까?

예를 들면 :

string result = _storeService.PurchaseItem(buyer, item);

이 호출에는 명령과 쿼리가 모두 포함되어 있지만 쿼리 부분은 명령의 결과입니다. 다음과 같이 명령 패턴을 사용하여 이것을 리팩토링 할 수 있다고 생각합니다.

PurchaseOrder order = CreateNewOrder(buyer, item);
_storeService.PerformPurchase(order);
string result = order.Result;

그러나 이것은 코드의 크기와 복잡성을 증가시키는 것처럼 보이며 리팩토링하기에 매우 긍정적 인 방향은 아닙니다.

누군가가 작업 결과가 필요할 때 명령 쿼리 분리를 달성하는 더 나은 방법을 줄 수 있습니까?

여기에 뭔가 빠졌나요?

감사!

참고 : Martin Fowler는 cqs CommandQuerySeparation 의 한계에 대해 다음과 같이 말합니다 .

Meyer는 명령-쿼리 분리를 절대적으로 사용하는 것을 좋아하지만 예외가 있습니다. 스택을 팝하는 것은 상태를 수정하는 수정 자의 좋은 예입니다. Meyer는이 방법을 피할 수 있다고 정확하게 말했지만 유용한 관용구입니다. 그래서 저는 가능하면이 원칙을 따르는 것을 선호하지만 제 팝을 얻기 위해 그것을 깨뜨릴 준비가되어 있습니다.

그의 관점에서는 몇 가지 사소한 간단한 예외를 제외하고는 거의 항상 명령 / 쿼리 분리로 리팩토링 할 가치가 있습니다.


이 질문은 오래되었지만 아직 만족스러운 답변을받지 못했기 때문에 거의 1 년 전의 제 의견에 대해 조금 더 자세히 설명하겠습니다.

이벤트 기반 아키텍처를 사용하는 것은 명확한 명령 / 쿼리 분리를 달성 할뿐만 아니라 새로운 아키텍처 선택을 열고 일반적으로 비동기 프로그래밍 모델에 적합하기 때문에 (아키텍처를 확장해야하는 경우 유용함) 많은 의미가 있습니다. 종종 솔루션은 도메인을 다르게 모델링하는 데있을 수 있습니다.

구매 예를 들어 보겠습니다. StoreService.ProcessPurchase구매를 처리하는 데 적합한 명령입니다. 이것은 PurchaseReceipt. 에서 영수증을 반환하는 대신 더 나은 방법입니다 Order.Result. 매우 간단하게하기 위해 명령에서 영수증을 반환하고 여기에서 CQRS를 위반할 수 있습니다. 더 깔끔한 분리를 원하면이 명령은 ReceiptGenerated구독 할 수 있는 이벤트를 발생시킵니다.

도메인에 대해 생각하면 실제로 더 나은 모델이 될 수 있습니다. 계산원에서 체크 아웃 할 때이 과정을 따릅니다. 영수증이 생성되기 전에 신용 카드 수표가 필요할 수 있습니다. 더 오래 걸릴 수 있습니다. 동기식 시나리오에서는 계산원에서 기다리며 다른 작업을 수행 할 수 없습니다.


이 링크는 도움이 될 수 있습니다.


위의 CQS와 CQRS 사이에 많은 혼란이 있습니다 (Mark Rogers가 한 답변에서도 알아 차린 것처럼).

CQRS는 쿼리의 경우 모든 엔터티 및 값 유형이 포함 된 집계 루트에서 완전한 개체 그래프를 작성하지 않고 목록에 표시 할 간단한보기 개체 만 만드는 DDD의 아키텍처 접근 방식입니다.

CQS는 응용 프로그램의 모든 부분에서 코드 수준에서 좋은 프로그래밍 원칙입니다. 도메인 영역 만이 아닙니다. 원칙은 DDD (및 CQRS)보다 훨씬 오래 존재합니다. 데이터를 반환하고 상태를 변경하지 않고 언제든지 호출 할 수있는 쿼리로 애플리케이션의 모든 상태를 변경하는 명령을 엉망으로 만들지 말라고합니다. Delphi를 사용했던 옛날에는 lanquage가 기능과 절차의 차이를 보여주었습니다. '함수 절차'를 코딩하는 것은 나쁜 습관으로 간주되었습니다.

질문에 대답하려면 : 명령을 실행하고 결과를 가져 오는 방법을 생각할 수 있습니다. 예를 들어, void 실행 메서드와 읽기 전용 명령 결과 속성이있는 명령 개체 (명령 패턴)를 제공합니다.

그러나 CQS를 고수하는 주된 이유는 무엇입니까? 구현 세부 정보를 볼 필요없이 코드를 읽기 쉽고 재사용 할 수 있습니다. 코드는 예상치 못한 부작용을 일으키지 않도록 신뢰할 수 있어야합니다. 따라서 명령이 결과를 반환하고 싶고 함수 이름 또는 반환 개체가 명령 결과가있는 명령임을 명확하게 나타내면 CQS 규칙에 대한 예외를 수락합니다. 더 복잡하게 만들 필요가 없습니다. 나는 Martin Fowler (위에서 언급)에 동의합니다.

그건 그렇고 :이 규칙을 엄격하게 따르지 않으면 유창한 API 원칙이 깨지지 않습니까?


나는 다른 사람들이 제안한 이벤트 중심의 아키텍처 제안을 좋아하지만 다른 관점을 던지고 싶습니다. 명령에서 실제로 데이터를 반환하는 이유를 살펴 봐야 할 수도 있습니다. 실제로 결과가 필요합니까, 아니면 실패하면 예외를 던지면서 벗어날 수 있습니까?

나는 이것을 보편적 인 해결책으로 말하는 것이 아니라, "응답을 다시 보내기"모델 대신에 더 강력한 "실패에 대한 예외"로 전환하는 것은 분리가 실제로 내 코드에서 작동하도록하는 데 많은 도움이되었습니다. 물론 더 많은 예외 핸들러를 작성해야하므로 트레이드 오프입니다.하지만 적어도 고려해야 할 또 다른 각도입니다.


문제는 다음과 같습니다. 명령 결과가 필요할 때 CQS를 어떻게 적용합니까?

대답은 : 당신은하지 않습니다. 명령을 실행하고 결과를 얻으려면 CQS를 사용하지 않는 것입니다.

그러나 흑백 독단적 순결은 우주의 죽음이 될 수 있습니다. 항상 가장자리 케이스와 회색 영역이 있습니다. 문제는 CQS의 한 형태이지만 더 이상 순수한 CQS가 아닌 패턴을 만들기 시작한다는 것입니다.

모나드는 가능성입니다. 명령이 무효를 반환하는 대신 Monad를 반환 할 수 있습니다. "void"모나드는 다음과 같이 보일 수 있습니다.

public class Monad {
    private Monad() { Success = true; }
    private Monad(Exception ex) {
        IsExceptionState = true;
        Exception = ex;
    }

    public static Monad Success() => new Monad();
    public static Monad Failure(Exception ex) => new Monad(ex);

    public bool Success { get; private set; }
    public bool IsExceptionState { get; private set; }
    public Exception Exception { get; private set; }
}

이제 다음과 같은 "Command"메소드를 사용할 수 있습니다.

public Monad CreateNewOrder(CustomerEntity buyer, ProductEntity item, Guid transactionGuid) {
    if (buyer == null || string.IsNullOrWhiteSpace(buyer.FirstName))
        return Monad.Failure(new ValidationException("First Name Required"));

    try {
        var orderWithNewID = ... Do Heavy Lifting Here ...;
        _eventHandler.Raise("orderCreated", orderWithNewID, transactionGuid);
    }
    catch (Exception ex) {
        _eventHandler.RaiseException("orderFailure", ex, transactionGuid); // <-- should never fail BTW
        return Monad.Failure(ex);
    }
    return Monad.Success();
}

The problem with grey area is that it is easily abused. Putting return information such as the new OrderID in the Monad would allow consumers to say, "Forget waiting for the Event, we've got the ID right here!!!" Also, not all Commands would require a Monad. You really should check the structure of your application to ensure you have truly reached an edge case.

With a Monad, now your command consumption might look like this:

//some function child in the Call Stack of "CallBackendToCreateOrder"...
    var order = CreateNewOrder(buyer, item, transactionGuid);
    if (!order.Success || order.IsExceptionState)
        ... Do Something?

In a codebase far far away . . .

_eventHandler.on("orderCreated", transactionGuid, out order)
_storeService.PerformPurchase(order);

In a GUI far far away . . .

var transactionID = Guid.NewGuid();
OnCompletedPurchase(transactionID, x => {...});
OnException(transactionID, x => {...});
CallBackendToCreateOrder(orderDetails, transactionID);

Now you have all of the functionality and properness you want with just a bit of grey area for the Monad, but BEING SURE that you aren't accidentally exposing a bad pattern through the Monad, so you limit what you can do with it.


Well, this is a pretty old question but I post this just for the record. Whenever you use an event, you can instead use a delegate. Use events if you have many interested parties, otherwise use a delegate in a callback style:

void CreateNewOrder(Customer buyer, Product item, Action<Order> onOrderCreated)

you can also have a block for the case where the operation failed

void CreateNewOrder(Customer buyer, Product item, Action<Order> onOrderCreated, Action<string> onOrderCreationFailed)

This decrease the cyclomatic complexity on the client code

CreateNewOrder(buyer: new Person(), item: new Product(), 
              onOrderCreated: order=> {...},
              onOrderCreationFailed: error => {...});

Hope this helps any lost soul out there...


Take some more time to think about WHY you want Command Query Separation.

"It lets you use queries at will without any worry of changing system state."

So it is OKAY to return a value from a command to let the caller know it succeeded

because it would be wasteful to create a separate query for the sole purpose of

finding out if a previous command worked properly. Something like this is okay in

my books:

boolean succeeded = _storeService.PurchaseItem(buyer, item);

A disadvantage of your example is that it is not obvious what is returned by your

method.

string result = _storeService.PurchaseItem(buyer, item);

It is not clear what 'result' is exactly.

Using CQS (Command Query Seperation) allows you to make things more obvious

similar to below:

if(_storeService.PurchaseItem(buyer, item)){

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}

Yes, this is more code, but it is more clear what is happening.


I'm really late to this, but there are a few more options that haven't been mentioned (though, not sure if they are really that great):

One option I haven't seen before is creating another interface for the command handler to implement. Maybe ICommandResult<TCommand, TResult> that the command handler implements. Then when the normal command runs, it sets the result on the command result and the caller then pulls out the result via the ICommandResult interface. With IoC, you can make it so it returns the same instance as the Command Handler so you can pull the result back out. Though, this might break SRP.

Another option is to have some sort of shared Store that lets you map results of commands in a way that a Query could then retrieve. For example, say your command had a bunch of information and then had an OperationId Guid or something like that. When the command finishes and gets the result, it pushes the answer either to the database with that OperationId Guid as the key or some sort of shared/static dictionary in another class. When the caller gets back control, it calls a Query to pull back based the result based on the given Guid.

The easiest answer is to just push the result on the Command itself, but that might be confusing to some people. The other option I see mentioned is events, which you can technically do, but if you are in a web environment, that makes it much more difficult to handle.

Edit

After working with this more, I ended up creating a "CommandQuery". It is a hybrid between command and query, obviously. :) If there are cases where you need this functionality, then you can use it. However, there needs to be really good reason to do so. It will NOT be repeatable and it cannot be cached, so there are differences compared to the other two.


CQS is mainly used when implementing Domain Driven Design, and therefore you should (as Oded also states) use an Event Driven Architecture to process the results. Your string result = order.Result; would therefore always be in an event handler, and not directly afterwards in code.

Check out this great article which shows a combination of CQS, DDD and EDA.

참고URL : https://stackoverflow.com/questions/3688068/how-would-one-apply-command-query-separation-cqs-when-result-data-is-needed-f

반응형