Programowanie to nie tylko ‘kodzenie’ i wystawianie faktur – to także sztuka rozwiązywania skomplikowanych problemów w jak najprostszy możliwy sposób.

Często w swojej pracy spotykałem się z tak zwanym ‘legacy code’ - nie mniej jednak obcowanie z nim (zwłaszcza dla nowego członka zespołu) czy refactoring takowego jest prawdziwą katorgą, a i często prowadzi to nieprzewidzianych problemów co implikuje na niepotrzebne koszty (finansowe / czasowe). Dlatego istotnym jest sam projekt, podejście do architektury – nie tylko do technologii.

Zbierając swoje doświadczenia - choć nie są jakieś wielce pokaźne, ale za to różnorodne - chciałbym zaproponować wariacje znanych już metodyk. Głównym celem jest odseparowanie warstwy samej aplikacji od warstwy technologii (frameworku). Stąd pomysł na FIP – Framework Isolation Pattern.

Jako że nie ma sensu wymyślać koła na nowo, a dużo lepiej korzystać z istniejących rozwiązań, doświadczeń oraz łączyć je w nowy sposób - tak też warto poznać istniejące rozwiązania, które na szybko przybliżę.

Zacznijmy od początku - czyli projektowania.


Domain-Driven Design (DDD) – Projektowanie zorientowane na domenę

Podejście to jest zorientowane na modelowanie domeny biznesowej – sama koncepcja została zaproponowana przez Erica Evansa. Cechuje się większą rolą potrzeb biznesowych nad technologicznymi – powinna odzwierciedlać rzeczywiste procesy biznesowe (oczywiście w ujęciu na odpowiednim poziomie abstrakcji). Sama logika biznesowa powinna być oddzielona od technologii czy frameworków. Cechuje się podziałem na konkretnie wyszczególnione warstwy:

  • Domain – warstwa domeny – w których zawieramy takie elementy jak encje, agregaty, reguły biznesowe.
  • Application – warstwa aplikacji - zawierająca przypadki użycia oraz ogólnie pojęte zarządzanie przepływem danych.
  • Infrastructue – warstwa infrastruktury – techniczna warstwa, repozytoria, servicy frameworku
  • User Interface – warstwa interfejsu użytkownika - UI lub API.


Zalety takiego podejścia to między innymi spójność modelu biznesowego czy dobre, spójne zarządzanie złożonością dużych aplikacji. Także istotnym argumentem jest ułatwienie współpracy między zespołami developerskimi, a biznesem (łatwiejsza komunikacja, lepsze zrozumienie wspólnych problemów).


Architektury

Jest kilka zaproponowanych architektury które rozwiązują te, bardzo zbieżne do siebie problemy – do bardziej znanych i cenionych należą między innymi:


Clean Architecture – Czysta architektura

Zaproponowana przez Roberta C. Martina – zwanego i znanego też jako Uncle Bob. Definiuje ona jasny, sztywny podział między logiką biznesową, a zależnościami technologicznymi.


Do głównych założeń należą:

  • Reguła zależności - zależności powinny być skierowane do wewnątrz systemu. To znaczy, że same frameworki, bazy danych czy interfejsy użytkownika nie powinny mieć wpływu na logikę biznesową.
  • Separacja warstw – aplikacja powinna mieć wydzielone warstwy, które to każda kolejna powinna zależeć jedynie o bardziej centralnej (wewnętrznych w tym wypadku – metodyka od ogółu do szczegółu).
  • Łatwość testowania – centralna logika powinna być niezależna od infrastruktury, co rzutuje na łatwość testowania jednostkowego.


Zaproponowana struktura warstw:

  • Entities – modele domenowe, zasady biznesowe niezależne od samej aplikacji.
  • Use Cases – konkretne przypadki użycia aplikacji, realizujące logikę biznesową.
  • Interface Adapters – warstwa adaptacyjna. Do tej warstwy możemy zaliczyć takie elementy jak kontrolery, repozytoria czy konwertery danych (Data Providery)
  • Framework & Drivers - obrzeża samej aplikacji czyli sama infrastruktura, bazy danych, interfejsy użytkownika (UI)


Zalety to na pewno niezależność od frameworków - sam kod aplikacji nie jest sztywno związany z żadnym elementem. Atutem także jest łatwość zamiany technologii – zmiana samego frameworka na inny lub też bazy danych. Daje to także możliwość prostego testowania samej aplikacji.


Hexagonal Architecture – Architektura Heksagonalna

Znana także jako architektura Ports and Adapters – zaproponowana została przez Alistaira Cockburna. Celem było stworzenie takiego podejścia, które może komunikować się z różnymi systemami zewnętrznymi poprzez dobrze zdefiniowane interfejsy (porty).


