ASP.NET MVC: Method chaining in controllers

ASP.NET MVC: Method chaining in controllers

Zaczynając swoją przygodę z ASP.NET MVC (oraz w ogóle z programowaniem) miałem sporo problemów z utrzymaniem porządku w moich akcjach na kontrolerach. Bardzo często pojawiało się tam mnóstwo warunków i niepotrzebnej logiki. Ten problem trzeba było sensownie rozwiązać, więc z kolegami wypracowaliśmy sobie pewną konwencję, której twardo się trzymaliśmy. Nazywaliśmy ją “biznes akcjami”. Polegało to na tym, że cała logika oddelegowana była do metody napisanej w serwisie, który był wstrzykiwany do konstruktora kontrolera. To podejście miało jednak kilka wad, więc dostałem zadanie które polegało na usprawnieniu tego mechanizmu. Zapraszam do przeczytania tego posta, w którym przedstawię swój sposób na utrzymywanie porządku w kontrolerach.

“Business actions”

Wywoływanie akcji zaczęło sprowadzać się do trzech kroków:

  1. Wywołanie głównej metody serwisowej
  2. Wykonanie odpowiedniego zdarzenia w momencie, gdy metoda nie wyrzuciła żadnego wyjątku – sukces
  3. Wykonanie odpowiedniego zdarzenia w momencie, gdy metoda serwisowa wyrzuciła jakiś wyjątek – fail

Pojawiło się pytanie, jak ugryźć taki problem – wymyśliliśmy “biznes akcje”. Poniżej przykładowy pseudokod:

Niby fajnie, ale moim zdaniem nie jest to czytelne. Przy bardziej skomplikowanych akcjach na pierwszy rzut oka nie widać co tu się tak na prawdę dzieje. W dodatku łatwo się pomylić, wystarczy przypadkiem zamienić kolejność wywoływania metod i trzeba debugować.

Pewnego dnia postanowiliśmy z kolegą usprawnić ten proces. Moim zadaniem było napisanie czytelnego mechanizmu, który w jasny sposób będzie umożliwiał programiście obsłużenie wszystkich trzech kroków w poprawny sposób. Długo myślałem nad tym jak to zrobić i przypomniałem sobie o wzorcu, który powszechnie znany jest z języka JavaScript – Chain of Promises. Dlaczego by nie napisać podobnego mechanizmu dla kontrolerów? No to do dzieła.

Method chaining – podstawowa implementacja

Zacznijmy od zdefiniowania interfejsów które pozwolą nam na wywołanie odpowiednich metod w ustalonej kolejności. Na moje potrzeby zdefiniowałem cztery interfejsy. Pierwszy z nich odpowiada za wykonanie całego mechanizmu. Metoda ta będzie kończyła nasz łańcuch:

Następnie zdefiniowałem kolejne trzy interfejsy pozwalające wywołać takie akcje jak Invoke (do naszej metody serwisowej), OnSuccess oraz OnFail:

Każdy z tych interfejsów dziedziczy po IExecute. Dzięki takiemu zabiegowi możemy wywołać metodę serwisową bez dodatkowych operacji OnSuccess lub OnFail (lub z tylko jedną). Dodatkowo w IFail znalazła się deklaracja metody Always. Jak nazwa wskazuje, możemy przekazać tam akcje, która wykona się niezależnie od tego, czy metoda serwisowa wyrzuci wyjątek, czy nie.

Powyższe interfejsy zaimplementowałem w abstrakcyjnej klasie BaseController, która służy mi jako kontroler bazowy, po którym dziedziczą wszystkie inne kontrolery:

Nie chcąc robić za dużego bałaganu w naszej klasie bazowej, utworzyłem osobną klasę Chain, której rolą jest utrzymywanie odpowiednich delegatów dla metod, które chcę wywołać. Dodatkowo zaimplementowałem tam krótkie metody umożliwiające ustawienie odpowiednich operacji:

Chain – wykorzystanie

