Refaktoryzacja ku heksagonalnym Railsom
Rails to świetny mechanizm dostarczania i kiepskie miejsce na logikę biznesową. Deep-dive o architekturze heksagonalnej w Railsach — porty i adaptery, izolacja domeny od frameworka i jak daleko to ciągnąć, zanim stanie się przeinżynierowaniem.
Krąży od jakiegoś czasu prowokacyjna idea: Rails to nie twoja aplikacja. Rails to mechanizm dostarczania — znakomity do zamiany żądań HTTP w wiersze bazy i z powrotem — ale faktyczna wartość twojego oprogramowania, reguły biznesowe, które czynią je warte pieniędzy, ma zwyczaj rozpuszczać się w kontrolerach, modelach, callbackach i concernach, aż staje się niemożliwa do znalezienia, przetestowania w izolacji czy ogarnięcia bez bootowania całego frameworka. Architektura heksagonalna to jedna zdyscyplinowana odpowiedź na to. Oto co to znaczy w kontekście Rails i — równie ważne — jak daleko to ciągnąć, zanim lekarstwo stanie się gorsze niż choroba.
Sedno: porty i adaptery
Architektura heksagonalna (zwana też „ports and adapters”) umieszcza twoją logikę biznesową w centrum, a wszystko inne — web, bazę, usługi zewnętrzne — wypycha na krawędzie. Centrum, twoja domena, nie wie nic o Railsach, HTTP czy SQL. Komunikuje się ze światem zewnętrznym przez porty (interfejsy, które sama definiuje), a świat zewnętrzny wpina się w te porty przez adaptery (implementacje).
Obraz myślowy: heksagon z twoją domeną w środku. Z jednej strony adaptery napędzają domenę — kontroler Rails, zadanie rake, zadanie Sidekiq wszystkie wołają ten sam kod domeny. Z drugiej strony domena napędza adaptery — potrzebuje coś utrwalić, więc woła port repozytorium, a adapter ActiveRecord go implementuje. Kluczową właściwością jest kierunek zależności: wszystko wskazuje do wewnątrz. Domena nie zależy od niczego; framework zależy od domeny. Rails staje się detalem na krawędzi, a nie rzeczą, w którą twoja logika jest wplątana.
Jak to wygląda w praktyce
Najużyteczniejszym pierwszym krokiem jest oddzielenie przypadków użycia (operacji biznesowych) i repozytoriów (trwałości) od frameworka. Domena definiuje zwykłe obiekty Ruby i interfejsy, których potrzebuje:
# domena — bez Rails, bez ActiveRecord, sama logika biznesowa
class PlaceOrder
def initialize(orders:, payments:)
@orders = orders # port repozytorium
@payments = payments # port bramki
end
def call(cart:)
order = Order.new(items: cart.items, total: cart.total)
@payments.charge(order.total)
@orders.save(order)
order
end
end
PlaceOrder nie wie, że @orders jest wsparte ActiveRecordem ani że @payments
rozmawia ze Stripe. Zależy od ról — czegoś, co potrafi zapisać zamówienie, czegoś, co
potrafi obciążyć — nie od konkretnych klas. Adaptery żyją na krawędzi:
# adapter — strona Rails/AR, implementująca port
class ActiveRecordOrders
def save(order)
OrderRecord.create!(order.to_h) # tłumacz obiekt domeny <-> rekord AR
end
end
# kontroler jest teraz cienkim adapterem napędzającym
class OrdersController < ApplicationController
def create
order = PlaceOrder.new(orders: ActiveRecordOrders.new,
payments: StripeGateway.new).call(cart: current_cart)
redirect_to order_path(order.id)
end
end
Jedynym zadaniem kontrolera jest przetłumaczenie żądania HTTP na wywołanie domeny i
wyniku z powrotem na odpowiedź. Detal trwałości (ActiveRecordOrders) jest
wstrzykiwany, więc da się go podmienić — na atrapę w testach, na inny skład później —
bez wiedzy domeny.
Po co się trudzić: zyski
To więcej pracy niż czysty Rails, więc musi zarobić na swoje miejsce. Prawdziwe wygrane:
- Domena jest testowalna w milisekundach.
PlaceOrderda się unit-testować z atrapami w pamięci dla jego portów — bez bazy, bez HTTP, bez bootu Railsów. Logika, która liczy się najbardziej, dostaje najszybsze, najbardziej skupione testy. - Reguły biznesowe są znajdywalne. „Jak działa składanie zamówienia?” ma jedną
odpowiedź: przeczytaj przypadek użycia
PlaceOrder. Logika nie jest rozsmarowana po kontrolerze, trzech callbackach modelu i obserwatorze. - Framework staje się wymienialny na krawędziach. Podmiana Stripe na innego dostawcę płatności to nowy adapter, a nie polowanie po bazie kodu. Nawet podmiana samego Rails staje się wyobrażalna, bo domena nigdy od niego nie zależała.
- Usługi zewnętrzne są mockowalne na czystym szwie. Port jest granicą; testy wstrzykują atrapę adaptera i nigdy nie dotykają sieci.
Uczciwy koszt i jak daleko iść
Teraz kluczowe zastrzeżenie, bo architekturę heksagonalną równie łatwo nadużyć, co
obiekty usługowe. Pełne ports-and-adapters wszędzie to mnóstwo pośredniości: obiekty
domeny oddzielone od rekordów AR, kod mapujący między nimi, interfejsy portów,
implementacje adapterów, okablowanie wstrzykiwania zależności. Dla aplikacji CRUD, która
jest głównie formularzami nad bazą — czyli wielu aplikacji Rails, i dokładnie tym, w
czym Rails jest błyskotliwy — to czysty narzut. Napiszesz trzy warstwy, by osiągnąć to,
co robił Post.create, i nic nie zyskasz, bo nie było żadnych złożonych reguł
biznesowych do ochrony na początku.
Pragmatyczne stanowisko, które trzymamy:
- Domyślnie czysty Rails. Dla prostego CRUD pozwól ActiveRecord być twoją domeną. „Model jest obiektem biznesowym” Railsów działa cudownie, dopóki reguły biznesowe nie staną się naprawdę złożone. Nie płać za izolację, której nie potrzebujesz.
- Wprowadzaj granice tam, gdzie koncentruje się złożoność. Gdy jeden obszar aplikacji ma zawiłe reguły — ceny, kwalifikowalność, wielokrokowy workflow, ciężka integracja z usługami zewnętrznymi — to tam szew heksagonalny się opłaca. Izoluj pokręconą domenę; zostaw prosty CRUD jako czysty Rails.
- Nie potrzebujesz całego heksagonu, by uzyskać korzyść. Nawet samo wydzielenie przypadków użycia (obiekty usługowe ze wstrzykniętymi zależnościami) i umieszczenie trwałości za repozytorium dla złożonych części łapie większość wartości bez hurtowego przepisywania. Przyjmij kierunek (zależności wskazują do wewnątrz; izoluj domenę) bardziej niż pełną ceremonię.
Werdykt
„Rails to nie twoja aplikacja” to użyteczna prowokacja, nie absolutne prawo. Rails to fantastyczny mechanizm dostarczania, a dla większości tego, co robi większość aplikacji, pozwolenie frameworkowi być aplikacją jest dokładnie właściwe. Ale gdy nadejdzie prawdziwa złożoność biznesowa, logika domeny zasługuje na ochronę przed frameworkiem — testowalna sama, znajdywalna w jednym miejscu i zależna od niczego poza sobą. Architektura heksagonalna daje ci słownictwo do tego: porty, adaptery, zależności wskazujące do wewnątrz. Stosuj ją chirurgicznie, tam gdzie złożoność faktycznie żyje, a uczyni trudne części twojej aplikacji dramatycznie łatwiejszymi do ogarnięcia. Stosuj wszędzie, a właśnie odbudowałeś Rails, źle. Umiejętnością jest wiedza, które części twojej aplikacji są heksagonem, a które zawsze były w porządku jako czysty Rails.