Ostatnio zobaczyliśmy w jaki sposób możemy chronić granice modułów biznesowych w monolicie przy pomocy modułów dostępnych w narzędziach budowania takich jak Maven czy Gradle. Wtedy wspomniałem, że takie rozwiązanie ma dosyć duży narzut implementacyjny. Można więc zadać sobie pytanie - czy istnieja jakaś alternatywa? A i owszem. Sprawdźmy jak możemy chronić nasze moduły przy wykorzystaniu od dawna obecnego w Javie, ale zapomnianego, package scope.

Switch the context

Żeby nie robić powtórki z rozrywki, spróbujmy narzucić inny kontekst. Załóżmy, że w aplikacji mamy dwa moduły w warstwie domenowej. Niech będą to warehouse i sale. Nasz przypadek biznesowy będzie bardzo trywialny. Po prostu, nie możemy sprzedać czegoś jeśli nie ma tego na magazynie. I tyle. Decyzja, aby nie komplikować przypadku biznesowego pozoli nam bardziej skupić na głównej cześci artykułu.

Wykorzystując moduły Maven/Gradle musieliśmy się trochę napracować, aby wszystko dobrze ustrukturyzować. Niektóre osoby miałyby pewnie subiektywne odczucie, przy większej liczbie modułów, że “jakoś słabo im to wygląda”. Tych modułów naprawdę mogłaby być ogromna ilość… Co więc możemy zrobić mniejszym sumptem i osiągnąć podobny efekt? Skorzystać z pakietów w Javie.

Pakietowanie w Javie

No właśnie… Pakiety! A dokładniej dostęp pakietowy. Jestem już trochę w branży. Każda osoba z ekosystemu Javy jaką spotkałem, potrafiłaby na rozmowie rekturacyjnej bez zastanowienia wymienić obecne modyfikatory dostępu w Javie. Gorzej ta statystyka wygląda w praktyce. Ciągle się spotykam, że by default każda nowa klasa jest publiczna i najcześciej już taka zostaje do końca. Nie ma chwili refleksji, że ta klasa nie musi być widocza na zewnątrz. A brak takiego podejścia w wielu przypadkach prowadzi do chaosu w kodzie. Ale dobra, znowu odbiegam od tematu.

Zobaczmy możliwą implementację powyższego wymagania wykorzystując w tym celu package-scope. Tak jak wspomniałem, dzięki niemu możemy chronić wewnątrzną implementację każdego z modułów.

Przykład modułu w formie pakietu Przykład modułu w formie pakietu

Tylko jedna klasa jest publiczna. Ta która reprezentuje publiczne API naszego modułu. To wystarczy, naprawdę! Kontroler, będący w podpakiecie web, czy konfiguracja Springa mogą też mieć dostęp package-private. Nikt nam w naszym pakiecie nie będzie bruździł. Całość została schowana przed światem zewnętrznym. Prawda, że piękne? Zobaczymy jak wygląda klasa reprezentująca fasadę.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package pl.cezarynsanecki.packagemodule.sale;

import pl.cezarynsanecki.packagemodule.kernel.ProductId;
import pl.cezarynsanecki.packagemodule.sale.api.CannotSaleException;
import pl.cezarynsanecki.packagemodule.warehouse.WarehouseFacade;

public class SaleFacade {

  private final WarehouseFacade warehouseFacade;

  SaleFacade(final WarehouseFacade warehouseFacade) {
    this.warehouseFacade = warehouseFacade;
  }

  public void sale(ProductId productId) {
    boolean available = warehouseFacade.isAvailable(productId);
    if (!available) {
      throw new CannotSaleException(productId);
    }
    // some logic
  }

}

Mamy tutaj scenariusz biznesowy, który został opisany w metodzie sale. Nie dzieje się tutaj nic specjalnie ciekawego. Jedyne na co warto zwrócić uwagę to, że konstruktor też ma dostęp pakietowy. Oznacza to, że tylko ten pakiet może stworzyć instancję tej klasy, nikt więcej.

Pakiet warehouse nie ma w ogóle dostępu do klas takich jak SaleRepository czy Sale. On nawet nie wie i nie chce wiedzieć o ich istnieniu. Jedyne co może mu się kiedyś przydać to fasada z modułu sprzedaży. Jak widać to podejście nie jest skomplikowane. Wymaga jedynie dyscypliny i zmiany sposobu myślenia o tworzeniu oprogramowania. Dzięki niemu osiągamy enkapsulacje na wyższym poziomie.

Trzeźwe spojrzenie na problem

Moim zdaniem powyższe rozwiązanie ma naprawdę ogromną ilość zalet. Jest proste, szybkie i chroni nas przed bałaganem, ale… No właśnie. Łatwo popełnić w nim błąd. Raz gdzieś się zapomni usunąć słowo kluczowe public i pierwsza szyba w budynku może polecieć. Istnieje też ogromna pokusa zwiększenia dostępu danej klasy, wiecie “tak raz, bo trzeba szybko dostarczyć feature”. O wiele łatwiej w tym podejściu jest złamać to co zostało ustalone w kontekście modułowości. Maven i Gradle chronią nas przed czymś takim o niebo lepiej.

Komuś również może przekszadzać to, że wszystko co należy do modułu musimy umieszczać w jednym pakiecie, który puchnie. Nie możemy zrobić sobie hierarchii podpakietów, która nam pomoże w porządkach. Znaczy możemy o ile to klasy w tych podpakietach korzystają z fasady znajdującej się w głównym pakiecie modułu. Jednak na pewno nie na tym nam zależy. Takie podejście wymaga od nas tworzenie kolejnych publicznych klas co może znowu doprowadzić do wcześniej wspomnianego chaosu. Jak żyć? Java nam w tym nie pomoże. Jest to ograniczenie nad którym ubolewam. Oczywiście są ciekawe narzędzia jak Spring Modulith, o których pisałem wcześniej. Ale o nich porozmawiamy sobie innym razem, również w kontekście chronienia modułów.

Z tego powodu w głównej mierze musimy znacząco polegać na sumienności programistów oraz nad mechanizmem Code Review. Jeśli przepuścimy tam chociaż jednego robaka może nam się wylęc z tego porządna brygada.

Podsumowanie

Jak zawsze, narzędzie należy dobierać do rozwiązywanego problemu. Package-private w kontekście modułów jest naprawdę tanie i szybkie. Na pewno warto z niego korzystać. Jeśli będzie uzasadnienie biznesowe, aby korzystać z modułów narzędzi budowania to wtedy warto pójść w tym kierunku. W innym przypadku zostańmy przy tym rozwiązaniu i pilnujmy się nawzajem na Code Review. Nie do końca mamy ochronę na poziomie kompilacji, a dopiero daleko, daleko później. Ale, coś za coś. W każdej sytuacji należy po prostu kierować się pragmatyzmem.