Benoît B. Mandelbrot (ur. 20 listopada 1924 w Warszawie, zm. 14 października 2010 w Cambridge, Massachusetts) – francuski i amerykański matematyk pochodzenia polsko-żydowskiego. Zajmował się szerokim zakresem problemów matematycznych, znany jest przede wszystkim jako ojciec geometrii fraktalnej, opisał zbiór Mandelbrota oraz wymyślił samo słowo „fraktal”.
Wikipedia
W świecie oprogramowania, modularyzacja ma kluczowe znaczenie dla utrzymania elastyczności, skalowalności i czytelności kodu. Uczestniczyłem w wielu dyskusjach na temat tego, jaki rozmiar powinien mieć moduł. Padają w nich często skrajne opinie, począwszy od „lepszych jest kilka dużych modułów”, po „im mniejsze moduły, tym lepiej”. Prawda jak zawsze leży gdzieś pośrodku, a w tym wpisie wskażę, jak ją namierzyć.
Czym jest moduł
W świecie inżynierii oprogramowania, moduł nie ma jednoznacznej definicji. Dla jednych, modułami mogą być mikrousługi, dla innych pliki JAR lub pakiety, a dla jeszcze innych moduły Maven lub subprojekty Gradle. Ta różnorodność w definicjach komplikuje dyskusję na temat modularyzacji.
Dla potrzeb tego artykułu, przyjmę szeroką definicję modułu jako logicznej jednostki kodu w ramach programu lub biblioteki, posiadającej zarówno interfejs, jak i implementację. Tę prostą koncepcję przedstawia poniższy obrazek.
Tym samym modułem może być np. mikrousługa, pakiet, klasa oraz metoda. Także prywatna metoda jest modułem żyjącym w ramach modułu-klasy. Interfejs modułu obejmuje zarówno formalne aspekty, takie jak schemat API (np. JSON schema) czy sygnatura metody, jak i nieformalne, np. wymagania dotyczące kolejności wywołań metod czy zagadnienia związane z wielowątkowością. Innymi słowy interfejs nieformalny to ten, który musi być wyrażony w formie dokumentacji lub komentarzy.
Ta definicja modułu pozwala wyrazić jego złożoność jako sumę złożoności interfejsu i implementacji. Zaś ta złożoność jest kluczowa przy projektowaniu modułów.
Głębokie i płytkie moduły
Koncepcja głębokich i płytkich modułów stanowi istotny element w projektowaniu oprogramowania. Podział ten odnosi się do sposobu, w jaki moduły są zaprojektowane i jak prezentują swoje interfejsy dla klientów. Głębokie moduły charakteryzują się małym, zwięzłym interfejsem, który ukrywa skomplikowaną implementację za kulisami. Z drugiej strony płytkie moduły mają rozległy interfejs, za którym ukryta jest bardzo prosta implementacja.
Głębokie moduły są często preferowane. Użytkownicy mogą korzystać z rozbudowanych funkcji poprzez prosty interfejs, bez konieczności zrozumienia wszystkich szczegółów wewnętrznych. Natomiast w przypadku płytkich modułów, interfejs może być bardzo rozbudowany, jednak dostarczana funkcjonalność jest względnie prosta. W skrajnym przypadku interfejs może być bardziej złożony od implementacji. Prostym przykładem może być metoda
public createUser() {
return new User();
}
W tym przypadku wywołanie createUser()
jest nawet dłuższe niż new User()
.
Opisane zalety głębokich modułów nad płytkimi mogą prowadzić do wniosku, że powinniśmy optymalizować kod pod kątem projektowania maksymalnie głębokich modułów. Jednakże zwróćmy uwagę na to, jak to wpływa na operację wydzielenia metody (extract method) lub klasy. Wydzielenie metody oznacza, że powstaje nowa metoda. Jest to nowy moduł, który ma swoją implementację i interfejs. Jak pokazano na poniższym obrazku, całkowita implementacja przed i po podzieleniu jest stała: A1 ~= A2 + B
. Natomiast całkowity interfejs jest większy IA < IA + IB
. Oznacza to, że wydzielenie metody niemal zawsze prowadzi do wzrostu całkowitej złożoności projektu. Wyjątkiem może być sytuacja, gdy wydzielenie metody służy redukcji duplikacji kodu, w wyniku czego całkowita implementacja ulega zmniejszeniu.
Wzrost całkowitej złożoności powiązany z rozbijaniem kodu na mniejsze fragmenty prowadzi do absurdalnego wniosku, że najgłębszy i najprostszy projekt uzyskamy, gdy cały kod będzie umieszczony w jednej klasie lub metodzie. Niewątpliwie potrzebna jest praktyka, która może zrównoważyć ten kierunek.
Magiczna liczba siedem
W 1956 roku George A. Miller w swoim artykule pt. „The Magical Number Seven, Plus or Minus Two: Some Limits on Our Capacity for Processing Information” opisał, że ludzki umysł może świadomie przetwarzać około siedmiu elementów informacji naraz.
W kontekście głębokich modułów, magiczna liczba siedem staje się kluczowym kryterium równoważenia ich złożoności, a przy tym głębokości i szerokość. Złożoność ultra głębokich modułów może być tak duża, że ludzki umysł nie może ich objąć. Według George’a A. Millera, ludzki umysł może świadomie przetwarzać około siedmiu elementów informacji naraz. Innymi słowy: interfejsy i implementacje nie powinny być zbyt obszerne ani zbyt skomplikowane, aby programista mógł łatwo przetworzyć informacje, korzystać z modułu oraz go modyfikować w ramach potrzeb.
Doprecyzowując, badania Millera dotyczą pamięci krótkotrwałej i przetwarzania niepowiązanych konceptów. Oznacza to, że im więcej pracujemy z danym kodem, tym lepiej się go uczymy i możemy zacząć używać pamięci długoterminowej, robiąc miejsce w mocno ograniczonej krótkoterminowej. Drugą kwestią jest limit dotyczący niezwiązanych konceptów. Dla przykładu, jeśli mamy w metodzie siedem zmiennych, których nazwy nic nam nie mówią, to dla wielu ludzi będzie to granica efektywnego posługiwania się tymi zmiennymi. Dobre nazewnictwo może przesunąć ten limit w górę bo poprzez nazwę będzie się odwoływać do pamięci długotrwałej oraz powiązań między zmiennymi. Podobnie przestrzeganie standardów, konwencji i wzorców może odciążyć pamięć krótkotrwałą. Niemniej jednak, niezależnie od tego, jak bardzo będziemy próbować naginać badania psychologiczne, ludzkie umysły mają ograniczenia i lepiej nie przekraczać w ramach jednego modułu wspomnianego progu siedmiu konceptów.
Zasada siedmiu konceptów także może prowadzić do absurdalnego ekstremum. Skupiając się na upraszczaniu poszczególnych modułów możemy doprowadzić do powstania milionów mikromodułów, które w pojedynkę są trywialne i bardzo płaskie, jednak ich mnogość wprowadza nadmierną złożoność.
Synergia
Z tego wszystkiego wynika prosty wniosek: nie możemy optymalizować tylko całkowitej złożoności projektu, bo ludzki umysł nie jest w stanie przetwarzać tak złożonych zagadnień. Potrzebny jest balans pomiędzy całkowitą złożonością a lokalną, co w naturalny sposób prowadzić do fraktalnej natury modułów.
Opisane dwie praktyki równoważą się, pozwalając projektować optymalne moduły. Nie za duże – zgodne z zasadą siedmiu konceptów, a jednocześnie wystarczająco głębokie aby były wartościowe i łatwe w użyciu. Zasada ta jest dobrym punktem wyjścia dla projektowania modułów, wokół której można aplikować inne praktyki software design, takie jak spójność poziomu abstrakcji w ramach modułu lub spójność pod kątem zmian.
Fraktale w software design
Fraktale to geometryczne struktury, które są samopodobne na różnych skalach. Rozważmy na chwilę analogię z drzewem narysowanym przez dziecko. Małe dziecko najczęściej rysuje drzewo jako zielony owal na brązowym prostokącie. Jednak w rzeczywistości, struktura drzewa jest znacznie bardziej złożona, duża gałąź składa się z mniejszych, każda mniejsza przypomina tą większą i tak dalej. Nawet w żyłkach liści można dostrzec powtarzającą się strukturę podobną do rozgałęziających się gałęzi drzewa. Ta analogia pokazuje, że pierwsze spojrzenie na moduły w oprogramowaniu może być zbyt uproszczone, niczym obrazek drzewa wykonany przez dziecko.
Analogicznie do drzewa fraktalnego, modularyzacja w oprogramowaniu przebiega fraktalnie. Zaczynamy od Organizacji, którą dzielimy na subdomeny, na przykład „Zarządzanie produktem”, „Zarządzanie zamówieniami” czy „Obsługa klienta”. W ramach tych obszarów, występują kolejne usługi, takie jak „Przetwarzanie płatności” czy „Zarządzanie koszykiem zakupów”. W przypadku „Przetwarzania płatności”, możemy mieć wiele komponentów obsługujących różne metody płatności, śledzenie transakcji czy analitykę i raporty. W zależności od architektury i złożoności, każdy z tych komponentów może być osobną mikrousługą, plikiem JAR lub po prostu pakietem. Możemy tak się zagłębiać, dochodząc do metod wywołujących inne metody itd.
W tym ujęciu, nie możemy mówić o absolutnym rozmiarze idealnego modułu. Zamiast tego, modularyzacja jest wielopoziomowa, na każdym poziomie powinniśmy zachować balans głębokości i złożoności ograniczonej przez możliwości poznawcze ludzkiego mózgu.
Przekaz na dziś
- Głębokie moduły i fraktalna modularyzacja są ważne, ale są tylko punktem wyjścia do dobrego designu.
- Kod powinien być zrozumiały nawet dla zmęczonego programisty.