Wstęp
Zdecydowałem się uruchomić kolejną “ścieżkę” na tym blogu. Polega ona na implementacji pewnego problemu i zaprezentowaniu tutaj rozwiązania w całości, jako solution Visual Studio. Jest to mój pierwszy taki post, jednak w przyszłości planuję dalej “podążać za białym królikiem” aż do jego nory ozdobionej kratką #.
Uważam, że przeglądanie cudzego kodu to najlepszy sposób na poznanie możliwości języka, naukę pewnych praktyk programistycznych oraz weryfikację własnych przyzwyczajeń, dlatego też chcę podzielić się moim sposobem kodowania. Z drugiej strony liczę na komentarze Czytelników dotyczące przedstawionych rozwiązań. Co jest źle pomyślane, co można ulepszyć, co się podoba a co nie? W ten sposób może wszyscy się nauczymy czegoś nowego. Dodatkowo – czuję perwersyjną satysfakcję gdy jeździ się po mnie jak po burej suce, więc zapraszam do ostrej krytyki i podzielenia się lepszymi propozycjami :). Poziom szczegółowości – dowolny, od pojedynczych metod do całego podejścia do problemu.
Ale koniec pierdół, przejdźmy do właściwej części:
.ZIP
Plik z kodem źródłowym jest dostępny do pobrania na stronie SAMPLES.
Treść
Na forum CodeGuru regularnie pojawiają się pytania o architekturę rozwiązań klient/serwer, zwykle w oparciu o zwykłe Sockety lub klasy pomocnicze z przestrzeni nazw System.Net.Sockets. W tym poście zaprezentuję przykładowe rozwiązanie takiego problemu.
Elementy wykorzystane w tym programie
- mechanizm gniazd w C#
- wątki
- zdarzenia
- serializacja
- typy ogólne (generics)
- metody rozszerzające (extension methods)
- wyrażenia lambda (lambda expressions)
Fizyczna struktura projektów
Całość składa się z kilku projektów:
- TcpIp.Server – biblioteka oferująca nasłuchiwanie na połączenia przychodzące od klientów oraz komunikację z nimi
- TcpIp.Client – biblioteka z zaimplementowanym klientem łączącym się do uruchomionego wcześniej serwera
- TcpIp.Common – struktury współdzielone pomiędzy obie strony komunikacji
- TcpIp.Extensions – miejsce na dodawanie własnych rozszerzeń do oferowanej funkcjonalności
- TcpIp.Server.Console – konsolowa aplikacja uruchamiająca serwer
- TcpIp.Client.WinForms – okienka wykorzystujące funkcjonalność klienta
Na największą atencję zasługują pierwsze cztery biblioteki, ponieważ to one będą ewentualnie wykorzystane gdzieś indziej. Testowa konsola i okna są zrobione ot tak, żeby można było po prostu zobaczyć jak działa całość. Jak widać – nie ma testów jednostkowych. Moim zdaniem dołączenie ich jako milczącego dodatku spowodowałoby więcej szkód niż korzyści.
Na co zwrócić uwagę
Ten post nie ma być dokumentacją całego rozwiązania, jedynie wskazywać pewne punkty w implementacji warte szczególnego zainteresowania. Więc po kolei:
Przesyłane dane: MessageBase
Wszystkie przesyłane informacje grupowane są w serializowane klasy dziedziczące z MessageBase (np LoginMessage zawiera Id, a TextMessage nadawcę, odbiorcę, datę oraz treść wiadomości). We wspomnianych wątkach na CG często radą jest implementacja własnego protokołu tekstowego, co oczywiście działa, ale wymaga dużo więcej zachodu i jest o wiele trudniejsze w utrzymaniu
Dodawanie dodatkowych informacji: Package, PackageHeader
Klasa Package to po prostu opakowanie właściwych informacji przesłanych pomiędzy klientem i serwerem. Składa się ona jedynie z nagłówka (PackageHeader) oraz samej wiadomości. Nagłówek w obecnej postaci nie jest zbytnio przydatny, ponieważ przechowuje on tylko czas utworzenia wiadomości przed jej wysłaniem. Można tam jednak dodać kolejne pola wymagane przez konkretne zapotrzebowanie biznesowe, jak na przykład informacje o aktualnym użytkowniku w którego kontekście wykonywana jest operacja wysłania wiadomoćci.
Przetwarzanie odebranych danych: IMessageHandler, MessageHandlerManager
Po raz kolejny odwołam się do forum CG, na którym często polecanym sposobem na poradzenie sobie z komunikacją jest odebranie pierwszej linii wiadomości (zawierającej tekstowy identyfikator operacji do wykonania), a następnie w mega-ogromnej instrukcji SWITCH wykonywanie odpowiednich czynności. Pozwolę sobie nie wymieniać wad takiego rozwiązania tylko przejdę od razu do mojej propozycji. Po pierwsze – klasy serwera i klienta mają służyć do obsługi KOMUNIKACJI, bez zagłębiania się w szczegóły przesyłanych danych. Do tego założeniem moim było udostępnienie możliwości rozszerzenia istniejącej implementacji bez konieczności zmian w kodzie klienta i serwera – czyli taki minifrejmłorczek.
Dlatego też postanowiłem, że każdemu typowi przesyłanych wiadomości będzie odpowiadać klasa implementująca interfejs IMessageHandler. Deklaruje on tylko jedną metodę, HandleMessage, i typ poradzenia sobie z wiadomością jest całkowicie niezależny od reszty systemu. Przykładowo, zaimplementowany przeze mnie LoginMessageHandler przypisuje odpowiednie Id komponentowi odbierającemu tą wiadomość, a serwerowa implementacja TextMessageHandler przekazuje wiadomość tekstową do dalszego przesłania pod numer odbiorcy.
Delegacją wiadomości do odpowiednich handlerów, jak też zachowaniem mapowania pomiędzy typem wiadomości a instancją handlera, zajmuje się klasa MessageHandlerManager.
Zaproponowana implementacja posiada ograniczenie – tylko jeden handler może obsługiwać jeden typ wiadomości. Nic nie stoi jednak na przeszkodzie, aby doimplementować mechanizm łączenia handlerów w łańcuchy wykonujące po kolei cały zestaw czynności. W ten sposób można by było zaplanować całą skomplikowaną procedurę wywoływaną krok po kroku przy nadejściu danych informacji.
Słowo o serwerze: klasy Server i SingleClientChannel
Architektura serwera jest raczej standardowa. Głównym jej składnikiem są dwie klasy, Server oraz SingleClientChannel, służące odpowiednio do oczekiwania na nowe nadchodzące połączenia oraz do komunikacji z jednym konkretnym klientem. Odpalenie wątku i zapisywanie/odczytywanie ze strumienia – koniec.
Automatyczne wykrywanie serwera: Client.AutoReconnect
W klasie Client zaimplementowałem możliwość automatycznego łączenia się z serwerem aż do skutku, co X milisekund. W ten sposób użytkownik klasy nie musi martwić się sprawdzaniem połączenia – wywołuje Connect() na kliencie i to tyle.
Jak poinformować system o nadejściu wiadomości: MessageNotifications
O ile nadejście wiadomości na serwerową stronę systemu zawsze będzie w całości obsłużone przez odpowiednie handlery, o tyle strona kliencka powinna oferować mechanizm powiadamiania całego systemu o tym fakcie. Do tego służy oczywiście mechanizm zdarzeń. Dlatego też w projekcie TcpIp.Client.WinForms proponuję statyczną klasę MessageNotifications, która umożliwia wywołanie własnych zdarzeń przez handlery znajdujące się w tej samej bibliotece. Odpowiednie komponenty, jak na przykład główne okno aplikacji, może podpiąć się do nich i wyświetlać treść wiadomości do pola tekstowego – co też właśnie czyni.
Rozszerzalnośc rozwiązania: TcpIp.Extensions
Dodałem do projektu kilka elementów ukazujących możliwości rozszerzania tego mechanizmu własnym kodem. Przykładowa biblioteka Extensions zawiera implementację nowego typu wiadomości (LoginWithPasswordMessage), a konsolowa aplikacja rejestruje do niej handler całkowicie niezależnie od standardowo odbywających się rejestracji. Dodatkowo w konsolowej aplikacji serwerowej dopisałem nowy handler (IgnoringAnonymousHandler), który powoduje zignorowanie wiadomości tekstowych nadesłanych od klienta nieposiadającego prawidłowego numeru Id. Tam też podmieniłem standardowo rejestrowany handler.
“A teraz coś z zupełnie innej beczki”: ControlExtensions.InvokeIfRequired
Nie ma to bezpośredniego związku z komunikacją klient/serwer, jednak pozwolę sobie zwrócić tutaj uwagę na fajny sposób radzenia sobie z aktualizacją UI z wątku innego niż główny. Chodzi o metodę Procent.Samples.TcpIp.Client.WinForms.Utils.ControlExtensions.InvokeIfRequired.
Podsumowanie
Moim celem była prezentacja sposobu rozwiązania problemu komunikacji TCP/IP pomiędzy serwerem i klientem. Można mieć wątpliwości co do sensowności niektórych elementów (np. brak obsługi hasła podczas logowania), jednak nie o to tutaj chodziło. Chętnie poznam opinie i uwagi dotyczące załączonego kodu, więc komentarze jak zawsze mile widziane. Mam jednocześnie nadzieję, że chociaż kilka osób znalazło w tym projekcie trochę inspiracji i nauczyło się czegoś nowego.
Odnośnie opisu, to według mnie atrakcyjniejsze byłoby podejście top-down, w którym najpierw przedstawiana byłaby koncepcja całego rozwiązania (ot na przykład wyjaśnienie, co to jest pakiet, z czego jest zbudowany, czy można go rozszerzać), a dopiero dalej, jeśli w ogóle, poruszane byłyby zagadnienia związane z implementacją. Myślę, że to właśnie koncepcja jest istotna i może zachęcić do zerknięcia w kod, a nie odpowiedzialności poszczególnych klas. Proponowałbym również przenieść link do archiwum z kodem źródłowym na sam koniec notki, wydaje mi się, że byłby wtedy bardziej zauważalny.
Mam pewną uwagę do organizacji kodu: byłoby dużo wygodniej, gdybyś wrzucił kod klientów i serwera do jednej solucji. Do jego uruchomienia wystarczyłaby wtedy tylko jedna instancja Visual Studio z poprawnie skonfigurowaną opcją Multiple Startup Projects.
Co do implementacji, to uważam, że stosowanie serializacji/deserializacji w odniesieniu do całych pakietów, jak również parowanie typ CLR/rodzaj pakietu to nie są najszczęśliwsze decyzje ze względu na wersjonowanie, wydajność, interoperacyjność i możliwość ponownego wykorzystania kodu.
Problemy z wersjonowaniem mogłyby wystąpić, gdyby użytkownik łączący się z serwerem dysponował starszą wersją oprogramowania klienckiego. Ze względu na niezgodność lub brak definicji typów reprezentujących pakiety, zabawa z komunikatorem ograniczyłaby się u niego do łapania wyjątków, a jedynym pocieszeniem byłaby myśl, że w tym samym momencie krztusi się również serwer.
Kolejnym minusem przyjętego podejścia jest konieczność deserializacji każdego pakietu po stronie serwera. W efekcie tracimy czas procesora i pamięć na stworzenie grafu obiektów, z których część jest przydatna jedynie po stronie klienta. Z tym wiąże się jeszcze jeden efekt uboczny – każda alokacja pamięci na stercie zarządzanej to prowokowanie GC. Ponadto przy częstszym odśmiecaniu pamięci istnieje ryzyko, że więcej krótko żyjących obiektów będzie promowanych do starszych pokoleń. Z tych względów po stronie serwera powinniśmy unikać niepotrzebnych alokacji pamięci.
Odnośnie rozpoznawania rodzaju pakietu na podstawie typu CLR, to podejście takie ogranicza możliwość wielokrotnego wykorzystania kodu. Załóżmy, że nasz protokół wykorzystuje kilka rodzajów pakietów, które mają taką samą strukturę, a różnią się jedynie znaczeniem. Niestety nie możemy reprezentować ich z wykorzystaniem jednego typu. Musimy utworzyć typ bazowy, a na jego podstawie tyle pustych typów, ile mamy rodzajów pakietów.
Ponadto podejście takie niejako wymusza na nas użycie jakiegoś zastępstwa dla mechanizmu zdarzeń – u Ciebie padło na interfejs IMessageHandler i statyczną klasę MessageHandlerManager. Rozwiązanie to ma pewne wady, z których największą jest to, że handlery nie są związane z konkretną instancją serwera lub klienta, lecz z domeną aplikacji. W praktyce ogranicza to ilość takich instancji, jakie możemy utworzyć w programie, do jednej. Sensownym rozwiązaniem problemu byłoby stworzenie wspólnej klasy bazowej dla klienta i serwera. W ten sposób np. problem niestandardowego mechanizmu obsługi właściwości i zdarzeń rozwiązali twórcy WPF, wprowadzając klasę DependencyObject, z której wywodzą się tak pozornie odległe od siebie byty, jak pola tekstowe i przekształcenia liniowe z wykorzystaniem macierzy.
Dopracowania lub w zasadzie opracowania wymagałaby metoda konfiguracji serwera i klientów. Obecne rozwiązanie ze statyczną klasą Settings jest bardzo prowizoryczne. W zasadzie wystarczyłoby utworzyć właściwości Port i (u klienta) Host i skonsumować je w kodzie. (W przypadku serwera trzebaby jeszcze dodatkowo przenieść tworzenie instancji TcpListener do metody Start.) Klasa Settings mogłaby się przydać do zainicjalizowania tych właściwości z poziomu programów demonstracyjnych. Domyślam się, że obecne rozwiązanie wynika po prostu z lenistwa ;).
O ile wysyłanie wiadomości przez klienta odbywa się z wątku głównym, o tyle odbieranie wiadomości ma miejsce w wątku, w którym klient nasłuchuje informacji od serwera. Z obserwacji wiem, że posługiwanie się Invoke/BeginInvoke, a w szczególności zrozumienie potrzeby synchronicznego dostępu do interfejsu użytkownika sprawia problemy wielu osobom. Z tego względu warto zadbać, by wielowątkowa natura tworzonych przez nas klas była jedynie szczegółem implementacji, niewidocznym dla użytkownika końcowego. Szczególnie, że można to zrobić w zaledwie kliku liniach kodu. W klasie Client wystarczy zaimplementować metodę:
void HandleMessage(object message)
{
MessageHandlerManager.HandleMessage(this, (MessageBase) message);
}
a następnie w metodzie Connect, przed utworzeniem delegata threadStart, stworzyć zmienną:
AsyncOperation asyncOperation = AsyncOperationManager.CreateOperation(null);
i zastąpić wywołanie metody MessageHandlerManager.HandleMessage w następujący sposób:
asyncOperation.Post(HandleMessage, package.Message);
Gotowe. Klasa AsyncOperationManager "zatrzaskuje" w obiekcie AsyncOperation kontekst synchronizacji wątku, w którym została wykonana jej metoda CreateOperation. Następnie przy pomocy metody Post (nieblokującej – lub blokującej Send) możemy zlecać wykonanie kodu w tym wątku z poziomu innych wątków.
Według mnie nie należy odrzucać klasycznego podejścia przy implementacji protokołów komunikacji. Akurat "wielki switch" jest tylko narzędziem, faktycznie niezbyt subtelnym, natomiast podejście jest właściwe. Format pakietu mógłby wyglądać następująco:
|_Nagłówek_______________________________________________
|
| ID komunikatu (4 bajty)
| Długość wiadomości, bez nagłówka (4 bajty)
| Długość dodatkowej części nagłówka (4 bajty)
|
|_Wiadomość______________________________________________
|
| .
| .
| .
Serwer po otrzymaniu wiadomości:
1. Czyta nagłówek.
2. Zleca odnalezienie fabryki handlerów zdolnej do utworzenia handlera dla wiadomości określonego rodzaju:
IMessageHandlerFactory factory = MessageHandlerFactory.GetMessageHandlerFactory(header.MessageId);
3. Następuje poszukiwanie odpowiedniej fabryki – tu może znajdować się wielki switch, co jest złym rozwiązaniem, albo przeglądanie fabryk zarejestrowanych w kodzie lub w pliku konfiguracyjnym. Robi się z tego taki mini pojemnik IoC:
<apl.net>
<messageHandlerFactory defaultFactory="GenericMessageHandlerFactory">
<factories>
<clear />
<add name="LoginMessageHandlerFactory" messageId="1" type="Apl.Net.LoginMessageHandlerFactory" />
<add name="TextMessageHandlerFactory" messageId="2" type="Apl.Net.TextMessageHandlerFactory" />
.
.
.
<add name="GenericMessageHandlerFactory" type="Apl.Net.GenericMessageHandlerFactory" />
</factories>
</messageHandlerFactory>
</apl.net>
albo w ten sposób:
MessageHandlerFactory.Factories.Clear();
MessageHandlerFactory.Factories.Add(new LoginMessageHandlerFactory { MessageId = 1, Name = "LoginMessageHandlerFactory" });
MessageHandlerFactory.Factories.Add(new TextMessageHandlerFactory { MessageId = 2, Name = "TextMessageHandlerFactory" });
.
.
.
IMessageHandlerFactory defaultFactory = new GenericMessageHandlerFactory { Name = "GenericMessageHandlerFactory" };
MessageHandlerFactory.Factories.Add(defaultFactory);
MessageHandlerFactory.DefaultFactory = defaultFactory;
4. Jeśli została odnaleziona fabryka, jest tworzony i inicjalizowany handler wiadomości:
if (factory != null) {
// Pobierz wiadomość w postaci binarnej.
byte[] messageBits = … ;
IMessageHandler handler = factory.Create(this, header, messageBits);
handler.HandleMessage();
}
else {
// Odrzuć wiadomość.
…
}
To już wszystko. Handler może w ogóle nie wgłębiać się w treść wiadomości, tylko przesłać ją dalej. Postępowanie po stronie klienta powinno wyglądać analogicznie.
Jest to rozwiązanie podobne do Twojego w założeniach (modułowość, rozszerzalność itd.), ale:
1) serwer operuje wyłącznie na nagłówku pakietu – rodzaj wiadomości jest określany na podstawie pierwszych 4 bajtów, a nie na podstawie typu CLR,
2) protokół jest odporny na problemy z wersjonowaniem – nagłówek może zawierać dodatkowe informacje, które starsze wersje oprogramowania klienta mogą po prostu zignorować,
3) handler jest związany z konkretną instancją serwera lub klienta,
4) protokół nie opiera się na .NET-owej "magii" i może być z łatwością implementowany na innych platformach.
Norrrrmalnie Olek jestem pod wrażeniem – że chce ci się robić takie rzeczy:) Dzięki za uwagi. Z pewnością każdy może wynieść z nich coś pożytecznego, o coś takiego mi chodziło.
Co do formy przedstawienia treści zamieszczonego posta… wiem że można to było zrobić dużo lepiej. Mam jednak na to tyle czasu, że od zaświecenia w głowie pomysłu stworzenia czegoś takiego do zamieszczenia gotowego materiału na blogu minęły aż 3 tygodnie. Gdybym starał się dopieścić każdy element tak jak mi się wymarzyło to pewnie minęłoby co najmniej drugie tyle:).
Może ktoś doda coś jeszcze (oprócz "nie zostało nic do dopisania":) )?
nie zostało nic do dopisania ;), apl wyczerpał temat, zostaje tylko kiwać głową i czekać na kolejny wpis na blogu ;)
a jeszcze coś dodam jednak ;) brakuje mi tutaj takiej formy jak jest na NeHe, gdzie jest jakiś kod dołączony, ale oprócz tego autor zamieszcza poszczególne fragmenty kodu i opisuje co konkretnie ten fragment robi i dlaczego to robi. Jakoś tak z czasów gdy robiłem w OpenGL ta forma mi bardziej odpowiada.
@tom:
dzięki za uwagi, zamieszczając następnym razem coś nadającego się do działu Samples postaram się bardziej rozwinąć część tekstową
Warto spojrzeć na webcast Ayende Rahien [u]http://www.ayende.com/97/section.aspx/download/217[/u] który opisuje szczegółowo tworzenie własnej enterprise service bus. W jego rozwiązaniu handlowanie wiadomości jest rozwiązane podobnie jak u macka gdzie każda wiadomość jest rozpoznawana na podstawie typu. Przy czym zaletą w rozwiązaniu Ayende jest wyeliminowanie rejestrowania poszczególnego MessageHandlera dlatego że jest on wyciągany z kontenera IOC. Zaś sama rejestracja MessageHandlera w kontenerze nie wymaga żadnego pliku konfiguracyjnego i dodawania poszczególnego handlera, gdyż każdy MessageHandler dziedziczy po AbstractMessageHandler(więcej na ten temat w kodzie prezentacji)
Odnośnie wersjonowania zastanawia mnie sytuacja opisana przez Olka, gdzie dochodzi do zmiany interfejsu serwisu udostępnianego przez serwer. Moim zdaniem nigdy do takiej sytuacji nie powinno dość. Wyjątkiem może być jedynie przypadek w którym serwis jest wykorzystywany tylko i wyłącznie przez nas(nawet wtedy bym się zastanawiał, gdzyż łamiemy kontrakt – możemy stworzyć nowy kontrakt ale starego nie powinniśmy ruszać).
Nie jest jasne dla mnie jasne jaka jest potrzeba tworzenia fabryki fabryk MessageHandlerów :) [quote]IMessageHandlerFactory factory = MessageHandlerFactory.GetMessageHandlerFactory(header.MessageId); [/quote] gdyż moim zdaniem nigdy nie powinno dochodzić do sytuacji gdzie jest wiele handlerów dla jednego typu wiadomości. Takie rozwiązanie moim zdaniem śmierdzi :), gdyż nie wyobrażam sobie sytuacji gdzie klient wysyła ten sam typ zapytania a jest wywoływana różna logika biznesowa po stronie serwera w zależności od tego jaki został MessageHandler wywołany. Każda logika realizacji zapytania klienta powinna się znajdować w domenie a nie w infrastrukturze, którą jest MessageHandler
Nie jestem również zwolennikiem podejścia w którym MessageHandler tworzony jest na podstawie pliku konfiguracyjnego, ponieważ może to doprowadzić do problemów z zarządzaniem(należy się zastanowić czy jest potrzebna zmiana routowania message w wdrożonej aplikacji) i przede wszystkim komplikuje to testowaniu jednostkowe MessageHandlerFactory.
Romek, dzięki za komentarz, chociaż nie zgodzę się z Tobą :).
[i]Przy czym zaletą w rozwiązaniu Ayende jest wyeliminowanie rejestrowania poszczególnego MessageHandlera dlatego że jest on wyciągany z kontenera IOC.[/i]
Osobiście nie widzę różnicy. Wszystkie rozwiązania – moje, Procenta, Ayende – zakładają, że jest gdzieś jakiś statycznie dostępny pojemnik (ja nazywam go fabryką, Procent menadżerem, zaś Ayende pojemnikiem IoC), w którym najpierw rejestrujemy handlery (bezpośrednio lub pośrednio, przez fabryki), a następnie używamy go do znajdowania handlerów do obsługi konkretnych typów wiadomości. Ayende w zasadzie jedynie zautomatyzował sobie proces rejestrowania handlerów w pojemniku.
[i]Odnośnie wersjonowania zastanawia mnie sytuacja (…), gdzie dochodzi do zmiany interfejsu serwisu (…). Moim zdaniem nigdy do takiej sytuacji nie powinno dość. Wyjątkiem może być jedynie przypadek w którym serwis jest wykorzystywany tylko i wyłącznie przez nas(nawet wtedy bym się zastanawiał, gdzyż łamiemy kontrakt – możemy stworzyć nowy kontrakt ale starego nie powinniśmy ruszać).[/i]
Według mnie trochę zbyt teoretyzujesz w tym miejscu. Przede wszystkim wersjonowanie kontraktów nie jest takim znowu niespotykanym scenariuszem (np. w kontekście WCF – http://msdn2.microsoft.com/en-us/library/ms733832.aspx). Weźmy przykład: mamy serwer chat, Alicję i Roberta. Alicja posiada klienta w wersji 1.0, zaś Robert w wersji 2.0. W wersji 1.0 możliwe było jedynie wysyłanie prostych wiadomości tekstowych:
W wersji 2.0 pojawiła się możliwość wysyłania wiadomości HTML. Postanowiono nie zmieniać starego kontraktu, tylko utworzyć nowy:
Robert chce wyłać wiadomość do Alicji:
– Klient Roberta wysyła wiadomość typu [i]Message2[/i],
– Serwer widzi, że wiadomość jest kierowana do Alicji i przesyła ją dalej,
– Klient Alicji odrzuca wiadomość, ponieważ nie znajduje handlera dla typu [i]Message2[/i].
Wiadomość jest odrzucana, pomimo że klient Alicji mógłby ją obsłużyć. Można próbować jakoś temu zaradzić, np. po zalogowaniu klient mógłby wysyłać komunikat z numerem wersji, wówczas serwer mógłby podjąć się zadania konwersji nowszych wiadomości do starszego typu. To niestety nie rozwiązuje problemu interoperacyjności – jeśli serwer przesyła nam zserializowane binarnie obiekty, jak zabrać się za pisanie alternatywnego klienta takiej sieci, nie dysponując orygialnymi definicjami typów?
[i]Nie jest jasne dla mnie jasne jaka jest potrzeba tworzenia fabryki fabryk MessageHandlerów :) (…) gdyż moim zdaniem nigdy nie powinno dochodzić do sytuacji gdzie jest wiele handlerów dla jednego typu wiadomości.[/i]
Ten mechanizm niekoniecznie musi być użyty w ten sposób (chociaż może, np. do zwracania różnych lub różnie skonfigurowanych handlerów zależnie od wersji protokołu, przy pomocy którego komunikuje się klient). Raczej chodzi o to, żeby rozwiązać problem handlerów-singletonów – singletonem jest w takiej sytuacji fabryka, która w zależności od potrzeb może tworzyć nowe obiekty handlerów, mogące przechowywać własny stan, lub zwracać zawsze tę samą instancję. Równie dobrze możnaby wykorzystać do tego celu pojemnik IoC i dla typów handlerów wymagających przechowywania stanu zdefiniować czas życia instancji jako [i]transient[/i], a dla pozostałych jako [i]singleton[/i]. Zaproponowałem takie rozwiązanie, gdyż jest z powodzeniem stosowane w ASP.NET – co warto podkreślić – na poziomie infrastruktury. Konkretnie mam na myśli obiekty implementujące interfejsy [i]IHttpHandler[/i] i [i]IHttpHandlerFactory[/i]. Do tworzenia instancji handlerów HTTP ASP.NET zawsze wykorzystuje fabryki. Twórcy umożliwili rejestrowanie w sekcji [i]httpHandlers[/i] pliku web.config typów wywodzących się nie tylko z [i]IHttpHandlerFactory[/i], lecz również z [i]IHttpHandler[/i], jednak pod maską wszystko, co nie jest fabryką, jest opakowywane w instancję klasy implementującej [i]IHttpHandlerFactory[/i].
[i]Nie jestem również zwolennikiem podejścia w którym MessageHandler tworzony jest na podstawie pliku konfiguracyjnego, ponieważ może to doprowadzić do problemów z zarządzaniem(należy się zastanowić czy jest potrzebna zmiana routowania message w wdrożonej aplikacji) i przede wszystkim komplikuje to testowaniu jednostkowe MessageHandlerFactory.[/i]
To akurat mało ważne. Zaproponowałem plik konfiguracyjny jako jedną z wielu metod rejestrowania własnych handlerów. Równie dobrze można np. klasy fabryk oznaczyć własnymi atrybutami i wrzucić pliki DLL do odpowiedniego folderu, z którego zostaną wczytane przez aplikację.