Teraz przyszedł czas na implementację metod z wyżej wymienionych interfejsów, zacznijmy od metody Invoke:

Tworzę nową instancję klasy Chain i ustawiam główną operację do wykonania. Metoda zwraca obiekt implementujący interfejs ISuccess (czyli nasz kontroler), więc kolejna metoda jaką wywołamy to OnSuccess:

Pojawił nam się tutaj customowy wyjątek. Jest to HttpException, który zabezpiecza nas przed wywołaniem metody OnSuccess lub OnFail bez użycia głównej metody Invoke. Bo jaki jest sens wykonywania jakiejś operacji OnSuccess, jeżeli żadna główna operacja nie została przed tym wykonana? 🙂 Programista, który się zagapi powinien zobaczyć odpowiedni komunikat w swojej przeglądarce informujący go o tym, że o czymś zapomniał… Analogicznie zaimplementowane zostały metody interfejsu IFail:

Czas na implementację metody Execute. Będzie ona odpowiadać za wykonanie się całego mechanizmu:

Kilka razy wspominałem już o wyrzucanych biznesowych wyjątkach. W moim projekcie zaimplementowałem BusinessException, po którym dziedziczą wyjątki biznesowe. Dzięki takiemu zabiegowi możemy wychwycić złamanie jakiejś reguły biznesowej w aplikacji i na złapaniu takiego wyjątku wyświetlić odpowiedni alert z wiadomością dla użytkownika aplikacji. Inne wyjątki będą łapane i logowane np. przez moduł HTTP i log4net’a.

Wynik końcowy – wykorzystanie zaimplementowanego mechanizmu

Czas na wykorzystanie tego w praktyce. Napiszmy sobie SampleController, który dziedziczy po naszym kontrolerze bazowym, a jego akcje będą wykorzystywały nasz mechanizm:

Wygląda to zdecydowanie lepiej. Teraz można odczytać, że akcja wywołuje główną metodę z serwisu DoSomeOperation z modelem jako jej parametr. Jeśli się powiedzie, wykona operację ActionView, jeśli wyrzuci wyjątek biznesowy, to wykona się ActionRedirect. Metoda DoSomeRegularOperation wykonana zostanie niezależnie od wyniku głównej operacji.

Oczywiście w bazowym kontrolerze MVC nie ma takich metod jak ActionView oraz ActionRedirect. Są to udekorowane metody podstawowe z dodatkowymi funkcjonalnościami jak wyświetlanie odpowiednich alertów, ale to może być temat na osobny post.

A czy Wy macie jakieś ciekawe sposoby na trzymanie porządku w swoich kontrolerach? Może mój przykład totalnie nie pasuje do Twojej konwencji i masz pomysł na lepszy i ciekawszy mechanizm? Może masz pomysł jak jeszcze usprawnić przedstawiony tutaj kod? Zapraszam do dyskusji 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

4 thoughts on “ASP.NET MVC: Method chaining in controllers”

  1. Miło widzieć nowy blog, wpis również interesujący, ale zastanawia mnie po co kontrolerowi wszystkie interfejsy dla execute i po co trzymać chain w kontrolerze, niech metoda invoke zwraca odpowiedni obiekt (fluent interface)

  2. Ostatnio miałem bardzo podobne zadanie do wykonania lecz tyczyło się to Web Api. Pierwsze podejście zrobiłem podobnie jak Ty czyli w Base miałem wszsystkie mozliwe metody (Invoke, Success, Fail ….). Lecz moj przelozony (swietny specialista) nie dal mi approve gdyż jak on stwierdzil po co wprowadzac programiste w blad wystawiajac metody (Success, Fail …) w Base one powinny być dopiero dostępne po wywołaniu Invoke. Tak też zrobiłem, że w Base miałem jedynie metode Invoke ktora to dopiero zawracała interfejs w metodaki (Success, Fail…). Ogolnie fajnie sie czyta i gratuluje bloga 🙂