Antywzorce w projektach IT
Mogę iść o zakład, że w trakcie swojej kariery w świecie IT spotkasz się z takim pytaniem: Czy warto inwestować w dobrą architekturę systemu? Jeśli nie zostanie ono zadane bezpośrednio Tobie, zapewne usłyszysz dyskusje na ten temat - kto wie, może na sąsiednim panelu albo w firmowej kuchni. Jest to bowiem problem, który częstokroć jest na tapecie. Cykliczne debaty o architekturze to zjawisko zupełnie niezależne od wybranych technologii czy charakterystyki projektu... Wracając zatem do pytania: Czy to naprawdę jest gra warta świeczki?
Czy warto zadbać o dobrą architekturę?
Z programistycznego punktu widzenia odpowiedź jest jedna: Oczywiście, że tak! Argumentacja, na której można oprzeć to stanowisko, zawiera się w kilku elementach. Uporządkowana struktura systemu procentuje w dłuższej perspektywie. Porządek sprawia, że składowe elementy systemu są od siebie niezależne. Modyfikacje w takim przypadku są szybkie i jednocześnie łatwe do przetestowania. Prosto jest także nawigować po projekcie, kod jest czytelny, przystępny i zrozumiały. Pracę w takich warunkach można śmiało określić: "programistycznym rajem". Energia zainwestowana w architekturę systemu procentuje wysoką produktywnością.
Hipoteza kondycji systemu Martina Fowler’a
Co zatem stoi na przeszkodzie harmonijnej i spójnej architekturze? Brutalna rzeczywistość, czyli względy czysto ekonomiczne. Z punktu widzenia klienta, wewnętrzna organizacja kodu programu nie ma znaczenia. Liczy się jedynie to, czy produkt spełnia swoje wymagania. Nikt z nas nie zapłaci za produkt więcej tylko dlatego, że w jego wnętrzu, linie kodu tworzą ład i porządek. Dbałość o szczegóły wewnątrz projektu to dodatkowa praca, którą ktoś musi wykonać, a to przecież kosztuje.
Jeśli zależy Ci na podniesieniu swoich umiejętności w obszarze architektury oprogramowania, zapraszam Cię do poświęcenia odrobiny czasu na zapoznanie się z tym artykułem. Starałem się w nim opisać najbardziej powszechne błędy i tendencje, które generują problemy w dłuższej perspektywie.
Wierzę, że wiedza o zagrożeniach, jakie czyhają na twórców oprogramowania oraz informacje o sposobach wychodzenia z trudnych sytuacji, pomoże podnieść jakość rozwijanych produktów oraz podniesie komfort pracy każdego programisty.
Antywzorzec: “Boski Obiekt”
Co to takiego? Jak można rozpoznać ten antywzorzec? Przede wszystkim jest to klasa dużych rozmiarów, w której można zaobserwować realizację rozmaitych funkcjonalności. Mamy do czynienia z czymś, co można nazwać np. „trzy w jednym”. Nie istnieje także żadne rozgraniczenie tego, co dzieje się w systemie poprzez interfejsy.
Boski Obiekt to element systemu, we wnętrzu którego linie kodu liczymy w tysiącach. Jest to tylko jedna klasa. Jej rozmiar można przyrównać do kosmosu - niemal nieograniczony i wciąż rozszerzający się dalej. Odpowiedzialność także jest wyniesiona wysoko. Praktycznie każda akcja systemu jest wykonywana pod nadzorem Boskiego Obiektu lub przezeń bezpośrednio.
Problemy, jakie idą w parze z tym antywzorcem, wynikają z umieszczenia zbyt wielu elementów w jednym komponencie. Taki stan rzeczy powoduje problemy z utrzymaniem kodu takiej klasy oraz generuje komplikacje w wyodrębnianiu funkcjonalności. Co więcej, tworzenie rozbudowanych elementów skutkuje dużym obciążeniem pamięci. Nawet w przypadku, gdy sięgamy po małe i proste operacje, używamy znacznie więcej zasobów, niż powinniśmy.
W przypadku dziedziczenia Boskich Obiektów, klasy potomne otrzymają cały zestaw niepotrzebnych składowych, a to jest wbrew zasadzie ponownego użycia. Chcemy przecież tworzyć kod wysokiej klasy, bez zbędnych składników i korzystać jedynie z tego, co nam potrzebne. Dodatkowe problemy pojawiają się także przy pisaniu testów do takiej klasy. Nawet gdy zależy nam na przetestowaniu małego fragmentu, musimy zadbać o konfigurację masy dodatkowych kawałków. To droga przez mękę.
„Boski Obiekt” - przykład
Jak możemy poradzić sobie z tym antywzorcem?
Faktoryzacja powinna podążać w kierunku uproszczenia. Używamy oddzielnych klas dla pól lub właściwości. Metody powinny być małe i gdy tylko jest taka możliwość, logicznie rozdzielone. Dobrym pomysłem będzie skorzystanie z zasady "segregacji interfejsów". Polega ona na tym, że zależności pomiędzy klasami powinny opierać się na minimalnych kontraktach, zawieranych właśnie za pomocą interfejsów.
Sposób na pozbycie się „Boskiego Obiektu”
Osobna klasa reprezentująca „EmployeeModel”
Rozbicie na dodatkowe interfejsy: „Import Export”
Nowa klasa do obsługi bazy danych
Walidator do obsługi operacji
Antywzorzec: System, który gra i tańczy
Angielska nazwa tego antywzorca to: "Stovepipe system”. Używa się jej do określania systemów, w których możliwe jest udostępnienie danych lub funkcji innym systemom, ale z różnych przyczyn tak się jednak nie dzieje. Gdybyśmy chcieli przetłumaczyć ten termin dokładnie na polski, byłoby to coś w rodzaju “systemu rur kuchennych”. Każda z nich działa niezależnie, np. obsługując tylko wybrane mieszkanie, choć tak naprawdę efektywniej byłoby je pogrupować, połączyć we współdzielone piony i w ten sposób całość nieco uprościć. W końcu realizują one dokładnie te same zadania.
Najczęściej z tym problemem możemy spotkać się podczas pracy nad projektem rozwijanym przez lata. Największy nacisk kładzie się wtedy na utrzymanie systemu. Sprawdzona funkcjonalność ma działać, niezależnie od zmieniających się dookoła warunków. Pojawiające się błędy, które są powszechne i częste, naprawia się ad-hoc. Cel jest jeden: przywrócić działanie produktu najszybciej jak to możliwe. Miejsca na głębsze analizy i przemyślenia praktycznie nie ma. Tego rodzaju "łatanie" prowadzi do chaotycznej struktury, braku koordynacji oraz duplikacji kodu.
Z biegiem czasu, gdy projekt rozrasta się w dzikich, nieregulowanych warunkach, składowe elementy systemu zaczynają plątać się wzajemnie. Zmieniające się strategie i wizje produktu, różne doświadczenie oraz podejście programistów angażujących się w projekt na przestrzeni lat, skutkuje brakiem wydzielania abstrakcyjnych części dla podsystemów. To z kolei skutkuje wiązaniem punkt-punkt dla klas i niełatwo jest potem użyć tych elementów w innych miejscach. Implementacja tego rodzaju systemu jest krucha. Istnieje bowiem wiele zależności, które są ukryte i uwarunkowane przez konfiguracje, szczegóły instalacji, czy stan systemu. Wprowadzanie zmian i nowych funkcjonalności potęguje złożoność systemu, a dalsze utrzymanie tego systemu jest pełne komplikacji.
Jak rozpoznać objawy tego antywzorca?
- Duża rozbieżność między dokumentacją a zaimplementowanym oprogramowaniem: opisy funkcjonalności nie są aktualne
- Zmiany wewnątrz systemu są kosztowne w realizacji, a utrzymanie generuje zaskakujące koszty
- Rozwiązanie pojawiających się w systemie błędów wymaga od programistów wymyślania obejść, by poradzić sobie z ograniczeniami
- Z biegiem czasu zmiany w projekcie stają się coraz trudniejsze do wykonania
- Dodawanie nowych funkcjonalności do systemu skutkuje odkrywaniem nowych, poważnych błędów w niespodziewanych miejscach
Problemy zauważalne od strony architektury
- Brak abstrakcji
- Wiele unikalnych interfejsów
- Ścisłe powiązania pomiędzy parami klas
- Występowanie wielu różnych mechanizmów integracji podsystemów
- Brak jakiejkolwiek wizji rozwoju architektury projektu
Refaktoryzacja, czyli kierunek naprawy
Zmianę systemu należy rozpocząć od wyznaczenia podstawowego poziomu funkcjonalności, który będzie można dzielić przez wiele podsystemów, a w idealnym przypadku, przez wszystkie z nich. Może to być np. jeden wspólny sposób wymiany informacji między komponentami. Następnie trzeba zdefiniować interfejsy, które mogą realizować obsługę wszystkich podstawowych operacji. Wydzielenie tego rodzaju podstawowego poziomu daje możliwość zwinniejszego zarządzania implementacją. Redukujemy splatanie i zależności, a to zawsze krok w dobrą stronę.
Przykład:
Na rysunku poniżej widać podsystemy klienta oraz podsystemy realizowanych usług. Każdy podsystem posiada swój unikalny interfejs. Wszystkie instancje podsystemów są modelowane jako kolejne komponenty w diagramie klas. Rozwiązanie takiego przypadku uwzględnia wydzielenie wspólnej abstrakcji między podsystemami. Ponieważ istnieją dwie usługi każdego typu, każdy model może mieć jeden lub więcej interfejsów usług. Następnie każda konkretna usługa może być opakowana w celu obsługi wspólnej abstrakcji interfejsu.
Przykład uporządkowanej struktury systemu
Jeśli do systemu zostaną dodane dodatkowe urządzenia z tych abstrakcyjnych kategorii podsystemów, będzie można je zintegrować z istniejącym oprogramowaniem w łatwy sposób.
Antywzorzec: "Błotna bryła"
Jak go rozpoznać?
Jest to duży, chaotycznie uporządkowany, niechlujny system, który przypomina gęsto porośniętą dżunglę. Nawigowanie po kodzie jest trudne, na każdym kroku czai się niebezpieczeństwo i nie do końca wiadomo, co jest przed nami. To w czystej postaci antywzorzec wielkiej błotnej bryły.
Taki układ cechuje nieuregulowany wzrost oraz wielokrotne przeklejanie w różnych miejscach tych samych kawałków kodu. Dzielenie informacji odbywa się głównie na poziomie globalnym, nawet gdy sytuacja wymaga ścisłej kooperacji punkt-punkt. Dla tego antywzorca struktura systemu nigdy nie była definiowana. Nie ma żadnym wytycznych, w jaki sposób należy rozwijać projekt. Nikt także oficjalnie nie był nigdy przypisany do roli architekta... Każdy programista może robić dokładnie to, co chce i uważa za słuszne. Może to prowadzić do nieporozumień i późniejszych konfliktów między osobami rozwijającymi system.
Jak dochodzi do "narodzin" tego antywzorca?
Najczęściej poprzez ewolucję drobnego kawałka podsystemu, który z założenia został powołany do życia jedynie do realizacji kilku funkcjonalności i to w ograniczonym fragmencie czasu. Z różnych jednak przyczyn, przyjęte wytyczne tracą na znaczeniu. W pierwszej kolejności wydłuża się okres działania projektu. W myśl zasady: dlaczego rezygnować z czegoś, co działa?
Następnie taki system nieco się rozrasta. Stopniowo dochodzą nowe możliwości i kolejne linie kodu. Dokumentacja w dalszym ciągu nie wydaje się potrzebna, a porządkowanie architektury wygląda na zbędny wydatek. Pojawiające się problemy w działaniu systemu rozwiązuje się szybkimi zmianami, najczęściej poprzez dodawanie kolejnych instrukcji sterujących if/else. Kompleksowe rozwiązanie przyczyn u źródła nie jest nawet brane pod uwagę. Taki projekt zaczyna się lepić i przypominać bryłę...
Co możemy zrobić, by pozbyć się tego problemu?
Radosna twórczość programistów, gdzie każdy robi, co chce i jak chce, musi się zakończyć. To warunek wstępny, można powiedzieć - podstawowy. Najłatwiej jest to zrobić przez wyznaczenie osoby lub grupy ludzi odpowiedzialnych za przeglądanie implementacji innych osób. Bez ich aprobaty na produkcje nie może dostać się nic. Wypracowanie takiego przepisu skutkuje pobudzeniem do jakościowo lepszej pracy.
Dlaczego? Odpowiedź jest prosta: kod zostaje prześwietlony i zanalizowany przez innych doświadczonych programistów. Nasze arcydzieło sztuki „koderskiej” jest poddane próbie. Przestajemy być sędzią we własnej sprawie. Podskórnie czuć, że trzeba postarać się bardziej. Porządkowanie systemu nie tylko poprawia jego wewnętrzną organizację. Jest to także świetny materiał do doskonalenia umiejętności. Konstruktywne przeglądy implementacji, gdzie opieramy się jedynie na faktach, są świetną radą odnośnie tego, co poprawić w swym programistycznym fachu.
Refaktoryzacja “błotnej bryły” musi kierować się w stronę eliminacji duplikowanych funkcjonalności, pełnego pokrycia produktu testami oraz rozdzielaniem niezależnych części systemu, np. poprzez rozbicie na mniejsze kawałki, tworzenie pomocniczych klas i wykorzystanie nowych interfejsów. Dobrym pomysłem jest wyznaczanie pasujących do danego systemu wzorców projektowych, które dobrze wkomponują się w wizję lepszego produktu.
Podsumowanie
Na koniec tego artykułu chciałbym wlać nieco otuchy w serca tych, którzy rozpoznali wyżej wymienione antywzorce w projektach, nad którymi pracują. Błędy są nieuniknione, popełnia je każdy z nas. To nieodłączny element naszego życia. To samo dotyczy programowania. Jedyne co możemy z tym faktem zrobić, to odpowiednio "zadbać" o problemy. Ostatecznie nawet najpoważniejsze i głęboko zakorzenione defekty da się wyeliminować, choć niemałym kosztem. Tym mniejszym, im wcześniej je zmienimy.
Warto dbać o wewnętrzny porządek i ład systemu. Utrzymanie architektury na dobrym, zdrowym poziomie, procentuje z biegiem czasu. Ważne jest wyznaczenie odpowiedniego poziomu jakości kodu, który rozwijamy. Nawet małe optymalizacje i zmiany, które pozornie są niezauważalne, w ogólnym rozrachunku przybliżają produkt do idealnego stanu. Może warto zastanowić się nad zmianą obecnej architektury systemu? Pozbyć się złych rozwiązań, które często są złączone z projektem od lat? I wreszcie, załatwić sprawy refaktoringu tak, by antywzorce były tylko wspomnieniem?
O autorze
Piotr Kacprzak to Senior Software Developer odpowiadający za produkt Antenna System Ericsson. Obecnie pracuję nad utrzymaniem i wparciem urządzeń działających w oparciu o standard AISG (Antenna Interface Standards Group) 2.0, a także rozwojem nowych funkcjonalności z tego obszaru. W wolnych chwilach dotlenia umysł, jeżdżąc na rowerze.