Po co rozdzielać klasę CommandHandler za pomocą Handle () zamiast obsługi metody w samym Command

13

Mam część wzorca CQRS zaimplementowanego przy użyciu architektury S # arp w następujący sposób:

public class MyCommand
{
    public CustomerId { get; set; }

    // some other fields
}

public class MyCommandHandler<MyCommand> : ICommandHandler<MyCommand, CommandResult>
{
    Handle(MyCommand command)
    {
        // some code for saving Customer entity

        return CommandResult.Success;
    }
}

Zastanawiam się, dlaczego nie mieć klasy Commandzawierającej zarówno dane, jak i metodę obsługi? Czy jest to rodzaj korzyści z testowalności, gdy trzeba przetestować logikę obsługi poleceń niezależnie od właściwości poleceń? A może jest to częsty wymóg biznesowy, w którym musisz mieć jedno polecenie obsługiwane przez różne implementacje ICommandHandler<MyCommand, CommandResult>?

rgripper
źródło
Miałem to samo pytanie, które warto zobaczyć: blogs.cuttingedge.it/steven/posts/2011/…
rdhaundiyal

Odpowiedzi:

14

Zabawne, to pytanie przypomniało mi dokładnie tę samą rozmowę z jednym z naszych inżynierów na temat biblioteki komunikacji, nad którą pracowałem.

Zamiast poleceń miałem klasy Request, a następnie miałem RequestHandlers. Projekt był bardzo podobny do tego, co opisujesz. Myślę, że część zamieszania, które masz, polega na tym, że widzisz angielskie słowo „polecenie” i od razu myślisz „czasownik, działanie ... itd.”.

Ale w tym projekcie myśl o Dowództwie (lub Żądaniu) jak o liście. Lub dla tych, którzy nie wiedzą, czym jest usługa pocztowa, pomyśl o e-mailu. Jest to po prostu treść oddzielona od sposobu, w jaki należy na nią postępować.

Dlaczego miałbyś to zrobić? W najprostszych przypadkach wzorzec poleceń nie ma żadnego powodu, a klasa mogłaby bezpośrednio wykonywać pracę. Jednak wykonanie odsprzęgania jak w twoim projekcie ma sens, jeśli twoje działanie / polecenie / prośba musi przebyć pewien dystans. Na przykład w poprzek gniazd lub potoków lub między domeną a infrastrukturą. A może w twojej architekturze twoje polecenia muszą być trwałe (np. Moduł obsługi poleceń może wykonać 1 polecenie na raz, z powodu niektórych zdarzeń systemowych, przybywa 200 poleceń i po pierwszych 40 procesach zostaje zamknięty). W takim przypadku, mając prostą klasę tylko do wyświetlania komunikatów, bardzo łatwo jest serializować tylko część komunikatu do JSON / XML / binary / cokolwiek i przekazywać ją w potoku, dopóki program obsługi poleceń nie będzie gotowy do przetworzenia.

Kolejną zaletą oddzielenia polecenia od CommandHandler jest to, że teraz masz opcję równoległej hierarchii dziedziczenia. Na przykład wszystkie Twoje polecenia mogą pochodzić z podstawowej klasy poleceń obsługującej serializację. A może masz 4 z 20 programów obsługi poleceń, które mają wiele podobieństw, teraz możesz czerpać je z podstawowej klasy programu obsługi programów. Jeśli miałbyś mieć obsługę danych i poleceń w jednej klasie, ten rodzaj relacji szybko wymknąłby się spod kontroli.

Innym przykładem oddzielania byłby, gdyby twoje polecenie wymagało bardzo niewielkiej ilości danych wejściowych (np. 2 liczb całkowitych i ciągu), ale jego logika obsługi była wystarczająco złożona, gdzie chciałbyś przechowywać dane w pośrednich zmiennych składowych. Jeśli ustawisz w kolejce 50 poleceń, nie chcesz przydzielać pamięci dla całej pamięci pośredniej, więc oddzielasz Command od CommandHandler. Teraz ustawiasz w kolejce 50 lekkich struktur danych, a bardziej złożone przechowywanie danych jest przydzielane tylko raz (lub N razy, jeśli masz N programów obsługi) przez CommandHandler, który przetwarza polecenia.

