Żegnajcie == Servus
Większość z nas stara się wraz z nadejściem nowego roku zmienić częściowo swoje życie, poprzez różnego rodzaju noworoczne postanowienia. Osobiście nie jestem zwolennikiem tego typu rozwiązań, dlatego, że najczęściej kończą się one fiaskiem. W związku z tym stwierdziłem, że jeżeli mam coś zmienić w moim życiu to musi to nastąpić natychmiastowo.
I tak o to dzisiaj mam przyjemność od miesiąca cieszyć się z nowo podjętej pracy, w nowym mieście, w nowym kraju. Wybór padł na Niemcy, a dokładniej Monachium – miasto które od wielu lat według statystyk jest uznawane za jedno z najlepszych miast pod kontem statusu życia. Czy tak rzeczywiście jest, to się jeszcze okaże.
Wybrałem Niemcy, dlatego, że bardzo dobrze znam język niemiecki i mam tutaj sporą część mojej rodziny i znajomych. Za Monachium z kolei przemawiał nie tylko status życia ale również mocna pozycja na rynku IT, no i piwo, które jest również głównym powodem przeprowadzki dla firmy Jetbrains
.
W związku z tym wybór docelowego miasta nie był na tyle skomplikowany co sam wybór firmy, w której podjąłem nową pracę. Wszystko na tyle dobrze się ułożyło, że wylądowałem w firmie Autoscout24, która prowadzi największą internetową giełdę samochodową w Europie. Jednym z głównych powodów dla których wybrałem Autoscout24, jest perspektywa osobistego rozwoju, a to podyktowane jest skalą i sposobem realizacji projektów – oczywiście wszystko zwinne.
Mam nadzieję że w najbliższych dniach będę miał okazję pochwalić się szczegółami związanym z moją nową pracą i życiem w Monachium
Automatyczne testy w zmieniającym się środowisku
W poprzednim poście zwróciłem uwagę na pewne problemy, związane z pisaniem automatycznych testów, które wynikły w ramach konkretnego przypadku w naszym projekcie. Dla przypomnienia, sednem problemu były zmiany w kodzie wywołane refaktoryzacją i zmianami wymagań. Jako, że ostatnio skoncentrowałem się po części na refaktoryzacji, teraz chciałbym poruszyć temat zmieniających się wymagań.
Od samego początku mojej pracy jako programista napotykam się na projekty, w których bardzo dynamicznie zmieniają się wymagania. I tak się dzieje bez względu na to, czy zostanie przygotowana wcześniej szczegółowa dokumentacja potrzeb klienta, czy wymagania zostają definiowane ad-hoc. Początkowo, podobnie jak zapewne was wszystkich, strasznie mnie to wkurzało, ale z czasem zrozumiałem, że tak działa w dzisiejszych czasach biznes i w związku z tym należy się do tego dostosować. Dlatego też uważam metodyki zwinne za jedno z lepszych narzędzi, które pozwala nam programistom odnaleźć się w tych realiach.
Zgodnie z manifestem Agile, metodyki zwinne koncentrują się na odpowiednim reagowaniu na zmiany. Stąd też iteracyjne podejście jest jednym z filarów tych metodyk. Jednak samo podejście iteracyjne nie wystarczy. Jedną z głównych wad metodyki Scrum – jednej z najbardziej znanych metodyk zwinnych, okazał się brak wskazówek dotyczących realizowania projektów IT od strony technicznej (programowania). Zastosowanie miękkich technik zarządzania zawartych w Scrumie, prowadzi często do tego, że wydajność zespołu spada w późniejszej fazie projektu dlatego, że wytwarzane oprogramowanie staje się z każdą iteracją coraz to trudniejsze w zarządzaniu i rozwijaniu. Więcej na temat tych problemów można znaleźć w poście Jamesa Shore’a.
Jedną z najczęściej wspominanych praktyk, która pozwoli na zminimalizowanie tego typu problemów jest automatyzacja testów – najlepiej wprowadzając w oparciu o test driver development(TDD). Dlatego też wiele osób dzisiaj mówi, że TDD to jeden z filarów metodyk zwinnych. Rzecz w tym, że tak naprawdę zawsze praktyki takie jak TDD były ważnym elementem metodyk zwinnych. Przykładem na to może być metodyka zwinna Extreme Programming, w której skład wchodzi TDD. Nawet sam Ken Shwaber (twórca Scruma) mówi w książce Agile Project Management with Scrum o automatyzacji testów i innych pokrewnych technikach, jako narzędziach dopełniających scruma. Podobną tematykę porusza również Mike Cohn w książce Succeeding with Agile.
W związku z tym można dojść do wniosku, że TDD jest i zawsze był ważnym elementem metodyk zwinnych. A to z kolei znaczy, że unikanie testów automatycznych, argumentowane zmianami wymagań klienta jest nieuzasadniona. Moim zdaniem problem tkwi w tym, że w większości przypadkach programiści rozpoczynając przygodę z automatyzacją testów borykają się z problemem zarządzania testami, które szybko zniechęcają do tego podejścia. Przed omówieniem tego problemu w szerszym kontekście, chciałbym zwrócić uwagę na to, dlaczego tak ważne jest stosowanie testów automatycznych w projektach, w których często zmieniają się wymagania.
W porównaniu do wprowadzania nowych funkcjonalności w systemie, zmiana istniejącej funkcjonalności wiąże się najczęściej z większym ryzykiem. Dzieje się tak dlatego, że w większości przypadków zmiany istniejącej funkcjonalności nie zmieniają w stu procentach logiki już istniejącej funkcjonalności, lecz tylko jej część. To z kolei prowadzi do tego, że zmieniając tą część funkcjonalności możemy w szybki i niezauważony sposób wprowadzić nowe błędy w tej części, która nie powinna ulec zmianie. Nie mając w zapasie testów automatycznych nie jesteśmy w stanie szybki i wygodny sposób wychwycić tego rodzaju błędów. Warto zwrócić również uwagę na to, że wprowadzając zmiany w funkcjonalności powinniśmy zawsze się starać poprawiać już istniejący kod poprzez refaktoryzację. Zmiany funkcjonalności często są związane ze zmianą logiki biznesowej, a to z kolei znaczy, że kod, który wcześniej idealnie abstrahował logikę biznesową danej funkcjonalności nie musi koniecznie się sprawdzać w momencie, kiedy wprowadzimy nowe zmiany. Tego typu sytuacje często prowadzą do refaktoryzacji zwanej „breakthrough” opisanej szczegółowo w książce Domain Driven Design Erica Evansa. Refaktoryzacje te są bardzo trudne w realizacji bez odpowiedniego pokrycia kodu testami automatycznymi. Jeżeli z kolei pominiemy tego typu zmiany, szybko będziemy zmuszeni do zwrócenia tzw. długu technicznego, poprzez zmniejszenie naszej wydajności w kolejnych iteracjach. Więc jak widać koło się szybko zamyka.
Zapewne sobie myślicie, że fajnie mi się mówi, ale w praktyce z tym już nie jest tak łatwo. Zgadza się. Tak jak już wcześniej wspominałem, utrzymywanie testów automatyczny też kosztuje i w wielu przypadkach źle napisany testy będą dla nas uciążliwe i będą nas spowalniać prawie że na każdym kroku w trakcie wprowadzania zmian w istniejącej funkcjonalności. Stąd też, często można spotkać się z sytuacją, gdzie w późniejszej fazie projektu wcześniej pisane testy automatyczne zostają ignorowane. Istnieje wiele porad dotyczących poprawy jakości testów automatycznych, przede wszystkim testów jednostkowych, które trudno było by opisać w ramach jednego wpisu. Mimo tego chciał bym zwrócić uwagę na parę ważnych praktyk eliminujących przedstawiony wcześniej problem, którym powinniście się przyjrzeć bliżej.
Pisząc produkcyjny kod powinniśmy się starać pisać metody, które są krótkie i przejrzyste, dlatego, że są one łatwiejsze do zrozumienia i zarządzania. Stosowanie tej zasady również jest konieczne w przypadku automatycznych testów, gdyż zapobiega ona powstawaniu nieczytelnych testów. Nieczytelne testy mają to do siebie, że najszybciej je ignorujemy w przypadku, gdy dane testy nie wykonują się poprawnie po wprowadzeniu zmian. Tworzenie krótkich i zrozumiałych testów wiele pomaga, ale mimo tego pojawiają się często problemy związany z tym że, aby zrozumieć dany test musimy przejrzeć całe ciało testu. Temu można w łatwy sposób zapobiec, poprzez stosowanie odpowiednich nazw testów. Praktyka ta też się sprawdza w metodach produkcyjnego kodu, przy czym w wielu przypadkach testy będą opisane dłuższą i bardziej wyczerpującą nazwą. Poniżej znajdziecie przykład testu jednostkowego NUnit z projektu, w którym uczestniczę.
[Test]
public void Registering_twice_the_same_publisher_for_the_same_event_will_rise_an_error()
{
var sut = new EventBroker();
var publisher = new PublisherMock();
var subject = "test";
var eventName = "MyEvent";
sut.RegisterPublisher(subject, publisher, eventName);
Assert.Throws(typeof(InvalidOperationException), () => sut.RegisterPublisher(subject, publisher, eventName));
}
Należy zwrócić uwagę, że w powyższym teście istnieje tylko jedno zapewnienie(Assert w NUnicie), które sprawdza wynik testu. Stosując tego typu zasadę zapewniamy, że dany test ma tylko jedno i jedyne znaczenie, które powinno być opisane w nazwie testu. W wielu przypadkach spotykam się z testami, które w powyżej przedstawionym przypadku, sprawdzały by dodatkowo wynik pierwszego wywołania metody RegisterPublisher wprowadzając dodatkowe zapewnienie. To jest moim zdaniem kolejny z podstawowych błędów, który prowadzi do wcześniej opisanych problemów z nieczytelnymi testami.
Stosując wyżej przedstawione zasady szybko zauważycie, że każdy test ma odpowiedni kontekst, w ramach którego weryfikujemy wynik końcowy. Test w danym kontekście powinien jedynie ulec zmianie w momencie gdy zostaną wprowadzone zmiany wymagań dotyczące danego kontekstu. Za tym z kolei idzie to, że nie jesteśmy zmuszeni, przy zmianie części funkcjonalności zmieniać wszystkie testy, lecz tylko te, które są w kontekście zmian funkcjonalności. Dla osób, które spotkały się z metodyką behaviour driven development(BDD) i pisaniem testów w oparciu o wzorzec Context/Specification, zapewne to podejście jest znajome. Z kolei dla tych osób, które nie miały wcześniej do czynienia z tym wzorcem, polecam zapoznanie się z narzędziem Machine Specification, znanym szerzej jako MSpec, które w prosty sposób pozwala na pisanie testów w oparciu o ten wzorzec.
Oczywiście to nie wszystko i jak już wcześniej wspominałem, przekazanie całej wiedzy wymagało by prawdopodobnie spisania książki. Mimo to mam nadzieję, że byłem wam w stanie przedstawić jak ważne jest stosowanie testów automatycznych w przypadku wielu zmian wymagań biznesowych i jak ważna w tym jest jakość testów.
Dlaczego unikamy pisania automatycznych testów?
Ostatnio odbyłem bardzo ciekawą rozmowę z kolegą z pracy, która dotyczyła automatyzacji testów. Powodem tej dyskusji była duża liczba błędów, w jednym z modułów naszej aplikacji, za którą kolega był odpowiedzialny.Błędy te powstawały głównie ze względu na duże zmiany w istniejącym już kodzie wymuszone zmianą wymagań i refaktoryzacją. Osoby, które mnie znają zapewne zgadną jakie było moje pierwsze pytanie. Oczywiście było to:
A gdzie się podziały wszystkie testy w tym module?
Okazało, się, że jako jedyny z dostępnych modułów w naszej aplikacji nie posiadał on w ogóle żadnych automatycznych testów. To oczywiście nie znaczy, że w pozostałych modułach nie występują błędy, ale jest ich zdecydowanie mniej.
Kolega tłumacząc się odpowiedział, że nie miał po prostu na to czasu. Hmm, nic nowego, już chyba po raz setny słyszę tego typu odpowiedź. Kontynuując zapytałem się czy jego zdaniem testy automatyczne mają sens. I tutaj się ucieszyłem, bo kolega odpowiedział, że tak. Więc idąc za ciosem wdałem się w dyskusje z nim pytając, dlaczego ich nie pisze, jeżeli jest przekonany, że mają sens. Z dyskusji tej wynikły następujące wnioski, z którymi chciałbym się z wami podzielić.
Większość programistów za główną wadę pisania automatycznych testów – przede wszystkim jednostkowych, uważa potrzebę poświęcenie dodatkowego czasu na napisanie tych testów. Z drugiej strony wydaje mi się że każdy kto przynajmniej parę razy w życiu refaktoryzował istniejący kod, nie mając w zapasie testów potwierdzających poprawność kodu, chyba przyzna mi rację, że tego typu refaktoryzacja to po prostu katorga. Uważam, że w większości przypadków brak automatycznych testów jest jednym z głównych powodów, dlaczego rezygnujemy z poprawiania działającego ale nie czytelnego kodu. Patrząc na to łatwo można dojść do wniosku, że jeżeli chcemy utrzymywać porządek w naszym kodzie, to wymagane jest od nas utrzymywanie testów automatycznych, które w każdej chwili możemy uruchomić po to, by sprawdzić czy wszystko nadal działa poprawnie.
W wielu przypadkach programiści piszą testy dopiero po napisaniu funkcjonalności, która ma być przetestowana. Przyznam się szczerze, że czasami mi się to również zdarza, rzadko ale się zdarza. W przypadku gdy dopisuje test już po zaimplementowaniu funkcjonalności, czuję pewną niechęć i brak potrzeby pisania tego testu. Dzieje się tak głównie dlatego, że jestem zbyt pewny siebie (a może zbyt leniwy) i uważam, że napisany raz przeze mnie kod jest już poprawny. W wielu przypadkach jednak się mylę. W najgorszych przypadkach dochodzi do tego, że wkrada się błąd w kodzie dlatego, że pominąłem test. Jako, że życie mnie już pokarało parę razy za tego typu podejście, staram się pisać testy zawsze przed implementacją funkcjonalności. Poza tym pisanie testów po to dla mnie i dla wielu programistów jest jednym z najbardziej wkurzających zajęć. Jeżeli dodatkowo „ciśnie nas termin” w większości przypadkach zrezygnujemy z pisania testów po implementacji, dlatego, że traktujemy to jako marnotrawienie naszego cennego czasu. Jeżeli się zgadzacie z tym stwierdzeniem to zastanówcie się czy nie jest to dla was tylko dodatkowa wymówka na to, by się wywinąć z pisania testów?
Jeżeli rzeczywiście macie takie odczucie, to mam dla was małą oczywistą radę. Zaczynajcie pisanie kodu od napisania testu. Oczywiście to zajmie wam dodatkowy czas i początkowo trudno wam będzie wyszacować czas, który będzie potrzebny na realizację zadania. Z drugiej strony pisząc test przed napisanie funkcjonalności nie będziecie tak szybko rezygnować z pisania testów automatycznych. Za tym idzie z kolei to, że szybciej poznacie rzeczywistą wartość testów automatycznych, a w przyszłości będziecie też wam łatwiej wyszacować czas potrzebny na napisanie funkcjonalności wraz z testami.
Dla osób, które nie wiedzą jak zacząć z TDD polecam listę webcastów opublikowaną przez Łukasza. Przede wszystkim polecam webcasty z DNR TV w wykonaniu Jean Paul Boodhoo – sam z nich niejednokrotnie korzystałem, aby poznać TDD.
Logowanie i obsługa wyjątków – czyli jak łatwo można sobie utrudniać życie
Witam ponownie! Przez ostatni czas czułem pewne pohamowanie do pisania bloga, ale dziś stwierdziłem, że nadszedł w końcu czas na przełamanie tej stagnacji. A stało się tak, dlatego, że kolejny (już chyba 1000) raz napotkałem się na bezsensowne rozwiązanie w kodzie. Rozwiązanie to dotyczy przechwytywania wyjątków w celu ich zalogowania. Na pewno każdy z was się spotkał z tego typu kodem, który wygląda podobnie do tego poniżej:
try { DoSomething(); } catch (Exception ex) { log.Error(ex.Message); throw new Exception("DoSomething failed"); }
Co w nim jest tak złego, że aż skusiłem się napisać na ten temat?
Krótko mówiąc wszystko oprócz wywołania metody DoSomething
Po pierwsze: Nie ma najmniejszego sensu, by przechwytywać przy każdym wywołaniu kodu wszelki możliwy wyjątek po to, by zalogować jego wystąpienie, a następnie ponownie wyrzucić ten lub inny wyjątek. Dlaczego nie ma to sensu? Odpowiedź jest prosta. Jedyne uzasadnione przechwytywania wyjątków jest wówczas, gdy musimy zareagować na dany wyjątek wywołaniem odpowiedniej logiki. Logowanie wyjątków jak najbardziej zalicza się do tego typu sytuacji, przy czym najczęściej występuje to na poziomie całej aplikacji, gdzie po zalogowaniu wyjątku zapytanie klienta powodujące wyjątek zostanie zignorowane lub praca aplikacji zostanie przerwana. Oczywiście w obu przypadkach konieczne jest powiadomienie klienta o wystąpieniu błędu.
Po drugie: Zalogowanie tylko i wyłącznie wiadomości (linia 3 – właściwość Message) występującego wyjątku w większości przypadków nie wystarcza. Metoda DoSomething może wywoływać inne metody lub posiadać skomplikowane operacje, które z wielu przyczyn mogą się nie powieść. Skąd w takiej sytuacji dowiemy się, co dokładnie mogło być przyczyną wyrzuconego wyjątku? Oczywiście możemy liczyć na to, że na podstawie wiadomości wyjątku dojdziemy do źródła problemy, ale z mojego doświadczenia wychodzi na to, że to jest raczej rzadki przypadek. W takim razie, co powinniśmy jeszcze zalogować po to by uzyskać więcej szczegółowych informacji? StackTrace! Dopiero StackTrace pozwoli nam zlokalizować linię, , która w kodzie spowodowała wyjątek. Dzięki temu jesteśmy w stanie w szybki sposób dojść do rzeczywistej przyczyny błędu. Oprócz StackTrace’u należy również zalogować informacje o wewnętrznym wyjątku (InnerException) jeżeli taki wystąpił. Na ten temat będzie więcej za chwilę. Ze względu na to, że logowanie powyżej wymienionych rzeczy wydaje się bardzo oczywiste, ktoś mądry w firmie Microsoft wpadł na to, że metoda ToString obiektu Exception zwróci wszystkie tego typu informacje. Co prowadzi to tego, że metoda ToString w całości wygląda mniej więcej tak: Implementacja metody ToString klasy Exception

