Dzisiaj wracamy po raz kolejny do tematu pilnowania w ryzach modułowości naszej aplikacji. Mówiliśmy już o korzystaniu z modułów narzędzi budowania oraz pakietów Javy. Jednak co w przypadku jeśli żadne z tych rozwiązań nie przypadło Ci do gustu? Czy istnieje inne wyjście? Gdyby takiego nie było to ten wpis by nie powstał, prawda?

Kolejnym pomysłem może być skorzystanie z narzędzi takich jak ArchUnit. Dzięki nim jesteśmy w stanie wymusić na deweloperach, aby implementowali moduły w odgórnie założony sposób. W jaki sposób? Przekonamy się o tym za chwilę.

Non business context

Tym razem nie narzucimy sobie żadnego kontekstu biznesowego. Po prostu jeden moduł będzie miał nazwę first, a drugi second. Moduł second będzie chciał skorzystać z klas, które zostały zaimplementowane w module first.

Podział na moduły w kodzie Podział na moduły w kodzie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package pl.cezarysanecki.archunitmodules.second;

import pl.cezarysanecki.archunitmodules.first.api.FirstDto;
import pl.cezarysanecki.archunitmodules.first.internal.FirstInternalService;

public class SecondFacade {

  public void a() {
    FirstDto firstDto = new FirstDto();
  }

  public void b() {
    FirstInternalService firstInternalService = new FirstInternalService();
  }

}

Nic skomplikowanego. Klasa SecondFacade ma dwie metody. Co jest istotne, w jednej z nich tworzony jest obiekt klasy znajdującej się w podpakiecie api modułu first. W drugiej natomiast kreowany jest obiekt klasy z wewnętrznego pakietu internal. Jest to istotna informacja, ponieważ nie chcemy, aby inny moduł korzystał z klas znajdujących się w wewnętrznych pakietach innego modułu (poza jednym wyjątkiem, ale o tym za chwilę).

Dlatego wprowadziliśmy hierarchię pakietów wewnątrz modułu, aby był w nim porządek. Nie chcemy natomiast, aby przez Javę i jej braki ktoś teraz korzystał z publicznie dostępnych wewnętrznych klas. Oczywiście mogą zdarzyć się wyjątki. Niektóry pakiety takie jak api mogą nam służyć do wkładania tam klas reprezentujących DTO, które chcemy przekazywać na zewnątrz. Jednak takie jak internal chcielibyśmy, aby były tylko dostępne na potrzeby wybranego modułu. Jak coś takiego pogodzić? Właśnie dzięki ArchUnit.

ArchUnit to the rescue!

Biblioteka ArchUnit pozwala nam na wprowadzenie do naszego kodu porządku. W jaki sposób? Dzięki regułom, które możemy stworzyć sami poprzez wykorzystanie dostępnych klocków w postaci FluentApi. Załóżmy, że mamy sytuację taką jak została opisana wcześniej. Żeby ochronić to co sobie założyliśmy możemy napisać taki o to kawałek kodu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RunWith(ArchUnitRunner.class)
@AnalyzeClasses(packages = "pl.cezarysanecki.archunitmodules")
public class ArchModulesTest {

  @ArchTest
  public static final ArchRule ACCESS_TO_FIRST_INTERNAL_PACKAGES_CLASSES =
      noClasses()
          .that()
          .resideOutsideOfPackages("..first..")
          .should()
          .dependOnClassesThat(
              not(resideInAPackage("..first.api")).and(
                  resideInAPackage("..first.*")
              )
          )
          .as("cannot access internal packages of first package (except api)")
          .because("these are internal details of this package");

}

