Ile razy spotkałeś bądź spotkałaś się z problemem dotyczącym dotrzymania wcześniej ustalonych konwencji architektonicznych w kodzie? Zakładam, że na początku wszystko wyglądało pięknie. Zaprojektowaliście moduły, które miały być “samotnymi wyspami” z niewielkimi punktami styku w postaci publicznego API. Jednak do tej całej układanki wkradło się prawidziwe, projektowe życie. Nim człowiek się obejrzał, a wszystko wróciło do “normy”, czyli dawno wygrzanego schematu działania.

Chciałbym Ci pokazać, że ta sytuacja wcale nie musi przybrać takiego biegu spraw. Mam na to kilka rozwiązań i dzisiaj przedstawię Ci jeden z nich. Oczywiście nie jest to żaden silver bullet, ale powinien zadziałać w kilku specyficznych przypadkach. Co właściwie mam na myśli? Moduły. A właściwie moduły narzędzi budowania takich jak Maven albo Gradle. Sprawdźmy jak to wygląda w praktyce.

Context is the king!

Wyobraźmy sobie sytuację, gdzie mamy aplikację, która musi komunikować się z jakimś zewnętrznym serwisem. My mamy swój model, oni mają swój. Zależy nam na tym, aby obcy model nie przenikał do naszej, czystej jak łza, domeny.

Nie chcemy takiego powiązania Nie chcemy takiego powiązania

Chcemy postawić wyraźną granicę. Taką której nikt nie przekroczy w żadną ze stron. Jednak zanim zastosujemy sztuczkę z modułami Maven/Gradle, zastanówmy się jakie mamy możliwości.

Aktualne możliwości

Wydaje mi się, że nie ma za dużo opcji. W klasycznym podejściu moglibyśmy po prostu stworzyć mapper po jednej albo po drugiej stronie. Jest to jednak subiektywna opinia gdzie lepiej go umieścić.

Mapper po stronie modułu domeny albo zewnętrznego serwisu Mapper po stronie modułu domeny albo zewnętrznego serwisu

Zaletą tego podejścia jest prostota. Nie ma potrzeby tworzenia żadnych dodatkowych mechanizmów. Po prostu implementujemy mapper i tyle. Implikuje to wprawdzie pewien problem. Musimy się mocno pilnować, aby ktoś, umyślnie lub przez przypadek, nie wykorzystał obcego modelu bez skorzystania z mappera. Możemy oczywiście wyłapać taki błąd poprzez skorzystanie np. z Code Review, co może być wystarczające. Ale nie jest to rozwiązanie w 100% skuteczne. Jeśli się boimy o degradację domeny to warto pilnować rozwiązania już na poziomie kompilacji.

Modules to the rescue!

W tym momencie warto zaprezentować podejście z modułami znanymi z Maven oraz Gradle. Dzięki nim raczej trudno popełnić błąd w kodzie. Zacznimy na start od grafiki reprezentującej to rozwiązanie.

Dodatkowy komponent pozwalający na trzymanie porządku w kodzie Dodatkowy komponent pozwalający na trzymanie porządku w kodzie

Wydzieliliśmy osobny moduł, który zawiera porty oraz kontrakty. Domena z niego korzysta, aby używać dostępnych tam interfejsów. Natomiast moduł od zewnętrznego serwisu dostarcza adapter dla tego interfejsu. Wszyscy są szczęśliwi. Domena nic nie wie o konkretnej implementacji. Nie może też korzystać z modelu zewnętrznego serwisu. Zewnętrzny serwis też ma zakaz używania modelu domeny. Żaden programista nie popełni błędu, chyba że jawnie doda zależność do pliku konfiguracyjnego narzędzia budowania. Ale to raczej wyłapiemy bez najmniejszego błędu, prędzej czy później.

Przykład w kodzie

Gadanie gadaniem, ale nic tak nie wyjaśni lepiej koncepcji jak sam kod.

Struktura przykładowego rozwiązania Struktura przykładowego rozwiązania

Tak jak wcześniej wspomniałem. Mamy teraz do dyspozycji trzy moduły (dalej warto pamiętać, że to słowo jest używane w kontekście narzędzi budowania). Jak sobie zajrzymy do pom.xml (zdecydowałem się w przykładzie na Maven) domeny i zewnętrznego serwisu to zobaczymy tylko jedną zależność.

1
2
3
4
5
6
7
<dependencies>
    <dependency>
        <groupId>pl.cezarysanecki</groupId>
        <artifactId>api</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>	

Natomiast API nie ma żadnych zależności. Z tego powodu nie możemy w nim skorzystać z żadnych klas domenowych czy integracyjnych. Domena też nie może sięgnąć do integracji i vice versa. Jesteśmy mocno chronieni przed popełnieniem błędu.

Spójrzmy jeszcze na przykład jednej z klas domenowych.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
class UserService {

  private final UserPort userPort;

  UserService(final UserPort userPort) {
    this.userPort = userPort;
  }

  public void sampleMethod() {
    Result result = userPort.invoke();
    System.out.println(result);
  }

}

Jak widać powyżej. W klasie domenowej korzystamy tylko z interfejsu i dołączonego do niego kontraktu. Nie ma żadnej mowy o konkretnej implementacji. Wygląda na to, że nasze rozwiązanie działa!

Podsumowanie

Jak zawsze, zachęcam do rozważnego korzystania z narzędzi. Powyższe rozwiązanie jest naprawdę fajne, ale wymaga dodatkowego narzutu implementacyjnego. Nie zawsze chcemy ponieść taki koszt czy też nie ma to uzasadanienia biznesowego. Niemniej jednak w projekcie, gdzie mamy mało doświadczonych programistów, może ono być na wagę złota.

Zachęcam Cię do samodzielnego spróbowania i pobawienia się tym sposobem. Czy widzisz może jakieś inne wady albo zalety, o których tutaj nie wspomniałem? Daj znać w komentarzu!