W ostatniej części praktycznego przewodnika pokazującego, w jaki sposób można połączyć język F# z frameworkiem NServiceBus zdefiniowałem pojedynczą odpowiedzialność:
C# do Messagingu i Queueingu
F# do pozostałych elementów
Z taką koncepcją rozpoczynałem przebudowę systemu komentarzy, którego używam na tym blogu. W ten sposób po stronie C# zostały:
konfiguracja Endpointa NServiceBus
implementacja komponentów NServiceBus
hosting Endpointa NServiceBus
implementacja komponentu Webowego
hosting komponentu Webowego
część testów integracyjnych
Na stronę F# przeszły:
implementacja GitHub API
definicje kontraktów wiadomości NServiceBus
logika systemu
część testów integracyjnych
wszystkie testy jednostkowe
Przejdźmy przez realizację wybranych elementów.
F# - NServiceBus - kontrakty
Policy Comment Registration inicjalizowane jest przez wiadomość typu Command o nazwie RegisterComment. W NServiceBus wiadomość definiuje się poprzez klasę. Język F# umożliwia definiowanie klas, które są rozpoznawane przez język C#. Dzięki temu kontrakty mogą być definiowane w pierwszym języku, a używane w drugim języku.
Definicja RegisterComment wygląda tak:
Comment Registration kończąc realizację swojej odpowiedzialności, publikuje wiadomość typu Event o nazwie CommentRegistered:
A tak wyglądają kontrakty dla Comment Answer Notification:
Dlaczego dwie wiadomości typu Command skoro diagram pokazuje jedno połączenie? Wrócimy do tego w późniejszej części artykułu.
Zobaczmy, w jaki sposób można definiować wiadomość typu Message. Za przykład weźmy Policy GitHub Pull Request Creation:
Definicje kontraktów dla pozostałych Policies wyglądają analogicznie. Przyglądnij się końcówką nazw namespace dla każdego z typów wiadomości. Jest to zdefiniowana konwencja, która instruuje NServiceBusa o typie wiadomości do przetworzenia.
F# - Logika - interfejsy
Interfejs jest łącznikiem pomiędzy kodem napisanym w C# a kodem napisanym w F#. Interfejsy mogą być zdefiniowane w którymkolwiek języku. Postanowiłem umieścić je po stronie F#.
Interfejs logiki dla Comment Registration wygląda tak:
Metoda FormatUserName formatuje nazwę użytkownika. Metoda FormatUserComment formatuje przesłany komentarz. W takiej postaci dane pojawiają się na GitHubie.
Zobaczmy definicję interfejsu logiki dla Comment Answer Notification:
Główną odpowiedzialnością logiki jest zwrócenie informacji o tym, czy powiadomienie powinno być wysłane - metoda IsSendNotification. Pozostałe elementy zwracają dane potrzebne do wysłania komunikatu.
Na koniec przyjrzyjmy się definicji interfejsu logiki dla GitHub Pull Request Creation:
Celem logiki jest zgłoszenie Pull Requesta na GitHubie. Całość podzielona jest na trzy osobne kroki:
utworzenie brancha
aktualizacja pliku
utworzenie Pull Requesta.
Definicje interfejsów dla pozostałych Policies wyglądają analogicznie. Ponownie, przyjrzyj się końcówce w nazwach namespace. W ten sposób pogrupowane są kontrakty związane z interfejsami logiki.
F# - Logika - implementacja
Implementacja interfejsów wygląda tak samo, jak w języku C#. Trzeba utworzyć klasę, która dziedziczy po interfejsie, a następnie zaimplementować funkcjonalność zgodnie z jego sygnaturą. Zobaczmy szczegóły na trzech, znanych nam już przykładach. Na początek logika dla Comment Registration:
ICommentRegistrationPolicyLogic jest pomostem pomiędzy językiem C#, a językiem F#, dlatego implementacja CommentRegistrationPolicyLogic musi uwzględniać specyfikę pierwszego z nich, jak np. obsługa wartości null.
Logika dla Comment Answer Notification wygląda tak:
Implementując logikę, możemy wykorzystywać dobrodziejstwa języka F# jak np. dzielenie kodu na moduły. W powyższym przykładzie pominąłem szczegóły implementacji metody GetBody.execute.
Na koniec zobaczmy kod dla GitHub Pull Request Creation:
Ponownie, dla lepszej czytelności, pominąłem kod dla metod this.UpdateFile oraz this.CreatePullRequest. Ich implementacja wygląda analogicznie jak dla metody this.CreateBranch, która konstruuje nazwę, a następnie tworzy brancha za pomocą metody GitHubApi.CreateRepositoryBranch.execute.
F# - Testy
Zanim przejdziemy do szczegółów implementacji w języku C#, zobaczmy, jak może wyglądać kod testu jednostkowego:
Zasada działania jest taka sama jak dla testów napisanych w C#.
C# - NServiceBus - Konfiguracja Endpointa
Kod F# dołącza się do kodu C# w taki sam sposób, jak łączenie kodu C# <-> C#. Do projektu .csproj dołącza się projekt .fsproj. Interfejsy oraz klasy napisane w F# są rozpoznawane przez C#. Zobaczmy, jak to działa na przykładzie konfigurowania zależności w kontenerze udostępnianym przez NServiceBus:
Zmienna endpoint służy do konfiguracji różnych właściwości NServiceBusa. Metoda RegisterComponents pozwala wstrzykiwać zależności, które można wykorzystywać w Message Handlerach. Wstrzykiwane klasy zakodowane są w języku F#.
C# - NServiceBus - Policy - Saga
W poprzedniej części wspomniałem, że zaprojektowane Policy zgodnie z podejściem ADSD najlepiej implementuje się jako SagęNServiceBusa. Zobaczmy więc, jak taka implementacja może wyglądać. Na początek Comment Registration Policy:
wiadomość RegisterComment tworzy nową instancję Sagi
handler wiadomości wykonuje logikę biznesową, a następnie wysyła żądanie utworzenia GitHub Pull Requesta
Saga, po otrzymaniu odpowiedzi, publikuje zdarzenie CommentRegistered, rozgłaszając zarejestrowanie komentarza
Interfejs logiki oraz kontrakty używane są w taki sam sposób, jakby były napisane w C#, mimo, że zostały zakodowane w F#.
A tak wygląda przetwarzanie wiadomości dla Policy Comment Answer Notification:
Wróćmy do tematu dwóch wiadomości typu Command dla powyższego Policy. Saga może być wystartowana przez jedną z wiadomości:
RegisterCommentNotification
NotifyAboutCommentAnswer
Pierwszą wiadomość wysyła Comment Taking w momencie rejestrowania komentarza. Drugą wiadomość wysyła Policy Comment Answer Notification Event Subscribing. Jeśli popatrzysz na diagram z poprzedniej części, to nie znajdziesz tam ostatniego elementu. Dlaczego? Bardzo ważną zasadą ADSD jest rozdzielenie logicznej odpowiedzialności od fizycznej implementacji, a także fizycznego Deploymentu.
Logicznie, Comment Answer Notification nasłuchuje na Event od Comment Answer.
Fizycznie, NServiceBus desarilizuje pobraną wiadomość z kolejki, a następnie uruchamiana wszystkie Handlery, które obsługują rozpoznany typ wiadomości np. CommentApproved. Jeśli w jednym z nich wystąpi błąd, mechanizm re-try ponowi całe przetwarzanie, co spowoduje, że każdy z Handlerów wykona się ponownie. Jeśli nasze Handlery zostały zaprojektowane jako osobne elementy i mają się wykonywać niezależnie, to w tym, przypadku mamy zależność, której nie chcemy.
Rozwiązaniem jest minimalizowanie powstałej zależności, stąd klasa CommentAnswerNotificationEventSubscribingPolicy pełni rolę pośrednika, którego jedynym zadaniem jest nasłuchiwać na odpowiednie Eventy a następnie przekierowywać dane do CommentAnswerNotificationPolicy.
Jeśli w projekcie pojawi się inny komponent, który również będzie nasłuchiwał na Event CommentApproved, jego implementacja będzie wyglądać tak samo.
Kolejną różnicą pomiędzy logicznym projektem, a fizyczną realizacją jest to, że Comment Answer wysyła dwa osobne zdarzenia na poinformowanie o tym, czy komentarz został zaakceptowany, czy odrzucony:
CommentApproved
CommentRejected
Jest to szczegół implementacyjny, którego również nie chcemy mieć na wspomnianym diagramie. Innym możliwym rozwiązaniem byłoby wysyłanie jednego typu wiadomości ze statusem odpowiedzi.
Na koniec zobaczmy implementację GitHub Pull Request Creation:
Myślę, że teraz już widzisz podział odpowiedzialności pomiędzy przetwarzaniem wiadomości a realizacją logiki biznesowej.
C# - NServiceBus - IT/OPS
Zobaczmy na implementację Comment Taking. Jest to punkt rozdzielający dane, przetwarzane przez różne usługi:
Skomplikowane? Raczej nie :) Standardowe wysłanie wiadomości typu Command w odpowiednie miejsca z odpowiednimi danymi. Elementem łączącym wszystkie dane jest CommentId.
Pożegnanie z NancyFx
Od samego początku komponentem webowym, przyjmującym komentarz od użytkownika był komponent o nazwie Comment Module, zrealizowany za pomocą frameworka NancyFX. Framework ten przestał być utrzymywany dlatego naturalną decyzją było przejście na coś innego. Wybór nie powinien Cię zaskoczyć. Zdecydowałem się na ASP.NET, tym bardziej że NServiceBus wspiera konfigurowanie Endpointa za pomocą tzw. Generic Host. Przemianowałem też nazwę komponentu na Comment Controller.
Test & Deploy
W tych dwóch kwestiach w zasadzie nic się nie zmieniło. Techniki testowania pozostały te same:
automatyczne testy jednostkowe
manualne testy integracyjne.
Część kodu dla testów powędrowała na stronę języka F#.
Jeśli chodzi o wdrażanie, to bez zarzutu sprawdza się narzędzie Fake.
Podsumowanie
Doszliśmy do końca mini serii, w której miałeś/miałaś okazję zobaczyć, w jaki sposób można zaprojektować rozwiązanie wg. idei zawartych w krusie Advanced Distributed System Design (ADSD), a także sposób, w jaki można taki projekt zrealizować używając frameworka NServiceBus oraz języków C# i F#.
Całość implementacji znajdziesz na moim GitHubie. Zachowałem poprzednie rozwiązania w postaci branchy, aby móc analizować zmiany, jakie zachodziły w każdym kolejnym kroku rozwoju systemu.