Prawda, że wygląda banalnie i przyjemnie się go czyta? Po prostu mówimy, że żadna klasa, która znajduje się poza pakietem first, nie może korzystać z wewnętrznych podpakietów pakietu first poza first.api. Po adnotacji @ArchTest można domyślić się, że ta reguła ma formę testu. Co to oznacza? Niestety to rozwiązanie nie chroni nas na poziomie kompilacji. Żeby dowiedzieć się czy nie przekroczyliśmy jakiejś granicy musimy uruchomić test. Wiąże się z to z tym, że jeśli nie znaliśmy jakiejś reguły i nie implementowaliśmy rozwiązania pod nią to po prostu nam ona nie przejdzie. Dlatego warto na początku uświadomić nowych członków zespołu o regułach architektonicznych, aby nie dopuścić do takiej sytuacji. Bo jeśli niestety ona wystąpi to są dwa wyjścia. Albo trzeba poprawić całe rozwiązanie albo… dać @Ignore czy też zakomentować test. Słabo, co nie?

Wracając na chwilę do powyższej reguły. Ma ona jedną wadę w postaci takiej, że będziemy musieli ją kopiować per nowy moduł. Jednak na potrzeby tego artykułu nie zastanawiałem się czy można to zrobić lepiej. Jeśli masz pomysł jak można ją uogólnić to daj znać w komentarzu. Dobra, zobaczmy co się stanie, gdy uruchomimy ten test.

1
2
3
 
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'cannot access internal packages of first package (except api), because this is internal detail of this package' was violated (1 times):
Method <pl.cezarysanecki.archunitmodules.second.SecondFacade.b()> calls constructor <pl.cezarysanecki.archunitmodules.first.internal.FirstInternalService.<init>()> in (SecondFacade.java:13)

Dostajemy precyzyjną informację o tym w jaki sposób nasza reguła została złamana:

  • w której klasie doszło do naruszenia
  • w jakiej jej cześci doszło do naruszenia np. metodzie
  • jaka klasa jest niedozwolona do korzystania

Mam nadzieję, że ten prosty przykład uruchomił w Twojej głowie mnóstwo scenariuszy w jakich ArchUnit by Ci się przydał. Kosztem wad, które wymieniłem wcześniej, jest to naprawdę elastyczna biblioteka.

Bonus: Dogadajmy się

Na koniec chciałbym rzucić kontrowersyjny sposób na trzymanie porządku w kodzie naszej aplikacji. Wymaga on naprawdę ogromnej dyscypliny i doświadczenia członków zespołu. Po prostu możemy się dogadać jak nasza aplikacja ma wyglądać. Nie będziemy tworzyć czy używać żadnych narzędzi, które będą nas pilnowały. Wystarczy, że rozpropagujemy wiedzę/reguły pomiędzy członków zespołu. I tyle! Ufamy sobie i najwyżej wyłapujemy ewentualne błędy na Code Review. Albo nawet możemy i ten element odpuścić o ile naprawdę wierzymy nawzajem w swoje umiejętności.

Problem co do tego podejścia jaki przychodzi mi do głowy to możliwy brak jakiejkolwiek dokumentacji dogadanych reguł. Ciężko może nam być wprowadzać nowe osoby do projektu. Do tego trzeba mieć naprawdę dobrych ludzi w zespole. A jak wiemy, ciężko o tak solidny zespół w każdym aspekcie. Plusem tego rozwiązania na pewno jest brak konfiguracji dodatkowych narzędzi oraz swoboda w podejmowaniu decyzji przez każdego z członków zespołu. A Ty jak uważasz, ma to ręce i nogi?

Podsumowanie

Architektura aplikacji to naprawdę bardzo ciekawy temat, o którym można rozmawiać godzinami. Możemy się zastanawiać na tym w jaki sposób ją zaprojektować. Ale nawet najlepszy plan jest nic nie wart, gdy się go nie egzekwuje. Z tego powodu warto się wspomagać dodatkowymi narzędzami, które pilnują tego co wcześniej sobie ustaliliśmy. Chociaż nie zawsze jest to konieczne. Na koniec chciałbym dodać, że dobra architektura aplikacji to taka, którą nowi członkowie w zespole są w stanie szybko zrozumieć. Nie ma więc co jej nadmiernie komplikować.