Główne założenia architektoniczne:

  • Sama aplikacja nie powinna być zależna od frameworka / baz danych – tak samo jak w poprzednim omawianym przypadku.
  • Wszelka komunikacja z zewnętrznymi systemami powinna odbywać się przez interfejsy.
  • Rdzeń aplikacji jest otoczony adapterami - które umożliwiają współpracę między różnymi technologiami.


Struktura:

  • Core appliacation – domena i logika biznesowa – niezależna od frameworków
  • Ports – interfejsy - definiują punkty wejścia / wyjścia dla aplikacji (np. Interfejsy repozytoriów)
  • Adapters – adaptery – implementacja portów, które łączą aplikację z konkretną infrastrukturą (bazy danych. API, UI)


Onion Architecture – architektura cebulowa

Podejście zaproponowane przez Jeffreya Palermo, które skoncentrowane jest na warstwowym modelu, w którym zależności są skierowane do wewnątrz (podobnie jak w przypadku poprzednich architektur).


Struktura warstw:

  • Domain – warstwa domeny – centralna warstwa zawierająca encje i logikę biznesową aplikacji.
  • Application (Use Caces) - przypadki użycia - definiuje przypadki użycia w aplikacji
  • Infrastructure – warstwa infrastruktury – implementacja dostępu do baz danych czy interfejsów użytkownika.
  • Presentation – warstwa prezentacji – odpowiedzialna za interakcje z użytkownikiem, czyli np. REST API, widoki frontendowe


Zalety - zbieżnie jak w poprzednich przedstawionych architekturach – to całkowita separacja kodu aplikacji od technologii (frameworka). Daje także swobodę w testowaniu logiki biznesowej niezależnie od samej infrastruktury czy co istotniejsze w dzisiejszych czasach – pozwala na dogodną skalowalność.


FIP – Framework Isolation Pattern

Moja osobista wariacja na temat tych metodyk i podejść przedstawionych powyżej. Bardziej teoretyczna - choć w pewnym zakresie zrealizowana przy okazji mojego ostatniego projektu.


Otóż - główne założenia FIP:

  • Logika biznesowa, główna logika aplikacji powinna być wydzielona i być niezależna od technologii (frameworku).
  • Interakcja z frameworkiem lub innymi technologiami odbywa się poprzez wyraźnie zdefiniowane interfejsy lub adaptery.


Teoretyczna struktura:

/app
  /core         <- logika biznesowa, całkowicie niezależna od frameworka
  /ports        <- interfejsy do interakcji z frameworkiem
/adapters       <- implementacje interfejsów korzystające z frameworka
/framework      <- konfiguracja frameworka (np. kontrolery, routing)

 

Takie podejście daje przejrzystość samej aplikacji – wydzielenie na poszczególne komponenty, warstwy ułatwia migrację między frameworkami, samo testowanie aplikacji jest prostsze, a tym samym przyjemniejsze. Oraz co istotniejsze – zdecydowanie ułatwia utrzymanie samego kodu w dłuższej perspektywie czasu.

Dodatkowo – poprzez zastosowanie interfejsów / adapterów - można narzucić własną nomenklaturę nazewniczą w samej aplikacji. Framework (czy to Laravel czy to Symfony czy inny) jest tak naprawdę zbiorem różnych paczek composer'owych - często o różnym podejściu nazewniczym do ich klas oraz metod dostępnych. Opakowanie ich w dodatkową warstwę (serwisy, repozytoria np. ) daje nam większą swobodę, przez co łatwiej jest zastosować tzw. Sugar Syntax. To podejście rzutuje na szybsze i prostsze zrozumienie samego kodu aplikacji (czy to dla nowych członków zespołu, czy nawet dla biznesu - można np. W PHP pisać jak w jakimś pseudokodzie, zrozumiałym nawet dla osób nie znających się na sztuce programowania).

Takie podejście - opakowanie – ma także inną, kluczową zaletę. Łatwość przy refactorze – dokonujemy zmian jedynie w obrębie zaimplementowanych adapterów / interfejsów, a nie samej logiki aplikacji.

Pobocznym staje się także lepszy wgląd w używany zewnętrzny kod - może okazać się, że z danej, wielkiej paczki używamy raptem jednej lub dwóch metod. Czy jest sens utrzymywać w takiej sytuacji taki element niezależny od nas w samej aplikacji - której to zależności mogą wpływać na inne (częsty problem w composer czy npm np.)? Odpowiedź brzmi: NIE. Wtedy lepiej taki element napisać samemu – dostosowany w zupełności do naszych potrzeb i wymagań.