Grudniowy meetup (relacja tutaj) zapoczątkował ciekawą dyskusję na temat dziedziczenia w C++, interfejsów duplikacji kodu i czy czysty kod według Uncle Boba ma zastosowanie w środowisku embedded. Zainspirowany tą debatą postanowiłem podzielić się z Wami swoimi refleksjami. We wcześniejszym wpisie odniosłem się bardziej do kwestii związanych z C++, wydajnością i czystym kodem. Dzisiaj biorę na tapet decyzje architektoniczne i kompromisy projektowe.

Zacznijmy od ważnej uwagi – opinie w takim temacie są wypadkową poglądów filozoficznych i doświadczeń we własnych projektach. Każdy programista kieruje się własnymi preferencjami, a specyfika projektów różni się diametralnie. Toteż nie szukaj tu gotowych przepisów, bo jak zwykle, odpowiedź brzmi: „To zależy”.

Trudne wybory i zgniłe kompromisy

Dziwne konstrukcje w kodzie często są przejawem problemów na poziomie architektury. A ona nie jest taka prosta do zmiany. W efekcie nie możemy czegoś zrobić lepiej, bo byśmy musieli przebudować inną część kodu, co pociągnie za sobą lawinowe zmiany.

Jako inżynierowie lubimy mieć projekt porządnie zaprojektowany i zaimplementowany. Mamy rozwinięte poczucie estetyki i po prostu dobrze zrobiony projekt to dla nas powód do dumy. Do tego nad takim projektem łatwiej się pracuje, a często również lepiej działa.

Dlatego nie szkoda nam czasu na większe refactoringi, przebudowę całych modułów, poszukiwanie lepszych interfejsów. W ten sposób chcemy zrobić przysługę sobie z przyszłości i innym programistom czytającym ten kod. A przy okazji zyskujemy doświadczenie na przyszłość. Dzięki temu w podobnej sytuacji od razu będziemy wiedzieć, co się sprawdza i oszczędzimy sobie czasochłonnych prób i błędów.

Ale jest też druga strona medalu. Tak się składa, że osoby decyzyjne w projekcie zwykle nie podzielają naszego zamiłowania do perfekcjonizmu. Projekt ma realizować wymagania, ma zmieścić się w wyznaczonych deadline’ach i ma przynosić zyski. I w ten sposób dochodzimy do odwiecznego konfliktu Programista vs Biznes.

Sukces wymaga kompromisów

Kiedyś byłem jednoznacznie po stronie programisty. Jednak wraz z doświadczeniem przesuwam się coraz bardziej do środka.

Rzadko zdarza się, aby osoby pracujące w danym projekcie chwaliły jego design. Zwykle musiały gdzieś iść na kompromis. Nie są do końca zadowolone z istniejącego rozwiązania. Też takie projekty potrafią nieźle wyniszczać i po jakimś czasie chcemy po prostu dojechać do końca i pójść gdzie indziej. Znam to wszystko z autopsji.

Jednak takie projekty mają jedną wspólną cechę – zarabiają pieniądze. Pracują na utrzymanie całych zespołów developerskich przez lata. I dlatego powinniśmy do nich podchodzić z szacunkiem. Ewidentnie robią coś dobrze i dlatego odniosły sukces.

Większość developerów chce trafić do greenfielda i tworzyć projekt od zera. Mieć wpływ na architekturę, wybór narzędzi i różne inne decyzje projektowe. Ale to nie przypadek, że większość ofert to istniejące projekty rozwijane nieraz przez lata. To właśnie one udowodniły swą wartość biznesową i potrzebują rąk do pracy.

Czasami to, że spędzimy kilka miesięcy szukając idealnych rozwiązań w kodzie spowoduje, że kto inny szybciej wyda produkt i zajmie nasze miejsce na rynku. Dlatego z punktu widzenia biznesowego pragmatyzm i minimalizm są bardzo przydatne. “Better done than perfect”.

Balansowanie między perfekcjonizmem a pragmatyzmem

Dlatego powinniśmy mieć szeroki wachlarz technik i stosować je w zależności od sytuacji. Niektóre będą szczególnie przydatne właśnie gdy musimy zacisnąć zęby i dowieźć robotę na czas. Nie żyjemy w idealnym świecie.

Czy to znaczy, że powinniśmy równać w dół i domyślnie wybierać gorsze rozwiązania? Absolutnie nie! Powinniśmy jednak brać pod uwagę argumenty obu stron. I nieraz jeżeli do kwestii technicznych dodamy koszty, czas realizacji, ryzyko to nasz wcześniejszy wybór może nie wypaść już tak korzystnie.

Przykład:

W dobrze zaprojektowanym projekcie mamy interfejsy i abstrakcje. Wyższa warstwa nie zna szczegółów implementacyjnych, zna tylko interfejs. Dlatego możemy łatwo podstawić obiekt testowy. Możemy również później rozszerzać implementację. Na przykład początkowo nasze urządzenie przyjmuje komendy jedynie z ekranu i przycisków. Ale później ten sam interfejs możemy realizować na przykład z pomocą Modbusa czy TCPIP. Praca z tak zaprojektowanym systemem jest przyjemnością.

Częściej jednak w praktyce mamy do czynienia z bezpośrednim powiązaniem menu z przyciskami. Załóżmy, że chcemy zrobić niewielką zmianę. Na początek dodajemy testy, żeby upewnić się, że nie zepsuliśmy starej funkcjonalności. Zgodnie ze sztuką powinniśmy testować tylko publiczne API i nie wnikać w szczegóły implementacyjne. Jednak w praktyce nasz kod ma wiele zależności, długie funkcje, nie mamy dostępu do efektów działania bezpośrednio z API. Wniosek jest oczywisty. To wszystko symptomy problemów z architekturą – musimy ją przebudować! Ale czy na pewno chcemy przebudować pół aplikacji dla jednej małej funkcjonalności?

Dlatego mamy w swoim arsenale inne techniki. W przypadku testów możemy dostawać się do prywatnych obiektów, czy używać klas friend w C++. Może nie jesteśmy dumni, kiedy ich użyjemy ale robią robotę. A nie żyjemy w idealnym świecie.

Czy możemy zwalać całą winę na architekturę?

Architektura systemu stanowi kluczowy element, definiujący istnienie modułów i wzajemne relacje między nimi. Jednak zmiany w architekturze to zwykle złożony proces, wpływający na różne obszary projektu, zwłaszcza gdy pierwotna koncepcja nie była idealna (czyli w 99% przypadków).

W praktyce architektura systemu to efekt kompromisów. Nie jest idealna, a decyzje muszą być podejmowane w oparciu o aktualnie dostępną wiedzę. Z perspektywy czasu byśmy coś zmienili, ale nie ma czasu. A wtedy trzeba było po prostu ruszyć z miejsca. Tymczasem projekt rozwija się, nabudowujemy nowy kod dodatkowo utrudniając przyszłe zmiany.

Nieraz zdarza się również, że architektura systemu jest fikcją. A przynajmniej nie zdajemy sobie sprawy jak faktycznie wygląda. Diagramy w dokumentacji mogą nie pokrywać się z rzeczywistością, a często nawet nie istnieją. W końcu “kod mówi prawdę”.

Dlatego mówienie, że dany problem jest symptomem złej architektury i trzeba ją przebudować nie zawsze będzie dla nas pomocne.

Prawo Conwaya – czyli jak struktura firmy wpływa na architekturę

A do wszystkiego dochodzą jeszcze korpogierki. W dużych projektach tego typu zmiany wymagają całego procesu decyzyjnego. Często nie mamy sprawczości w projekcie. Duże zmiany powodują tarcia między działami w firmie, podwykonawcami, managerami i programistami i tak dalej. Stereotypowy programista znany ze swoich umiejętności interpersonalnych woli napisać coś samemu nieoptymalnie, ale z zachowaniem pełnej kontroli niż bawić się w miesiące meetingów i mailowych ping pongów.

Czym jest prawo Conway’a?

„Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.”

Czyli projekt aplikacji odwzorowuje ścieżki komunikacyjne w firmie. Między innymi dlatego, że łatwiej coś napisać naokoło niż szarpać się z korpobiurokracją. Kto nie jest winny, niech pierwszy rzuci kamieniem 😀

Więcej o prawie Conwaya u Radka Maziarki:

https://radekmaziarka.pl/2019/02/25/conways-law-jak-struktura-organizacji-wplywa-na-osiagane-rezultaty/

Co robić, jak żyć?

Dlatego, żeby radzić sobie w praktyce, musimy mieć szerszy wachlarz technik i stosować je w zależności od sytuacji. A na sytuacje składają się również czynniki biznesowe takie jak budżet, deadline’y, procedury firmowe, zastana baza kodu, ryzyko. Nie musimy wszystkiego rozwiązywać idealnie i przy okazji przebudowywać połowę systemu. Mała zmiana, która jest krokiem w dobrym kierunku nieraz wystarczy.

Kojarzy mi się to trochę z przysięgą Hipokratesa. Po pierwsze nie szkodzić.

Z jednej strony nie wprowadzać złych rozwiązań. Nie pisać paździerzu.

Z drugiej strony nie robić overengineeringu, mierzyć siły na zamiary, stosować proste rozwiązania jeśli to możliwe i wystarczające.

Ale mając to na uwadze robić refactor na bieżąco, kiedy jeszcze jest mały, kiedy jeszcze pamiętamy szczegóły i kiedy potrafimy ocenić, że dana zmiana naprawdę wyjdzie na plus, a nie tylko przepali czas.