Notki po polsku

Architektura trójwarstwowa

Architektura trójwarstwowa jest chyba najlepiej znaną implementacją wzorca warstw w architekturze aplikacji. Z mojego doświadczenia wynika jednak, że znajomość ta zwykle jest bardzo powierzchowna i nie sięga wgłąb (warstw;-)). Porzućmy więc na chwilę CQRS, DDD i inne fajne akronimy i wróćmy “do korzeni”. Co to jest architektura trójwarstwowa?

Definicja

Sama jej nazwa w języku polskim jest problematyczna. Angielska terminologia rozróżnia dwa ich rodzaje: tiers to warstwy fizyczne, natomiast layers – logiczne. W czasach kiedy architektura trójwarstwowa stawała się popularna (zastępując dwuwarstwowy model klient-serwer z terminalem i mainframem) nie było niezbędne rozróżnianie warstw fizycznych i logicznych, ponieważ odpowiadały one sobie jeden-do-jeden. Dziś mamy zupełnie inną sytuację, której polska terminologia nie jest w stanie oddać.

Warstwy fizyczne

W ujęciu fizycznym większość aplikacji biznesowych wciąż składa się z trzech warstw: interfejsu użytkownika (przeglądarka internetowa, smart client), logiki biznesowej (serwer WWW) oraz bazy danych (serwer RDBMS). Rzadkim, aczkolwiek spotykanym, rozwiązaniem w przypadku aplikacji z interfejsem HTML jest rozdzielenie fizyczne serwera WWW oraz serwera aplikacyjnego.

Warstwy logiczne

W ujęciu logicznym termin “architektura trójwarstwowa” nieco się zdewaluował. Istnieje obecnie wiele wzorców architektonicznych, które wywodzą się historycznie z architektury trójwarstwowej, ale mają z nią już niewiele wspólnego. Przyjrzyjmy się na początek najczęściej spotykanemu (i opisywanemu przez podręczniki Microsoft).

Przykład książkowy

Pierwszą warstwę stanowi interfejs ASP.NET WebForms. Poniżej niego znajduje się warstwa logiki biznesowej. Tradycyjnie (przed epoką LINQ to SQL) oficjalne diagramy Microsoft umieszczały na dole warstwę dostępu do danych zbudowaną w oparciu o ADO.NET. W ten sposób mieliśmy trzy warstwy logiczne. Warto zauważyć, że wszystkie one umieszczone są w jednej (środkowej) warstwie fizycznej. Tak było jednak do niedawna. Odkąd deweloperzy Microsoft mają do dyspozycji narzędzia ORM, nie ma już potrzeby samodzielnego budowania warstwy dostępu do danych. Cóż więc pozostaje? Warstwa interfejsu WebForms oraz warstwa logiki zawierająca także klasy reprezentujące encje biznesowe. Nie zmienia to jednak faktu, że opisana powyżej koncepcja jest najczęściej nazywana architekturą trójwarstwową. Dziwne, prawda? W których przyszło nam programować architektura trójwarstwowa jest terminem głównie marketingowym.

Przykład mniej książkowy — ActiveRecord

Aby nie opierać wszystkiego na przykładach produkowanych przez Microsoft, przyjrzyjmy się jeszcze dwóm innym wersjom architektury trójwarstwowej. Na początek wariant z wzorcem ActiveRecord. W tym wypadku także mamy do czynienia z dwoma warstwami: interfejsem i logiką biznesową. Specyfika wzorca ActiveRecord sprawia, że kwestie związane z persystencją są umieszczone tuż obok logiki aplikacji. Notka na marginesie: stosując stos technologiczny Microsoft wzorzec ActiveRecord polega na umieszczaniu logiki z klasach partial dla odpowiednich encji LINQ to SQL lub LINQ to Entities.

Przykład zupełnie nie książkowy — NHibernate

Drugi z przykładów spoza kręgu wpływów giganta z Redmond to aplikacja wykorzystująca NHibernate do dostępu do danych. W tym wypadku mamy do czynienia ze szczątkową wersją warstwy dostępu do danych zawierającą jedynie mapowania ORM. Mam pewne opory przed nazywaniem tego pełnoprawną warstwą. Powiedziałbym, że taka aplikacja jest dwuipółwarstwowa.

