Cześć, będzie trochę długo.
Oglądam sobie ostatnio sporo nagrań z różnych konferencji i temat, który się często przewija to architektura heksagonalna.
Jako, że wszyscy zachwalają, że to takie och i ach, i w ogóle najlepsze co może być, to chciałem sobie zobaczyć jakiś przykład jak to wygląda. I szczerze mówiąc, to trochę jestem zawiedziony, bo żaden z projektów, które widziałem mnie nie przekonał. Powiem więcej, jak dla mnie to mogłyby konkurować z FizzBuzzEnterpriseEdition o nagrodę za najbardziej rozdmuchany kod. Potem zrobiłem najgłupszą rzecz jaką można zrobić, czyli zacząłem używać mózgu i chyba coś sobie uświadomiłem.
Załóżmy, że klepiemy sobie wesoło klasę i wychodzi nam coś takiego:
class ABClass {
doA(){/*kilometry kodu */}
doB(){/*kilometry kodu */}
doAB(){/*kilometry kodu */}
}
Potem zauważamy, że ta nasza klasa to chyba robi trochę za dużo i generalnie jest taka jakaś za długa. Dzielimy ją więc na części.
class A {
doA(){/* kilometry kodu */}
}
class B {
doB(){/* kilometry kodu */}
}
class ABClass {
ABClass(A a, B b) {
this.a = a; this.b = b;
}
doA(){ a.doA(); }
doB(){ b.doB(); }
doAB(){ a.doA(); b.doB2(); }
}
Nie zrobiłem tu nic nadzwyczajnego wydzieliłem osobne funkcjonalności do osobnych klas. Potem przekazuję je sobie w konstruktorze. Nie wiem, ja tak robię i jest to dla mnie naturalne. Zwykła zasada pojedynczej odpowiedzialności. Ale czy na pewno? Czy to nie jest właśnie ta słynna "architektura portów i adapterów"?.
Ja patrzę na to tak: mam ABClass, która ma 2 porty, konkretnie klasy A i B, do które mogę sobie wymieniać, jak mi się żywnie podoba. Jeżeli przykładowo A przyjmuje w konstruktorze jednego inta, to mogę sobie tam podać 1, 3, czy 5 milionów. Złośliwi powiedzą, "ale teraz to tylko tworzysz różne instancje tej samej klasy. Nie możesz wymienić samej klasy A na taką z inną implementacją", a ja bym powiedział "pacz na to:"
interface IA {
doA();
}
class A implements IA{
doA();
}
class A2 implements IA {
doA();
}
Jeżeli z jakiegoś powodu okazałoby się po napisaniu klasy A,że potrzebuję 2 wersje klasy, to robię interfejs i 2 klasy które go implementują. Nie wcześniej, nie później. Po co mi interfejs, gdy mam tylko jedną klasę, która robi X? Dlaczego nie miałbym mieć interfejsu, jeśli mam 2 klasy, które mają robić podobne rzeczy?
Tak, takie podejście (iteracyjne? z braku lepszego określenia), będzie się wiązało z tym, że trzeba będzie robić refactor, choć mnie jakoś ta perspektywa nie przeraża, bo nie sądzę, żeby przy sensownie napisanym kodzie, było to jakieś strasznie długie. Są też narzędzia, które to ułatwią (obstawiam, że pewnie nawet zwykły find&replace da radę). Alternatywą jest oczywiście pisanie tony śmieciowego kodu "w razie jak będę chciał wymienić kiedyś tę klasę na inną", co jakoś tak w moim odczuciu zdarza się bardzo rzadko (albo wcale).
Żeby była jasność: nie jestem przeciwnikiem tworzenia interfejsów na samym początku, ale wtedy kiedy jest to rzeczywiście uzasadnione. Jeżeli wiem, że będę miał (co najmniej) 2 różne implementacje, które będę chciał używać zamiennie w zależności od jakichś czynników (np. sensowne wydają się 2 implementacje repository, jedna na hashmapie do testów, druga, która rzeczywiście się łączy do bazy danych) to ok. Podobnie mógłbym od razu podzielić moją ABClass, jeśli widziałbym na początku, że będzie robić za dużo (w sumie pewnie większość ludzi by tak zrobiła, ale chciałem pokazać mój tok rozumowania).
Natomiast zupełnie nie przekonuje mnie podejście: masz taki wzorzec, mądrzy ludzie mówią, że masz pisać tonę boilerplate'u i będziesz mógł sobie super łatwo wymienić każdy interfejs na dowolną implementację! Interfejs, która ma tylko jedną implementację...
W zasadzie to jeszcze sobie pomyślałem, że może to jest jakaś odpowiedź na irracjonalne zachowania biznesu. Nie wiem klient przychodzi co 2 dni, wywraca początkowe ustalenia do góry nogami, więc piszemy bardzo defensywny kod, bo wiemy, że na 90% każdą klasę trzeba będzie zmienić. Ale to już tylko moje dywagacje, bo moje doświadczenie zawodowe jest raczej mizerne.
Podsumowując jak dla mnie cała ta architektura, to tak na prawdę nic więcej poza zasadę pojedynczej odpowiedzialności opakowaną w tonę niepotrzebnego kodu i ładny marketing żeby dobrze się sprzedawało. Jednym słowem pic na wodę, fotomontaż.
Uff, to chyba koniec moich wywodów. Generalnie to zastanawiam się, czy to co tutaj opisałem ma sens, czy może jednak pierniczę głupoty? Fajnie jakby ktoś mógłby mi pokazać luki w moim rozumowaniu.