Wielu programistów zapewne wielokrotnie zetknęło się z kodem, o którym mogło powiedzieć, że jest ‚przekombinowany’. Często w takim wypadku można usłyszeć o over-engineeringu. Czy jednak zawsze właśnie z nim mamy do czynienia? Jaka jest definicja over-engineeringu, która jasno wyznacza co nim jest, a co nie? Czy istnieje jego przeciwieństwo? Przekonajmy się.


Adam Tulejko. PHP Developer w Piwik PRO. Backend. Web Developer z kilkunastoletnim stażem, głównie w PHP. Zwolennik łączenia dobrych praktyk z pragmatycznym podejściem, w szczególności interesuje się zagadnieniami dobrego designu w oprogramowaniu oraz przetwarzaniem dużych ilości danych. Obecnie w Piwik PRO zajmuje się rozwojem i optymalizacją platformy web analytics, obsługującej codziennie miliony requestów.


Over-engineering – czy istnieje tylko w programowaniu?

Zjawisko to nie jest spotykane wyłącznie w świecie programowania. Możemy się z nim zetknąć wszędzie tam, gdzie mamy do czynienia z projektowaniem. Co więcej, over-engineering jest pojęciem odnoszącym się nie tylko do skomplikowania danego rozwiązania, ale także jego współmierności do potrzeb. Ekspres do kawy może być nie tylko zbyt skomplikowany w obsłudze, ale także zbyt wytrzymały w stosunku do obciążeń, którym jest poddawany. Jeśli tak jest, mamy do czynienia z marnotrawstwem, gdyż najpewniej użyto do niego zbyt drogich materiałów, które mogły być wykorzystane w bardziej odpowiedni sposób.

Nie zawsze over-engineering jest czymś złym. Zdarza się, że jest świadomym działaniem. Dotyczy na przykład rozwiązań kluczowych dla bezpieczeństwa. Lepiej, aby nośność mostu była wyższa niż jego codzienne obciążenie. Przyczyną świadomego over-engineeringu bywa też dbałość o jakość i trwałość. Jeszcze w latach 90-tych często projektowano silniki spalinowe z istotnym zapasem wytrzymałości, które dobrze znosiły nawet znaczne podniesienie mocy.

Przeciwieństwo

Ok, a czy istnieje przeciwieństwo over-engineeringu? Tak! O wiele częściej spotykanym zjawiskiem jest under-engineering, czyli włożenie niedostatecznego wysiłku w przemyślenie rozwiązania danego problemu. Paradoksem under-engineeringu jest to, że w jego wyniku często powstają rozwiązania, które są niepotrzebnie skomplikowane. Tak więc nadmiernie skomplikowany kod może być wynikiem zarówno over- jak i under-engineeringu. Uniknięcie jednego i drugiego jest trudne do osiągnięcia i nie ma na nie prostego algorytmu. Najlepszym sposobem, aby przybliżyć się do ideału jest dobre poznanie domeny, którą oprogramowujemy. Im większa wiedza na jej temat, tym łatwiej będzie nam zadecydować, co jest wystarczającym rozwiązaniem, a co nie.

Przypadki under- i over-engineeringu

Zasada, której należy przestrzegać, aby unikać obydwu zjawisk jest dość prosta: nie pisz niepotrzebnego kodu i szukaj prostych rozwiązań. I na tym zdaniu mógłby się skończyć cały wywód. Tylko co to oznacza w praktyce? Nic nie pozwala tak dobrze czegoś zrozumieć jak dobry przykład. A najlepiej wiele przykładów. Under- bądź over-engineering objawia się jako:

1. Bezpodstawne założenia

W przypadku, gdy dana domena nie jest dokładnie poznana, a założenia co do wymagań biorą się z wyobrażeń na jej temat. Konsekwencją jest nie tylko kod realizujący wymagania, których nie ma, ale także nieodpowiedni dobór technologii.

2. Zbyt daleko posunięty design

Próby przewidywania dalszego kierunku, w którym będzie rozwijać się aplikacja na ogół kończą się porażką. Oprogramowywanie tych przewidywań tym bardziej. Należy zrobić tylko to, do czego jest pewność, że będzie potrzebne [YAGNI!].

3. Większe jest lepsze

Czyli celowe tworzenie rozwiązań mających wzbudzić podziw, np. u osoby robiącej code review. Przynajmniej w mniemaniu autora kodu. Do tego przypadku można także zaliczyć tworzenie wymyślnych nazw klas i zmiennych, nie zawierających słownictwa związanego z oprogramowywaną domeną, chociaż istnieje taka możliwość.

4. Funkcjonalności o niskiej wartości