Po trzecie: Przechwytywanie wyjątku po to by wyrzucić nowy wyjątek innego typu ma sens. Przy czym typ nowo wyrzucanego wyjątku powinien jednoznacznie sygnalizować rodzaj problemu. W wyżej przedstawionym kodzie tak się nie dzieje, dlatego, że jest wyrzucany bazowy typ wyjątku – Exception. W przypadku stosowania opisanej przeze mnie strategii warto się zastanowić nad tym, czy nowo wyrzucany wyjątek uzbroić w InnerException w celu udostępniania bardziej szczegółowych informacji o wyrzucanym wyjątku. W celu zrozumienia tego problemu warto spojrzeć na przykład z życia wzięty.
Korzystając z biblioteki Common Service Locator izolujemy pisany przez nas kod od implementacji specyficznego kontenera Inversion of Control. W celu pozyskania przy pomocy ServiceLocator’a obiektu żądanego typu – przykładowo obiekt typu ApplicationController - wystarczy wywołać poniżej przedstawiony kod.
ServiceLocator.Current.GetInstance();
Wywołanie tego kodu powoduje przekierowanie zapytania do konkretnego kontenera IoC. Jako, że domyślnie większość z dostępnych kontenerów IoC wyrzucają wyjątek w momencie, gdy nie są w stanie pozyskać obiekt żądanego typu, nie trudno natrafić na poniżej przedstawiony przypadek wyjątku. Wyjątek ActivateException, jak sama nazwa wskazuje, mówi o tym, że wystąpił problem aktywacji obiektu przy użyciu kontenera IoC. Wyjątek ten jest specyficzny dla biblioteki CommonServiceLocator, który tak naprawdę został wywołany na wskutek przechwycenia wyjątku wywołanego przez kontener IoC – w tym przypadku StructureMap. Informacje zawarte w tym wyjątku nie wskazują na rodzaj problemu, dopiero informacje wyjątku StructureMap, który został dołączony jako wewnętrzny wyjątek (Inner Exception) pozwalają na to. Rozwiązanie to odciąża nas od przechwytywania wyjątków wyrzucanych przez użyty kontener IoC, dlatego, że jedynym wyjątkiem, którego możemy się spodziewać jest ActivateException biblioteki Common Service Locator. Dzięki temu wprowadzamy luźne sprzężenie pomiędzy kontenerem IoC a naszą aplikacją, nie tracąc możliwości śledzenia przyczyn błędów. Wyjątek ActivateException z wyjątkiem wewnętrznym
]
Struktura „klasycznej” aplikacji trójwarstwowej
Pisząc ten wpis miałem na celu porównać architekturę opartą o metodykę Domain Driven Design, z tą, która najczęściej jest stosowana przynajmniej w projektach dot netowych, czyli architekturą opartą od Data Driven Design. Ze względu na ogrom informacji, które byłyby zawarte w tym w wpisie, stwierdziłem, że lepiej będzie jeżeli podzielę to na dwa wpisy. Drugi pojawi się wkrótce – obiecuję
.
Na wstępie chciałbym również zwrócić uwagę na to, że Rafał Barszczewski w jednym ze swoich wpisów na swoim blogu poruszył podobny temat. Mam nadzieję, iż poniższy tekst będzie uzupełnieniem informacji przedstawionych przez Rafała, a nie ich powieleniem.
Zakładam, że każdy z Was jest świadom tego, że pisanie całej aplikacji w Code Behind nie doprowadzi do wielkich sukcesów projektu. Jeżeli jesteście innego zdania, nie czytajcie tego wpisu, bo prawdopodobnie nie macie do czynienia z aplikacjami biznesowymi.
Jednym z najbardziej znanych pojęć w świecie architektury oprogramowania jest luźne sprzężenie. Czy nie każdy z nas zastanawiał się nad tym, dlaczego luźne sprzężenie jest tak ważne? W każdym razie ja się zastanawiałem i to tak długo, dopóki nie musiałem zmienić prawie całej aplikacji tylko dlatego, że chciałem wprowadzić proste zmiany. Całe szczęście była to dla mnie nauczka i od tego czasu luźne sprzężenie jest najbardziej krytycznym niefunkcjonalnym wymaganiem w tworzonych przeze mnie aplikacjach.
Po tym jak zdałem sobie sprawę z tego jak ważne jest luźne sprzężenie, zacząłem się zastanawiać nad tym, co zmienić w moim podejściu do pisania kodu tak, bym nigdy więcej nie natrafił na podobne problemy. Dzisiaj już wiem, że nie ma na to złotego środka i wszystko zależy od rozważanego przypadku. Mimo to uważam, że jest pewne rozwiązanie, które jest jednym z pierwszych kroków w celu uzyskania mniej sprzężonej architektury aplikacji. Rozwiązaniem tym jest podział aplikacji na warstwy logiczne.
Większość z Was zapewne zna pojęcie warstw logicznych, ale założę się, że nie każdy jest świadom, czym one tak naprawdę są. Postaram się szybko to wytłumaczyć.
Warstwy logiczne są:
- Drugim najbardziej używanym pojęciem po luźnym sprzężeniu, jeżeli chodzi o architekturę oprogramowania:).
- Wzorcem architektonicznym pozwalającym aplikację podzielić na wiele grup podzadań (warstwy), z których każda grupa reprezentuję odpowiedni poziom abstrakcji. Grupy te mogą, a wręcz powinny, być od siebie zależne, przy czym zależności te muszą być jednokierunkowe. Poprzez jednokierunkową zależność należy rozumieć to, że grupa podzadań A, która jest zależna od grupy B, nie powinna być wykorzystywana przez grupę B. Grupa podzadań B nawet nie powinna mieć żadnego rodzaju świadomości o grupie A. Jedyne zależności jakie może mieć grupa B, to te do grup, które w hierarchii są poniżej jej.
Jeżeli chodzi o warstwy logiczne, to najczęściej znane są one ze względu na trójwarstwową architekturę aplikacji. Architektura ta zakłada, że aplikacja składa się z warstwy prezentacji zależnej od warstwy logiki biznesowej, która z kolei jest zależna od warstwy dostępu do danych.
Architektura trójwarstwowa w dzisiejszych czasach jest na tyle wszechobecna, że niemalże każdy projekt biznesowy, z którym miałem do czynienia z niej korzystał. Co nie znaczy, że jest ona złotym środkiem na wszystko. Głównym problemem związanym z tym podejściem jest to, że prowadzi ono często do tego, że każda warstwa reprezentuje jeden typ klasy.
- Warstwa prezentacji składa się wyłącznie z formularzy (Web, Desktop, etc.), w których zawarta jest logika prezentacji(Code Behind)
- Warstwa logiki biznesowej składa się z klas posiadających metody, za którymi kryje się odpowiednia logika biznesową. Metody te są najczęściej wywoływane bezpośrednio z poziomu warstwy prezentacji. Tego typu rozwiązanie znane jest jako wzorzec Transaction Script.
- Warstwa dostępu do danych zarządza dostępem do źródła danych. Najczęściej źródłem jest baza danych, ale zdarzają się również inne przypadki takie jak plik, czy inne aplikacje. W przypadku bazy danych, warstwa ta najczęściej się składa z metod, które wywołują odpowiednie zapytania SQL lub procedury składowane. Dzięki wprowadzeniu tego typu klas, jesteśmy w stanie wprowadzić luźne sprzężenie pomiędzy logiką biznesową aplikacji a źródłem danych, które aplikacja wykorzystuje.
W tym powyższym podejściu brakuje mi jednak jednej rzeczy, mianowicie obiektów domenowych. Obiekty te najczęściej znajdują swoje miejsce w architekturze trójwarstwowej, ale trudno je przyporządkować do konkretnej warstwy. Teoretycznie powinny być traktowane jako część warstwy logiki biznesowej. Jednak nie jest tak dlatego, że są one współdzielone przez wszystkie warstwy i służą jedynie jako medium komunikacji pomiędzy tymi warstwami. Należy zwrócić uwagę, że obiekty te posiadają jedynie stan (właściwości), a to znaczy, że są one typowym przykładem anemicznych obiektów domenowych.
Żeby całość lepiej zrozumieć posłużę się przykładem.
Załóżmy, że mamy aplikację, w której użytkownik posiada dostęp do przeglądania określonych zasobów. W celu przejrzenia zasobu wysyła zapytanie wraz z informacją identyfikującą określony zasób. Zapytanie to jest przechwytywane przez warstwę prezentacji i zostaje przekierowane do warstwy logiki biznesowej, gdzie z kolei jest sprawdzane, czy dany użytkownik może dokonać tego typu operacji. Następnie zostaje wykonywane zapytanie do warstwy dostępu do danych, w celu zwrócenia informacji o określonym zasobie. Zapytanie te tłumaczone jest na przykład na język SQL i zostaje wykonane zapytanie do bazy danych. Zwrócone informacje z bazy tłumaczone są do postaci obiektu domenowego, który zostaje zwrócony z warstwy dostępu do danych do warstwy logiki biznesowej. Z kolei ta sprawdza czy dany użytkownik, powinien mieć dostęp do tego obiektu domenowego. W przypadku gdy wszystko będzie się zgadzało, obiekt ten zostanie przekazany do warstwy prezentacji, w ramach której wygenerowany zostanie odpowiedni widok przedstawiający informacje zawarte w tym obiekcie domenowym.
Wszystko jest „fajne”, ale czy tego typu architektura aplikacji nadal powinna być nazywana trójwarstwową?
Architektura ta jest wygodna w przypadku prostych aplikacji, zwanych często „Data Driven”, dlatego, że aplikacje te nie posiadają skomplikowanych reguł biznesowych. Głównym problemem wyżej przedstawionej architektury jest duplikacja kodu z logiką biznesową, dlatego że stosowanie Transaction Script zmusza nas do definiowania osobnego kodu dla każdej poszczególnej operacji. Dlatego też w przypadku, gdy celem aplikacji jest realizacja wielu skomplikowanych reguł biznesowych, należy się zastanowić nad architekturą zgodną z założeniami Domain Driven Design.



Najnowsze komentarze