Anti-corruption Layer to poteżna rzecz! Potrafi trzymać nasz kod w czystości. Niczym fosa broniąca zamku przed najeźdźą czy też wał przeciwpowodziowy chroniący mieszkańców przed podniesionym poziomem rzeki. Tak samo ta warstwa “antyskażeniowa” trzyma w ryzach model złych serwisów, obecnych w zewnętrznym świecie, z dala od naszego, czystego jak łza kodu. Dlaczego o tym piszę? Bo nie wyobrażam sobie pisania kodu bez wykorzystania tego wzorca, gdy mam do czynienia z powyższą sytuacją. I oczywiście chcę się z Tobą podzielić moim doświadczeniem z placu boju, gdzie najlepiej sprawdziło mi się połączenie Anti-corruption Layer z architekturą heksagonalną.

Alt

O to cały sekret. Nasza domena robi swoje jakże ważne, biznesowe rzeczy. Jednak najpewniej nie działa ona w próżni i musi kontaktować się ze światem zewnętrznym w celu wymiany informacji. Najlepiej jak do tego celu używa portów w postaci interfejsów. Tworząc port powininniśmy mieć tylko jedną rzecz w głowie - zaprojektowanie odpowiedniego kontraktu niezbędnego domenie. Możemy zastanowić się jakie dane, których nie mamy w aplikacji, są nam niezbędne, aby pójść dalej z procesem biznesowym.

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface InsuredFindPort {

  ExternalInsured findBy(@NotNull ExternalId externalId);

  record ExternalInsured(
    ExternalId externalId,
    CompanyName companyName,
    Address address,
    PhoneNumber phoneNumber
  ) {
  }

}

Powyższy przykład pokazuje nam jak mogłoby to wyglądać w przypadku poszukiwania ubezpieczonego w zewnętrznym serwisie. W kontrakcie zawarte są tylko te dane ubezpieczonego, które są nam niezbędne dla naszego biznesu. Oczywiście API zewnętrznego serwisu może być o wiele bogatsze w informace, które nie są istotne z naszego punktu widzenia. Może też nastąpić inna sytuacja w postaci potrzeby połączenia dwóch pól, aby uzyskać jedną, ważną dla nas informację. Domenę nie powinny interesować te rzeczy. To nie jest jej odpowiedzialność. Pytanie więc brzmi - na czyje barki spada ta praca? Oczywiście na adapter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
class XyzServiceInsuredAdapter implements, InsuredFindPort {

  // fields

  @Override
  public ExternalInsured findBy(@NotNull ExternalId externalId) {
  XyzCustomerSearchCriteria searchCriteria = XyzCustomerSearchCriteria.of(externalId);
  return xyzFacade.searchCustomer(searchCriteria) // 1
    .getCustomers()
    .stream()
    .findFirst()
    .map(XyzServiceInsuredAdapter::map) // 2
    .orElseThrow(() -> new IllegalArgumentException("cannot find customer for external id " + externalId));
  }

  // mapper methods

}

Dla mnie to adapter jest odpowiedzialny za komunikację z zewnętrznym serwisem oraz zmapowanie otrzymanych danych na kontrakt, który przygotowała dla nas domena. W powyższym przypadku zewnętrzny serwis pozwala na wyszukiwanie ubezpieczonego po id, ale zamiast zwracać jeden rekord to zwraca kolekcję. Nie chcielibyśmy, aby ten kontrakt wpłynął na wygląd domeny. Stąd taki kod, a nie inny. To adapter dostosowuje się do kontraktu zawartego w porcie, a nie na odwrót. Dlatego z listy pobiera pierwszego klienta i w razie jego braku rzuca wyjątek. No właśnie… klienta! Jak widzisz mamy tutaj zmianę nazewnictwa. Zewnętrzny serwis nie wie czym jest ubezpieczony. Dla niego to po prostu klient. To ogromna zaleta tego podejścia. Pozwala nam się wyzbyć niechcianego słownictwa. Nie zawsze chodzi o sam kod, ważna jest także nomenklatura.

Robiąc pętlę do początku artykułu. Dla mnie Anti-corruption Layer i adapter świetnie się razem uzupełniają. To właśnie adapter może być faktyczną implementacją tego wzorca. To on staje się strażnikiem naszej domeny. Zna język w jakim porozumiewa się zewnętrzny serwis i tłumaczy go na coś zrozumiałego dla naszej aplikacji.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class XyzFacade {

  public XyzCustomersResponse searchCustomer(
      XyzCustomerSearchCriteria searchCriteria
  ) {
    XyzCustomersResponse response = xyzProvidingCustomerService.searchCustomer(
          searchCriteria);
          
    // loggers
    return response;
  }

}

Na koniec jeszcze chciałem pokazać jak może wyglądać klient przygotowany do rozmowy z zewnętrznym serwisem. Korzystam do tego najczęściej ze wzorca fasady. Klasy transferujące dane, obecne w API fasady, są dla mnie częścią infrastrukturalną. Z tego powodu moga one być “zabrudzone” wszelkimi adnotacjami pomagającymi nam mapować JSON czy XML. Po prostu w tym miejscu tworzę klienta potrafiącego komunikować się z zewnętrznym serwisem. Jest to jego jedyna odpowiedzialność. Można nazwać go więc klockiem, budulcem mającym swoje miejsce w szeregu.

I to tyle na dzisiaj. Mam nadzieję, że ten opis oraz przykład pomoże Ci tworzyć lepszy soft. Na koniec uwaga, jak dla mnie programista musi być trochę zapominalski. Chodzi tutaj konkretnie o to, że projektując port powinien w ogóle zapomnieć o tym, że będzie integrował się z jakimś konkretnym serwisem. Musi się on skupić tylko i wyłącznie na tym co jest niezbędne jego domenie. Natomiast implementując adapter powinien zapomnieć o całej reszcie, która nie dotyczy części integracyjnej. Taka oto rada ode mnie.