Tworzenie bądź pozostawianie fragmentów kodu, które nie są używane lub wręcz nie są związane z produktem (np. w wyniku bezmyślnego kopiowania gotowych rozwiązań ze Stack Overflow).

5. Nadmierne myślenie

Zbyt głębokie i zbyt długie analizy mogą prowadzić do zagubienia i przeoczenia najprostszego, bezpośredniego rozwiązania.

6. Pierwsza myśl

W programowaniu zwykle pierwsze pomysły nie są tymi najlepszymi. Pierwsza implementacja bywa dość zagmatwana. Wynika to m.in. z tego, że podczas niej dopiero poznajemy daną domenę. Rozwiązaniem jest refaktoryzacja. Najczęściej prowadzi ona do znacznego uproszczenia naszego kodu.

7. Big ball of mud

Narastająca złożoność wynikająca z niestosowania się do dobrych praktyk i wzorców, których celem jest przecież przedstawienie gotowych, prostych rozwiązań powszechnych problemów.

8. Wszędzie wzorce projektowe

Wciskanie wzorców projektowych wszędzie tam, gdzie wydaje się nam, że pasują. Użycie wzorca powinno upraszczać rozwiązanie oraz ułatwiać rozszerzanie danego kodu w przyszłości.

9. Nadmierna kontrola

Implementowanie w kodzie wszelkiego rodzaju gwarancji, gdy nie jest to potrzebne (np. kolejności procesowania danych lub dostarczenia wiadomości).

Prawdziwy przykład

Powyższa lista z całą pewnością nie przedstawia wszelkich możliwych przypadków, ale daje rozeznanie jak under- i over-engineering mogą wyglądać. Aby jeszcze lepiej poczuć, czym one są, rozpatrzmy konkretny przypadek. Zaprojektujmy zbiór interfejsów służących do pobrania danych z dowolnego źródła i dodania ich do bazy danych.

Wiemy, że na początku dane będą pobierane z plików CSV znajdujących się na zewnętrznym serwerze. Potrzebujemy więc interfejsu fetchera plików, który zapisze pobrany plik do tymczasowego katalogu:

Potrzebujemy także readera zapisanego pliku, który zainicjuje bufor, za pomocą którego zawartość pliku będziemy sukcesywnie wczytywać do pamięci:

Skoro mamy już zawartość pliku CSV w pamięci, trzeba go podzielić na linijki, a także obsłużyć każdą z nich (np. przekształcić dane w kolumnach) i zapisać do bazy danych. Posłużą nam do tego klasy implementujące następujące interfejsy:

Dla każdego wiersza FileHandler będzie odpalać handle() z RowHandler’a:

RowHandler będzie używać do zapisu każdej linijki do bazy klas implementujących PersistentStore:

Jak widać powstała nam całkiem pokaźna struktura interfejsów. Tylko czy potrzebna? Na początku wiemy, że będziemy pobierać pliki CSV. Nie wiemy natomiast, czy ten format będzie zawsze obowiązywał, skoro dane mają być pobierane z dowolnego źródła. Nie zawsze też jest potrzeba, aby plik zapisywać najpierw na dysku, a dopiero potem wczytywać partiami do pamięci. Tworząc powyższe interfejsy narzuciliśmy przyszłym implementacjom tworzenie być może niepotrzebnego kodu oraz potencjalnie wydłużyliśmy oprogramowanie trywialnych przypadków. Nikt nie siądzie z radością do implementowania tych interfejsów, gdy do obsłużenia będą małe pliki w formacie innym niż CSV.

Jak zatem przy tym stanie wiedzy powinny wyglądać nasze interfejsy? O wiele prościej. Choćby tak:

Zależnością naszego FileHandler‚a powinna być instancja PersistentStore. I to wszystko. To jak powinna wyglądać implementacja zostawiamy implementującemu. Dla małych plików CSV będzie to prawdopodobnie jedna klasa. Dla większych będzie ona bardziej zbliżona do struktury powyżej. Taki projekt nie zamyka nam też drogi do obsługi jakiegokolwiek formatu plików.

Podsumowanie

Jak widać unikanie over-engineeringu to tak naprawdę poszukiwanie złotego środka pomiędzy przeprojektowaniem i niedostatecznym zaprojektowaniem. Kluczem do jego osiągnięcia jest dobre poznanie domeny, a po nim dążenie do prostych rozwiązań. Zarówno over- jaki under-engineering często skutkują zbyt skomplikowanym kodem, którego utrzymanie jest trudne, powoduje coraz wolniejsze dodawanie kolejnych funkcjonalności i niekorzystnie wpływa na morale samych deweloperów. Warto więc codziennie do tej równowagi dążyć.


Zdjęcie główne artykułu pochodzi z picjumbo.com

Zapraszamy do dyskusji
Nie ma więcej wpisów

Send this to a friend