Dziś przyjrzymy się koncepcji Package-by-Feature (PbF), czyli sposobowi na organizację kodu tak, żeby, w miarę możliwości, wszystkie elementy związane z daną funkcjonalnością aplikacji znajdowały się w jednym pakiecie. Najpierw jednak krótkie przypomnienie jak działają pakiety w Javie. Bez zrozumienia tego mechanizmu będziemy mieli dużo problemów z prawidłowym zastosowaniem PbF.

Pakiety w Javie

Na początek jak definiujemy pakiet.

Listing 1. Definicja pakietu

package pl.koziolekweb.app.api;

Na początku pliku umieszczamy deklarację pakietu za pomocą słówka kluczowego package. Może być poprzedzona komentarzem, ważne, żeby była to pierwsza „żywa” linia kodu w pliku. Następnie podajemy nazwę pakietu. Przyjęło się, że nazwa jest domeną organizacji tworzącej kod, zapisanej od końca. Czyli najpierw domena najwyższego rzędu pl, potem nazwa organizacji koziolekweb i następnie już co tam potrzeba. Nazwa aplikacji, nazwa modułu, cokolwiek co nam przyjdzie do głowy.

Widoczność zawartości pakietów

Na początek klasy i ich metody. Jeżeli klasa nie ma zadeklarowanego modyfikatora widoczności, to jest widoczna tylko w pakiecie, w którym została zdefiniowana. Podobnie metody w klasie. Jeżeli nie mają żadnego modyfikatora, to są widoczne tylko dla elementów zdefiniowanych w tym samym pakiecie.

Trochę inaczej sprawa z interfejsami. Zasada dla interfejsu pozostaje bez zmian. Jeżeli nie ma modyfikatora, to jest widoczny tylko w pakiecie. Inaczej sprawa wygląda, gdy mówimy o metodach zdefiniowanych w interfejsie. Jeżeli nie mają żadnego modyfikatora, to są widoczne tak, jakby miały modyfikator public. Inaczej mówiąc, są widoczne dla wszystkich.

W przypadku widoczności obowiązuje swoista hierarchia, czyli publiczna metoda, żeby była widoczna dla wszystkich, musi być:

  1. Zdefiniowana w klasie publicznej, lub
  2. Zdefiniowana w interfejsie i zaimplementowana w danej klasie.

Reguły są sprawdzane na etapie kompilacji, ale nie w czasie wykonania. Z tego wynika jeszcze jedna zasada.

Jeżeli klasa nie jest publiczna, ma widoczność pakietową, to referencja do niej może być przekazana tylko, jeżeli typ referencji jest w hierarchii dziedziczenia danej klasy. Prościej, klasa niepubliczna może być używana wszędzie, o ile reprezentująca zmienna będzie typu widocznego w danym miejscu i będzie to typ, który jest nadklasą naszej klasy, lub nasza klasa implementuje dany interfejs.

Ostatnią regułą, którą musimy pamiętać, to brak hierarchiczności pakietów. Oznacza to, że elementy pakietu pl.koziolekweb.app.api nie widzą niepublicznych elementów z pl.koziolekweb.app i vice versa. Szczerze, to właśnie ta reguła powoduje najwięcej fakapów i jest źródłem problemów z PbF.

Jak możemy zorganizować kod

Dawno temu jak zaczynałem pracę, to panowała zasada, że do pakietu pakujemy rzeczy, które mają taką samą funkcję w systemie. Kontrolery lądują w controllers, serwisy w services, a przydasie w utils. Podejście to nazywano Package-by-Layer (PbL) i miało bardzo dużo sensu. Pamiętajmy, że adnotacje pojawiły się w dopiero w Javie 5. Wcześniej nie było prostej metody na odszukanie wszystkich klas, które maja podobną rolę w systemie np. wszystkich testów. Stosowano haki w postaci konwencji nazewniczej. Świetnie widać to w przypadku JUnita, który w wersji 3 uruchamiał jako testy wszystkie klasy, których nazwa kończyła się na Test, a metody setup i tearDown miały specjalne znaczenie.

Nieprzekonany? Ok, to pomyśl, że musisz napisać aspekt, który otoczy transakcją wszystkie metody we wszystkich repozytoriach. Dziś napiszesz coś w stylu within(@pl.koziolekweb.app.Transactional *) && execution(public * *(..)) i z głowy. Każda publiczna metoda z adnotacją zostanie otoczona odpowiednim kodem. A teraz BEZ użycia adnotacji. Już widzisz problem? Możesz użyć konwencji nazewniczej i pilnować czegoś, co jest nieweryfikowalne w czasie kompilacji. Możesz też upchnąć wszystko w jednym pakiecie i powiedzieć, że wszystko w danym pakiecie ma być otoczone transakcją.

Jednak język zmienia się i wraz z pojawieniem się adnotacji, mogliśmy porzucić PbL stosując je tylko w czasie negocjacji z menadżerami średniego szczebla – warstwa ciał, warstwa wapna, warstwa ziemi. Jednak tak się nie stało. Siła przyzwyczajenia zrobiła swoje i nadal w wielu projektach ludzie twardo upychają klasy w pakietach, a przydzielając je do poszczególnych pakietów patrzą tylko na to w jakiej warstwie leży dana klasa.

Dlaczego należy zmienić podejście

PbL ma pewne zalety. Jeżeli mamy stosunkowo niewielki projekt, to znacznie łatwiej jest w niego wejść osobie, która ma wykonać pracę czysto techniczną. Przykładowo masz poprawić działanie zapytań, to siedzisz w jednym pakiecie i wio. Masz migrować jakąś bibliotekę do nowszej wersji. Najprawdopodobniej będziesz dotykał konkretnej warstwy.