Architektura trójwarstwowa a MVC

MVC to jeden z najlepiej (obok Singletonu) znanych wzorców. Często natrafiam na pytania związane z relacją między MVC, a architekturą trójwarstwową. Jakiś czas temu zwykłem odpisywać, że koncepcje te nie mają ze sobą nic wspólnego, ponieważ MVC to wzorzec projektowy, a AT — architektoniczny. Czy jednak na pewno?

W dużych systemach z logiką biznesową zorganizowaną w formie modelu domeny rozróżnienie to ma sens. Stosuję wtedy MVC w logicznej warstwie interfejsu użytkownika. Warstwa logiki biznesowej to mój model domeny. W tym wypadku bardzo ważne jest zdanie sobie sprawy, że model z MVC to nie model domeny. Model z MVC to obiekty DTO przekazywane między warstwami GUI i logiki biznesowej.

Większość systemów nie jest jednak tak skomplikowana. Prosta aplikacja webowa zbudowana w oparciu o jakiś framework MVC (np. ASP.NET MVC) jest świetnym przykładem implementacji architektury trójwarstwowej (czyli tak naprawdę 2 lub 2,5 warstwowej). Widoki są częścią warstwy prezentacji, kontrolery i model — logiki biznesowej. Dostępu do danych istnieje zwykle jako peseudowarstwa na bazie mapera obiektowo-r. Tak więc MVC, będąc przede wszystkim użytecznym wzorcem budowy interfejsu użytkownika, może w niektórych wypadkach być także implementacją wzorca architektonicznego aplikacji trójwarstwowej

Podsumowanie

Architektura trójwarstwowa jest prawdopodobnie najlepiej znanym wzorcem architektonicznym. Jest ona całkiem niezłą abstrakcją większości systemów. W końcu prawie każda aplikacja ma interfejs użytkownika, logikę biznesową i jakiś dostęp do danych. Niestety w dzisiejszych czasach termin ten przestał być użyteczny w komunikacji: jest on zbyt obszerny i rozmyty.

VN:F [1.9.13_1145]
Rating: 5.0/5 (3 votes cast)

NoSQL Summer

Niedawno miałem przyjemność uczestniczyć w pierwszym spotkaniu Krakowskim spotkaniu inicjatywy NoSQL Summer. Zainteresowała mnie, przede wszystkim, forma spotkań — dyskusje dotyczące przeczytanych “lektur obowiązkowych”. Skojarzyło mi się bardziej z dyskusyjnym klubem filmowym, niż typową technologiczną juzergrupą. Postanowiłem się więc wybrać. Nie żałowałem.

Organizacji krakowskich spotkań podjął się Adam Pohorecki. Dzięki temu organizacyjnie wszystko było zapięte na ostatni guzik. Miłym dodatkiem było piwo sponsorowane przez Lunar Logic.

Spotkanie przybrało ostatecznie formę wymiany doświadczeń dotyczących różnych baz nierelacyjnych. Oto garść zebranych przeze mnie naprędce wniosków:

Mamy do czynienia z prawdziwym wysypem baz nierelacyjnych. Chyba każdy szanujący się programista za punkt honoru ma napisanie swojej (zupełnie jak niegdyś z kontenerami Dependency Injection).

  • Poszczególne bazy nierelacyjne różnią się podejściem do replikacji/klastrowania.
  • Najbardziej znany w Krakowie jest CouchDB.
  • Temat NoSQL jest raczej obcy środowsiku .NET. Smutne.
  • Częstym problemem z bazami nierelacyjnymi jest duży apetyt na przestrzeń dyskową.
  • Bazy nierelacyjne są już od dawna wykorzystywane produkcyjnie, nawet w Krakowie.
  • Transakcje obejmujące modyfikacje kilku dokumentów to ZŁO.
  • API baz nierelacyjnych jest bardziej zbliżone do sposobu działania ich mechanizmów składowania danych
  • Bazy nierelacyjne wymagają większej świadomości i wiedzy od dewelopera, ale w zamian oferują większe możliwości wpływania na zachowanie mechanizmu składowania danych (np. rozwiązywanie konfliktów.

Kolejne spotkanie z cyklu NoSQL Summer już w najbliższą środę w siedzibie Lunar Logic (Krasińskiego 17/5). Temat: map/reduce. Więcej informacji można uzyskać za pośrednictwem grupy google Kraków NoSQL Summer. W imieniu organizatorów, zapraszam.

VN:F [1.9.13_1145]
Rating: 0.0/5 (0 votes cast)

Domain Driven Design, czyli programowanie przez modelowanie

Podczas 58. spotkania Krakowskiej Grupy Developerów .NET miałem przyjemność poprowadzić prezentację pt. “Domain Driven Design, czyli programowanie przez modelowanie”. Ponieważ zakładam, że nie wszyscy jesteście z Krakowa i uczestniczyliście w spotkaniu, pozwolę sobie opisać o czym mówiłem. Prezentację możecie obejrzeć lub ściągnąć (format pptx) stąd (via SlideShare), a przykłady w kodzie stąd.

Rozwiązywanie problemów

Na początku chciałbym skontrastować “klasyczne” podejście do rozwiązywania problemów z podejściem wykorzystującym model. Można to zilustrować następująco:

Podejście klasyczne

Podejście z wykorzystaniem modelu

W wersji klasycznej programista implementuje bezpośrednio wymagania, tak jak zostały one spisane przez analityka (zwykle w formie 10/100/miliona linii “The system shall…”). Dobrze, jeśli w ten proces wpleciona jest pętla sprzężenia zwrotnego, która sprawia, że implementacja pierwszych wymagań ma wpływ na dalszy przebieg procesu analizy.

W podejściu “Domain-Driven” tak naprawdę nie istnieją role analityka i programisty. Istnieje tylko jedna rola — modelarz. Modelarz zajmuje się budową modelu rzeczywistości użytecznego w rozwiązaniu konkretnego problemu. Współpracuje on bardzo blisko z ekspertami dziedzinowymi, przetwarzając posiadaną przez nich wiedzę na spójny, niesprzeczny, a co najważniejsze, wykonywalny, model. Dopiero na bazie tego modelu tworzone jest ostateczne rozwiązanie. Możemy odnaleźć dwie pętle sprzężenia zwrotnego: pierwsza dotyczy samego modelu (im więcej modelarz stworzył, tym lepiej rozumie problem), a druga modelu w kontekście aplikacji (im więcej wiemy, co ma robić aplikacja, tym lepiej wiemy, które aspekty należy dokładniej modelować).

Cechy modelu

Skoro już wiemy, do czego przydaje się model, warto się zastanowić jakie cechy powinien posiadać dobry model. Oto moja osobista lista w kategorii “model powinien być”:

Użyteczny

Nie ma sensu budowa idealnego modelu rzeczywistości, ponieważ byłby on tak samo skomplikowany jak ta rzeczywistość. Z punktu widzenia złożoności obliczeniowej jest to niewykonalne. Znane powiedzenie mówi, że wszystkie modele są błędne, ale niektóre są użyteczne. Model, który budujemy na potrzeby naszego systemu powinien odzwierciedlać tylko ten fragment rzeczywistości, w ramach którego działa ten system i tylko pod kątem czynności bezpośrednio związanych z jego funkcjonowaniem.

Hermetyczny

Model jest kodyfikacją pewnych reguł rządzących modelowanym wycinkiem rzeczywistości. Reguły te nie powinny wyciekać z modelu, co oznacza, że jego klienci (użytkownicy) nie powinni musieć o nich wiedzieć. Przykładem może być reguła, że w modelu programu HR pracownik może mieć w danym momencie co najwyżej jednego pracodawcę. To model, a nie klient, powinien zadbać o to, aby przy zmianie pracy pracownik został odłączony od poprzedniego pracodawcy.

Podatny na modyfikacje

Jest to sprzeczne z zasadą Open-Closed Principle (open for extensions, closed for modifications), jednak w przypadku modeli sprawdza się bardzo dobrze. Aby model był nadążał za szybko zmieniającą się rzeczywistością, sam także musi się zmieniać. Nie chcemy przecież, aby skostniały model aplikacji był dla organizacji jej używającej hamulcem rozwoju, prawda?

Zrozumiały

O sukcesie modelu i całego projektu decyduje, czy wszyscy jego interesariusze rozumieją przestrzeń problemu. Model powinien stanowić podstawę porozumienia i bazę dla wszędobylskiego języka (ubiquitous language) służącego do komunikacji między wykonawcami systemu (ludźmi technicznymi), a ekspertami dziedzinowymi. W dzisiejszych czasach języki programowania takie, jak C#, są na tyle elastyczne, że pisanie kodu (modelu) tak, aby był zrozumiały dla nieprogramistów nie stanowi już większego problemu.

Wydajnie implementowalny

Mimo, iż model nie powinien być zależny od jakiejkolwiek konkretnej technologii, powinien jednak być (w jakiejś technologii) wydajnie implementowalny. Nie chcemy być zależni od NHibernate czy innego ORM-a, jednak akceptowanie faktu, że model będzie przechowywany trwale w jakiejś relacyjnej bazie danych za pośrednictwem jakiegoś ORM-a pozwala zoptymalizować wiele kwestii. Nie ma sensu udawać niezależności od wszystkich aspektów technologii.

VN:F [1.9.13_1145]
Rating: 5.0/5 (2 votes cast)

Jak zwątpiłem w transakcje

Transakcje to fajna sprawa. Polubiłem je od pierwszego użycia. Zostałem oczarowany przez ich magiczną właściwość — zwalniają z myślenia o spójności danych. Cool, przecież nie lubię myśleć. Jeszcze bardziej byłem oczarowany, gdy odkryłem transakcje rozproszone. To dopiero jazda. Mogę coś “zapdejtować” na tej bazie, na tej drugiej bazie i jeszcze wrzucić komunikat do kolejki MSMQ i wszystko wykona się transakcyjnie — w całości lub wcale.

Od dłuższego czasu zaczynam jednak wątpić w transakcje, szczególnie te rozproszone. Zbyt często ja sam lub ktoś ze znajomych wpada przez nie w pułapkę bez wyjścia. Zamiast używać najodpowiedniejszej technologii, musimy wtedy wziąć taką, która współdziała z naszym sposobem zarządzania transakcjami.

Najczęstszym problemem, na który natrafiałem jest odbieranie i wysyłanie komunikatów do kolejek z jednoczesnym zapisem danych w bazie. Pierwsza rzecz, która przychodzi w takim wypadku do głowy, to transakcja, która obejmuje zarówno infrastrukturę kolejkową, jak i RDBMS. Pierwsza nie oznacza, niestety, najlepsza.

Nieodpowiednia technologia

Wymaganie transakcyjności w komunikacji z RDBMS i kolejkami pchnęło mnie kilka lat temu do wykorzystania SQL Server Service Broker jako kolejki komunikatów. Unikalną cechą Service Broker’a jest fakt, że żyjąc wewnątrz silnika bazodanowego, jest w naturalny sposób zintegrowany z mechanizmami transakcyjnymi SQL Server. Nie ma potrzeby stosowania (wolnych) transakcji rozproszonych, aby wysłać komunikat i zrobić przysłowiowy “apdejt”.

Niestety poza tą jedną zaletą, Service Broker ma bardzo wiele wad, z których największą jest brak dobrego gotowego API w C#, o modelu usługowym (np. WCF) nie wspominając. Jest bardzo trudny w użyciu i utrzymywaniu. Definitywnie był to zły wybór technologiczny, podyktowany jedynie chęcią zastosowania mechanizmu transakcji.

Niezgodność API

W każdym nietrywialnym systemie wykorzystuje się wiele zewnętrznych bibliotek. Bardzo rzadko pochodzą one od jednego dostawcy. Zdecydowanie częściej jest to mieszanka rozwiązań open source oraz COTS. Problem pojawia się, gdy dwie z bibliotek mają współpracować w ramach jednej transakcji. Weźmy jako przykład NHibernate i NServiceBus. Ten pierwszy posiada własną abstrakcję reprezentującą transakcje, podczas gdy pod spodem korzysta z transakcji ADO.NET. Ten drugi wykorzystuje transakcje System.Transactions do dostępu do MSMQ.

W przypadku użycia obu technologii w jednej transakcji, pojawia się problem, jak sprawić, aby każda z bibliotek mogła wykorzystywać swoje API odwołując się do tej samej fizycznej transakcji. W wypadku wspomnianej pary, jedynym rozwiązaniem jest pozwolić NHibernate używać zewnętrznych transakcji System.Transactions. Jest to jednak zamiana jednego problemu na inny. Do niedawna bowiem pojawiał się w NServiceBus wyciek pamięci, ponieważ zachowanie NHibernate w wypadku współpracy z System.Transactions jest bardzo słabo udokumentowane i łatwo o błąd wynikający z niezrozumienia.

Błedne implementacje transakcyjności

Jonathan Oliver opisał swoje testy kompatybilności różnych silników bazodanowych z transakcjami rozproszonymi na swoim blogu. Wnioski nie są optymistyczne: jedynie sterowniki do SQL Server i Oracle w pełni i bez problemów je obsługują.

Z drugiej strony nie dziwie się, że mniej płatne lub darmowe rozwiązania nie wspierają rozproszonych transakcji. Zdecydowana większość systemów radzi sobie bez nich, więc zysk (w sensie pieniędzy z licencji lub satysfakcji użytkowników) z implementacji wsparcia dla nich byłby znikomy.

Proste rozwiązanie

Rozwiązanie jest oczywiście proste. Wymaga jednak nieco innego podejścia do projektowania komunikacji. Wystarczy zadbać o to, aby każdy komunikat był

  • albo idempotentny (wielokrotne przetworzenie takiego komunikatu daje taki sam efekt, jak przetworzenie jednokrotne),
  • albo jednoznacznie identyfikowalny (unikalne ID).

Drugi przypadek można sprowadzić do pierwszego dodając rejestr przetworzonych komunikatów zawierający ich unikalne ID i przed obsłużeniem komunikatu sprawdzać, czy aby nie został przetworzony wcześniej.

Po spełnieniu któregoś z powyższych warunków zyskujemy możliwość rozłącznego zarządzania transakcją związaną z obieraniem komunikatu (MSMQ, ServiceBroker) oraz transakcją bazodanową. Ta pierwsza powinna być zatwierdzana dopiero po zatwierdzeniu tej drugiej. Powoduje to, że mamy pewność, iż każdy komunikat zostanie przetworzony co najmniej raz. Z drugiej strony idempotentność gwarantuje nam, że skutki wielokrotnego przetworzenia będą takie, jak jednokrotnego. Ostatecznie więc uzyskujemy semantykę dokładnie raz — taką samą jak przy zastosowaniu transakcji rozproszonych.

Praktyka

Dokładnie taki mechanizm zastosowałem w swoim ostatnim systemie. Pozwolił mi on na użycie klienta Service Broker (użycie tej kolejki było narzucone z góry) w połączeniu z NHibernate bez konieczności integracji obu technologii. Ponieważ transakcje były rozdzielone, klient kolejek nie musiał wiedzieć nic o dostępie do danych i vice versa. Prawdopodobnie zaoszczędziło mi to kilka dni pracy przy implementacji, testach i poprawianiu bugów w warstwie integracyjnej. Nauczyłem się także, że najlepszą strategią integracji technologii jest unikanie integracji technologii, kiedy to tylko możliwe.

VN:F [1.9.13_1145]
Rating: 5.0/5 (1 vote cast)

Tunel SOAP w SOAP i WCF

Podczas wstępnego projektowania systemu, nad którym teraz pracuje, natknęliśmy się na dosyć interesujący problem. Polega on na tym, iż docelowe środowisko wdrożeniowe nie pozwala na komunikację między serwerem WWW, a serwerem aplikacyjnym. Komunikacja odwrotna jest możliwa. Sytuację tę przedstawia poniższy diagram.

Te raczej mocne obostrzenia podyktowane są (podobno) polityką bezpieczeństwa. Niestety są one zabójcze dla naszego systemu, ponieważ ma on służyć do monitorowania i zarządzania procesami uruchomionymi na serwerze aplikacyjnym. Jak więc monitorować i sterować czymś, z czym nie można się połączyć?

Tunel

Odpowiedzią jest tunel SOAP w SOAP wykorzystujący fakt, że komunikacja odwrotna (serwer aplikacyjny wysyła requesty do serwera WWW) jest, jak najbardziej, możliwa. Jak działa taki tunel? Tunel składa się z dwóch końców. Koniec kliencki umiejscowiony jest na maszynie WWW i przyjmuje żądania, które następnie są kolejkowane w pamięci. Serwerowy koniec tunelu jest aplikacją uruchomioną na serwerze aplikacyjnym, która okresowo (raz na kilka sekund) odpytuje (wywołując operację Fetch) koniec kliencki, czy są jakieś zakolejkowane żądania. Jeśli tak, pierwsze żądanie z kolejki jest zwracane w odpowiedzi na wywołanie Fetch. Przesłane żądanie jest następnie forwardowane do odpowiedniego procesu serwerowego. Prześledźmy to na przykładzie.

Tunel w działaniu

  1. Serwer WWW wysyła żądanie. Adresatem (nagłówek “To” w SOAP) jest serwer aplikacyjny, jednak żądanie jest fizycznie (na poziomie TCP) wysyłane na adres klienckiego końca tunelu.
  2. Generowany jest unikalny identyfikator żądania. Żądanie, wzbogacone o dodatkowy nagłówek zawierający wygenerowany identyfikator, jest kolejkowane w oczekiwaniu na wywołanie Fetch.
  3. Serwerowy koniec tunelu wywołuje operację Fetch celem pobrania pierwszego oczekującego żądania. Operacja ta zdefiniowana jest w ten sposób, że request jest pusty, a w odpowiedzi przesyłany jest dowolny komunikat SOAP.
  4. W odpowiedzi na Fetch żądanie (przesyłane jako odpowiedź) trafia do serwerowego końca tunelu.
  5. Serwerowy koniec tunelu forwarduje je (bez jakiejkolwiek ingerencji bądź analizy) do nasłuchującego lokalnie procesu serwera.
  6. Proces serwera zwraca odpowiedź do serwerowego końca tunelu.
  7. Odpowiedź jest forwardowana do końca klienckiego poprzez wywołanie operacji Reply, która pozwala na przesłanie dowolnego komunikatu SOAP (w odpowiedzi na Reply przesyłany jest pusty komunikat). Przed wysłanie do komunikatu dodawany jest nagłówek zawierający identyfikator, który zawierało żądanie.
  8. Kliencki koniec tunelu, na podstawie przekazanego identyfikator kojarzy przesłaną za pomocą Reply odpowiedź z oczekującym żądaniem, a następnie odpowiada klientowi przesyłając mu komunikat otrzymany od serwerowego końca tunelu.

Szczegóły implementacyjne

Jest kilka kwestii, o których trzeba pamiętać budując rozwiązanie tego typu. Pierwszą z nich jest konieczność użycia WS-Addressing (czyli de facto WSHttpBinding lub pochodnego). W przeciwnym wypadku informacje o adresacie nie będą przeźroczyście transportowane od klienta do serwera. Nie stanowi to problemu w przypadku prostych zastosowań (no security), jednak uniemożliwia np. stworzenie pewnej sesji (reliable session).

Kolejną kwestią techniczną jest konieczność każdorazowego kopiowania komunikatów WCF przed ich forwardowaniem. Specyfika WCF jest taka, że obiekt Message reprezentujący otrzymany komunikat SOAP może być odczytany co najwyżej raz. Jeśli potrzebujemy więcej razy — musimy utworzyć kopię.

Ustawienie właściwości Action i ReplyAction w atrybucie OperationContract na “*” powoduje, że serwis będzie operował na dowolnych komunikatach SOAP. Jest tylko jedna pułapka. Tylko jedna operacja w kontrakcie może mieć ustawione “*”, ponieważ inaczej nie byłoby wiadomo do której operacji skierować komunikat.

Wyjaśnienie

Nie zachęcam was, broń Boże, to stosowania takich zabawek dla samej idei. Jest to bardzo szczególne rozwiązanie bardzo szczególnego problemu, który zapewne nie występuje zbyt często (oby). Jest to właściwie obejście procedur bezpieczeństwa i, jako takie, powinno być stosowane z rozwagą. W końcu ktoś te procedury w jakimś celu ustalał, prawda?

VN:F [1.9.13_1145]
Rating: 0.0/5 (0 votes cast)