DXM
źródło
Chodzi o to, że w tym kontekście polecenie / żądanie nie jest zdalne / trwało / itp. Jest obsługiwane bezpośrednio. I nie widzę, jak rozdzielenie tych dwóch elementów pomogłoby w dziedziczeniu. Utrudniłoby to. Ostatni paragraf też jest swego rodzaju brakiem. Tworzenie obiektów nie jest kosztowną operacją, a 50 poleceń jest liczbą pomijalną.
Euforyczny
@Euphoric: skąd wiesz, jaki jest kontekst? O ile architektura S # arp nie jest czymś wyjątkowym, widzę tylko kilka deklaracji klas i nie masz pojęcia, jak są one używane w pozostałej części aplikacji. Jeśli nie podoba ci się liczba, którą wybrałem jako 50, wybierz coś w rodzaju 50 na sekundę. Jeśli to nie wystarczy, wybierz 1000 na sekundę. Chciałem tylko podać przykłady. Czy nie uważasz, że w tym kontekście będzie miał tyle poleceń?
DXM,
Na przykład dokładna struktura jest widoczna tutaj weblogs.asp.net/shijuvarghese/archive/2011/10/18/… . I nigdzie nie ma tego, co powiedziałeś. A jeśli chodzi o szybkość, problem polega na tym, że użyłeś argumentu „wydajność” bez profilowania. Jeśli masz wymagania dotyczące takiej przepustowości, nie będziesz używać architektury ogólnej, ale zbudujesz coś bardziej wyspecjalizowanego.
Euforyczny
1
Zobaczmy, czy to był twój ostatni punkt: OP poprosił o przykłady, a powinienem powiedzieć, na przykład, najpierw projektujesz prosty sposób i aplikacja działa, następnie skalujesz i rozszerzasz miejsca, w których używasz wzorca poleceń, a następnie uruchamiasz się i dostajesz 10 000 maszyn rozmawiających z twoim serwerem, a twój serwer nadal używa oryginalnej architektury, następnie profilujesz i identyfikujesz problem, a następnie możesz oddzielić dane poleceń od obsługi poleceń, ale tylko po profilowaniu. Czy naprawdę sprawiłbyś, że byłbyś szczęśliwszy, gdybym zawarł to wszystko w odpowiedzi? Poprosił o przykład, dałem mu jeden.
DXM
... więc właśnie przejrzałem opublikowany przez Ciebie post na blogu i wydaje się, że jest on zgodny z tym, co napisałem: rozdziel je, jeśli twoje polecenie musi pokonać pewien dystans. Na blogu wydaje się, że odnosi się do magistrali poleceń, która jest po prostu kolejną potokiem, gniazdem, kolejką komunikatów, esb ... itd.
DXM
2

Normalny wzorzec poleceń dotyczy danych i zachowania w jednej klasie. Ten rodzaj „wzorca dowodzenia / obsługi” jest nieco inny. Jedyną zaletą w porównaniu do normalnego wzorca jest dodatkowa zaleta polegająca na tym, że twoje polecenie nie zależy od ram. Na przykład twoje polecenie może wymagać dostępu do DB, więc musi mieć jakiś kontekst DB lub sesję, co oznacza, że ​​jest zależne od frameworków. Ale to polecenie może być częścią Twojej domeny, więc nie chcesz, aby zależało ono od ram zgodnie z zasadą odwracania zależności . Oddzielenie parametrów wejściowych i wyjściowych od zachowania i posiadanie dyspozytora do ich połączenia może to naprawić.

Z drugiej strony utracisz przewagę zarówno dziedziczenia, jak i kompozycji poleceń. Myślę, że to prawdziwa moc.

Również drobne nitpick. To, że ma w nazwie polecenie Command, nie czyni go częścią CQRS. Chodzi o coś bardziej fundamentalnego. Tego rodzaju struktura może służyć jednocześnie jako polecenie i zapytanie.

Euforyk
źródło
Widziałem link weblogs.asp.net/shijuvarghese/archive/2011/10/18/…, na który zwróciłeś uwagę, ale nie widzę żadnych znaków autobusu w kodzie Arch Arch S # mam. Sądzę więc, że takie rozdzielenie w moim przypadku jedynie rozkłada klasy i rozpryskuje logikę.
rgripper
Hmm, dzięki za wskazanie. Wtedy moja sprawa jest jeszcze trochę gorsza, ponieważ w kodzie mam ICommandProcessor jest IOC'ed i rozwiązany do CommandProcessor (który sam tworzy IOC dla programów obsługi poleceń) - mętna kompozycja. W tym projekcie wydaje się, że nie ma przypadków biznesowych dla więcej niż jednego hadlera dla polecenia.
rgripper