Dobra architektura nie powstaje od razu. Jest ona raczej wypracowana na bazie różnych doświadczeń. Jednak w większości systemów jest ona określana na sztywno na samym początku, kiedy jeszcze nie mamy wystarczającej wiedzy, aby zrobić ją dobrze. Jest to źródłem wielu problemów z utrzymaniem. Zamiast tego powinniśmy pogodzić się z faktem, że dobre rozwiązania wymagają czasu i umożliwić architekturze ewolucję.
Wymagania
Zanim w ogóle siądziemy do projektowania architektury, musimy najpierw wiedzieć co nasz system ma robić. Dlatego do tworzenia architektury przystępujemy dopiero jak zdefiniujemy wymagania. Tutaj pojawia się problem, bo bardzo często osoby decyzyjne same nie wiedzą, czego tak naprawdę chcą. Najczęściej jednak jest tak, że główne funkcje systemu są jasno określone, natomiast poboczne mogą jeszcze się zmieniać. Jest to wystarczająca wiedza, żeby rozpocząć prace nad architekturą pod warunkiem, że wiemy jakie są możliwe kierunki zmian.
Architektura systemu jest pewnym kompromisem między prostotą systemu a łatwością jego rozbudowy. Każda decyzja o wprowadzeniu ogólnego rozwiązania mogącego obsłużyć różne przypadki wiąże się z większą złożonością powstającego kodu. Natomiast decyzja o napisaniu czegoś na sztywno oszczędzi nam czas teraz, ale ewentualna zmiana będzie dużo trudniejsza do wprowadzenia. Dlatego mając wymagania i wiedząc co się może potencjalnie zmienić jesteśmy w stanie świadomie podejmować te decyzje. Na początku projektu, kiedy nie ma jeszcze dużo kodu modyfikacje architektury nie są zbyt czasochłonne, więc możemy ją łatwo zmieniać.
Schemat architektury
Zwykle mówiąc o architekturze systemu mamy na myśli rysunek zawierający bloczki połączone strzałkami. Sam robiłem podobnie na przykład w swoim Micromouse – link. (Popełniłem tam wiele błędów, które opisuję w tym cyklu). Zwykle tworzymy go na samym początku projektu i jest on nam niezbędny, aby wydzielić komponenty systemu i zależności między nimi. Rozpoczynając modelowanie jeszcze sami nie mamy pełnej wizji systemu. Musimy dopiero go poznać i służy do tego właśnie rozrysowanie różnych możliwych koncepcji. Bez wątpienia taki schemat architektury jest dla nas bardzo pożyteczny.
Niestety bardzo często na takim prostym schemacie prace nad architekturą się kończą. Mimo swojej przydatności taki szkic to za mało, żeby zaimplementować cały działający system. Informacje zawarte na schemacie są bardzo ogólnikowe, a bez odpowiedniego kontekstu są one zupełnie bezwartościowe dla innych osób. Szkic architektury jest więc na pewno czymś przydatnym, ale cała architektura to jednak coś więcej.
Częstym problemem tego typu schematów jest również nadmierne skupienie na zewnętrznych bibliotekach, frameworkach itp. Mogą one pomagać w łatwiejszym zrozumieniu oczekiwań stawianych danym komponentom systemu. Często również zastosowanie gotowego kodu mocno przyspieszy projekt. Jednak architektura powinna być bardziej ogólna i nie bazować na konkretnej bibliotece. To powinien być raczej szczegół implementacyjny, który można łatwo wymienić na inną bibliotekę realizującą podobne zadania. Często też zbytnie skupienie na gotowych bibliotekach zaciemnia nam obraz i odciąga od głównych problemów systemu.
Ewolucja architektury
Skoro wymagania mogą jeszcze ewoluować, to architektura również powinna mieć taką możliwość. Tym bardziej, że nasze zrozumienie problemu z czasem rośnie i jesteśmy w stanie zaproponować optymalniejsze rozwiązania. Zwykle raczej jest tak, że architektura powstaje na samym początku i jest już wyryta w skale. W miarę jak dowiadujemy się o systemie nowych rzeczy, staramy się na siłę wpasować je w istniejącą architekturę przy minimalnych zmianach. To błąd, bo projektując początkową architekturę nie zdawaliśmy sobie sprawy z tych problemów i nie przystosowaliśmy do nich architektury. Starając się na siłę dopasować do istniejącego modelu będziemy do dawać do naszego systemu różne dziwne konstrukcje i stopniowo zmieniając go w niemożliwego do utrzymania potwora.
Podczas tworzenia architektury powinniśmy na bieżąco ją sprawdzać pisząc szkielet kodu i badając interakcje między modułami. Tylko tak możemy się dowiedzieć, że pomysł jest dobry, nie jest przekombinowany , albo niemożliwy do realizacji. Dzięki temu też nie jesteśmy architektem zamkniętym w wieży z kości słoniowej, który zupełnie zrezygnował z kodzenia na rzecz UMLa i jego pomysły są niezrozumiałe i niemożliwe do implementacji.
Dokumentowanie
Dzięki szkieletowi kodu mamy pojęcie jak system powinien działać i jesteśmy w stanie dokładniej opisać naszą architekturę. Początkowy szkic możemy uzupełnić o dokładniejszy opis kompetencji poszczególnych modułów, możemy uwiecznić na diagramach model komunikacji między nimi i zdefiniować odpowiednie funkcje API. Tutaj podoba mi się podejście z DDD, które mówi o modelu domeny opisującym działanie naszego systemu w języku zrozumiałym dla ekspertów z danej dziedziny, którzy niekoniecznie muszą znać się na programowaniu. Dzięki temu możemy korzystać z ich wiedzy definiując działanie systemu.
Tworząc architekturę powinniśmy pamiętać o odpowiednim jej udokumentowaniu. Większość osób odrzuca dokumentację jako z definicji nieaktualną, niedokładną i bezużyteczną. Jednak mądrze stosując kombinację schematów, opisów, dokumentacji w kodzie i dyscypliny jesteśmy w stanie stworzyć i utrzymywać dokumenty, które są przydatne. Warto to robić, ponieważ przeczytanie kilku akapitów dokumentacji i przeanalizowanie schematu są szybsze niż szukanie wszystkiego tylko w kodzie albo zadawanie ciągle tych samych pytań osobie, która zaimplementowała jakiś moduł. Tutaj oczywiście też trzymamy się zasady, że dokumenty ewoluują, a nie są wyryte w skale.
Nazewnictwo
DDD ma jeszcze fajne podejście do nazewnictwa. Model naszego systemu powinien posługiwać się takimi samymi nazwami, jakich używają eksperci dziedzinowi. Programiści mogą nie znać tych nazw, dlatego powinno się tworzyć słowniczek nazw specyficznych dla domeny. Często system łączy kilka różnych dziedzin i ta sama nazwa w dwóch różnych kontekstach może oznaczać coś innego. Takie konteksty to naturalne linie podziału naszego systemu i każdy z nich powinien zawierać swój niezależny słowniczek. To takie krótkie streszczenie koncepcji z DDD, zainteresowanych zachęcam do poczytania o DDD, ubiquitous language i bounded context.
Dlaczego nazwy są ważne? To one sugerują do czego dane symbole są wykorzystywane i przyspieszają zrozumienie kodu. Dzięki odpowiednim nazwom programiści będą mogli lepiej porozumieć się z osobami nietechnicznymi. Dlatego podczas ewolucji architektury nie możemy bać się zmieniać nazw, jeżeli są one mylące albo nie odzwierciedlają dobrze opisywanych symboli.
Podsumowanie
Najważniejsze wnioski można streścić jako:
- Najpierw wymagania, potem architektura.
- Nie bójmy się eksperymentować.
- Zdobywając wiedzę o systemie widzimy ulepszenia, które możemy wprowadzić.
- Dokumentujmy naszą architekturę, dokumenty też powinny ewoluować.
Niby zasady wydają się proste, ale jednak często o nich zapominamy
3 lutego 2019 at 22:12
Odnośnie rozbudowy architektury i późniejszych nieścisłości – to też było w jakiejś książce – że jak pracujemy nad POCem jakimś wydzielonym (co w embedded może być nowym driverem itp), który ma być szybką weryfikacją czegoś i działa – to nie powinniśmy się przywiązywać do tego kodu, tylko jeśli funkcjonalność będzie zatwierdzona – „zaorać go” i dostosować do standardów w projekcie, żeby uwspólnić wszystko i na to też przewidzieć czas w estymacjach.
Też warto ustalić sposoby komunikacji między modułami w projekcie, żeby wiedzieć czego się spodziewać – łatwiej nawigować po takim projekcie/wdrożyć kogoś nowego.