Problem zaczyna się, gdy projekt rośnie. Trzy kontrolery na krzyż zamieniają się w trzydzieści. Z kilku serwisów robi się kilkadziesiąt. W dodatku zaczynają się dziać bardzo złe rzeczy w kodzie. Zaczyna się „pożyczanie” funkcjonalności pomiędzy klasami. Powstaje hierarchia dziedziczenia oparta o współdzielenie pojedynczych metod. W końcu osoba, która ma poznać kod, otrzymuje bardzo dużo niepołączonych ze sobą informacji. Taki kod ciężko też zrefaktoryzować, bo połączenia pomiędzy elementami są nieoczywiste. W dodatku próba wydzielenia np. osobnego µSerwisu kończy się grzebaniem w wielu miejscach.

Spójność i sprzężenie

Zależności pomiędzy naszymi bytami możemy opisać za pomocą dwóch pojęć. Spójności i sprzężenia.

W typowej aplikacji, która wykorzystuje PbL mamy do czynienia z niską spójnością, wysokim sprzężeniem. Popatrzmy na poniższy rysunek.

┌──────────────────────────────────┐     ┌──────────────────────────────┐
│  pl.koziolekweb.app.controllers  │     │  pl.koziolekweb.app.services │
│                                  │     │                              │
│             ┌────┐               │     │             ┌────┐           │
│     ┌───────► C1 ├───────────────┼─────┼──┬──────────► S1 │           │
│     │       └─┬──┘               │     │  │          └────┘           │
│     │         │                  │     │  ▼                           │
│     │       ┌─▼──┐               │     │  │          ┌────┐           │
│     ├───────► C2 ├───────────────┼─────┼──┴──────────► S2 │           │
│     │       └────┘               │     │             └────┘           │
│     │                            │     │                              │
│     │       ┌────┐               │     │             ┌────┐           │
│     ├───────► C3 ├───────────────┼─────┼─────────────► S3 │           │
│     │       └────┘               │     │             └─┬──┘           │
│     │                            │     │               │              │
│     │                            │     └───────────────┼──────────────┘
│  ┌──┴────┐                       │                     │
│  │       │                       │                     │
│  │ Utils ◄───────────────────────┼─────────────────────┘
│  │       │                       │
│  └───────┘                       │
│                                  │
└──────────────────────────────────┘

Pakiet kontrolerów ma wiele połączeń do pakietu z serwisami. W dodatku niektóre z nich są dwukierunkowe, to znaczy serwisy wykorzystują elementy z kontrolerów. W takim układzie jedyne co nam pozostaje to próba wyrzucenia klasy Utils do osobnego pakietu, żeby zlikwidować tę cykliczną zależność. Nie rozwiązujemy jednak głównego problemu, jakim jest wiele interakcji pomiędzy pakietami. Pułapką może okazać się też, klasa Utils, która po „awansie” do osobnego pakietu zacznie zbierać funkcjonalności z różnych części aplikacji. Jakakolwiek zmiana w tej klasie może spowodować wybuch w nieoczekiwanym miejscu w kodzie. Spróbujmy innego podejścia. Pogrupujmy klasy według funkcjonalności „biznesowej”.

┌───────────────────────┐  ┌───────────────────────┐
│pl.koziolekweb.app.f1  │  │pl.koziolekweb.app.f2  │
│                       │  │                       │
│ ┌────┐     ┌────┐     │  │ ┌────┐                │
│ │    ├─────►    │     │  │ │    │                │
│ │ C1 ├──┼──┐ C2 │     │  │ │ C3 ├────┐           │
│ │    │  │  │    │     │  │ │    │    │           │
│ └─┬──┤  │  ├─┬──┘     │  │ └─┬──┘    │           │
│   │──┼──┼──┼─│        │  │   │       │           │
│ ┌─▼──┤  │  ├─▼──┐     │  │ ┌─▼──┐    │           │
│ │    │  │  │    │     │  │ │    │    │           │
│ │ S1 │  │  │ S2 │     │ ┌┼─┤ S3 │    │           │
│ │    │  │  │    │     │ ││ │    │    │           │
│ └────┘  │  └────┘     │ ││ └──┬─┘    │           │
│         │             │ ││    │      │           │
│ ┌───────▼──────┐      │ ││ ┌──▼──────▼────┐      │
│ │              │      │ ││ │              │      │
│ │ InternalUtils│      │ ││ │ InternalUtils│      │
│ │              │      │ ││ │              │      │
│ └────┬─────────┘      │ ││ └──────────────┘      │
│      │                │ ││                       │
└──────┼────────────────┘ │└───────────────────────┘
       │               ┌──┘
       │               │
    ┌──┼───────────────┼──────────┐
    │pl│koziolekweb.app│utils     │
    │  │               │          │
    │ ┌▼────────┐ ┌────▼───────┐  │
    │ │IntUtils │◄│StringUtils │  │
    │ └─────────┘ └────────────┘  │
    │                             │
    └─────────────────────────────┘

I teraz widać co się mniej więcej dzieje. Pakiety są spójne, czyli dużo interakcji zachodzi wewnątrz nich, a jednocześnie nie są sprzężone ponad miarę z innymi pakietami. Co prawda pakiet pl.koziolekweb.app.f1 jest mocno „zagmatwany”, ale ograniczyliśmy ten chlew do jednego miejsca. Praca z tym pakietem będzie prosta, ponieważ wyizolowaliśmy problem. Możemy dowolnie go zmienić i potencjalny merge kodu nie będzie powodować konfliktów.

Tyle teorii. Na praktykę będzie czas w 2 części.