Modularyzacja Ruby on Rails za pomocą Packwerk Episode I
Ludziom trudno jest zobaczyć szerszy obraz problemu bez poświęcania dużej ilości czasu i wysiłku. Dzieje się tak zwłaszcza podczas pracy z dużymi i złożonymi aplikacjami....
Częstym problemem podczas pracy z Railsami jest decyzja, gdzie umieścić logikę naszych funkcji.
Logika jest często umieszczana w kontrolerach, modelach lub, jeśli mamy szczęście, w obiektach usług. Jeśli więc mamy obiekty usługowe, to po co nam Use Cases?
Śledź mnie w tym artykule, aby odkryć zalety tego wzoru.
Przypadek użycia to lista działań lub kroków zdarzeń zazwyczaj definiujących interakcje między rolą a systemem w celu osiągnięcia celu.
Warto wspomnieć, że wzór ten jest stosowany na wiele różnych sposobów i ma alternatywne nazwy. Możemy go znaleźć jako Interaktorzy, Operatorzy lub Poleceniaale w Ruby społeczność, której się trzymamy Przypadek użycia. Każda implementacja jest inna, ale ma ten sam cel: służyć przypadkowi użycia systemu przez użytkownika.
Nawet jeśli w naszym projekt nie definiujemy wymagań przy użyciu Przypadek użyciai UML ten wzorzec jest nadal przydatny do strukturyzowania logiki biznesowej w praktyczny sposób.
Nasz Przypadki użycia musi być:
Weźmy przykład użytkownika, który chce coś kupić w naszym systemie.
moduł UseCases
moduł Kupujący
klasa Zakup
def initialize(buyer:, cart:)
@buyer = buyer
@cart = cart
end
def call
return unless check_stock
return unless create_purchase
notify end
private
attr_reader :buyer, :cart
def check_stock
Services::CheckStock.call(cart: cart)
end
def create_purchase
Services::CreatePurchase.call(buyer: buyer, cart: cart).call
end
def notify
Services::NotifyBuyer.call(buyer: buyer)
end
end
end
end
Jak można zobaczyć w tym kod przykład, utworzyliśmy nowy Przypadek użycia o nazwie Purchase. Zdefiniowaliśmy tylko jedną publiczną metodę połączenie. Wewnątrz metody wywołania znajdujemy dość podstawowe kroki, aby dokonać zakupu, a wszystkie kroki są zdefiniowane jako metody prywatne. Każdy krok wywołuje obiekt usługi, w ten sposób nasz Przypadek użycia definiuje tylko kroki do dokonania zakupu, a nie samą logikę. Daje nam to jasny obraz tego, co można zrobić w naszym systemie (dokonać zakupu) i kroków, aby to osiągnąć.
Teraz jesteśmy gotowi do wywołania naszego pierwszego Przypadek użycia z kontrolera.
klasa Kontroler
def purchase
UseCases::Buyer::Purchase.new(
buyer: purchase_params[:buyer],
cart: purchase_params[:cart]
).call
...
end
...
koniec
Z tej perspektywy Przypadek użycia wygląda podobnie do obiektu usługi, ale jego przeznaczenie jest inne. Obiekt usługi wykonuje zadanie niskiego poziomu i wchodzi w interakcje z różnymi częściami systemu, takimi jak baza danych, podczas gdy obiekt usługi Przypadek użycia tworzy nową abstrakcję wysokiego poziomu i definiuje kroki logiczne.
Nasz pierwszy Przypadek użycia działa, ale mogłoby być lepiej. Jak moglibyśmy to poprawić? Skorzystajmy z suchy klejnoty. W tym przypadku będziemy używać sucha transakcja.
Najpierw zdefiniujmy naszą klasę bazową.
class UseCase
include Dry::Transaction
class << self
def call(**args)
new.call(**args)
end
end
end
Pomoże nam to przekazać atrybuty do transakcji UseCase i wykorzystać je. Następnie jesteśmy gotowi do ponownego zdefiniowania naszego Purchase Use Case.
moduł UseCases
moduł Kupujący
klasa Purchase
def initialize(buyer:, cart:)
@buyer = buyer
@cart = cart
end
def call
return unless check_stock
return unless create_purchase
notify
end
private
attr_reader :buyer, :cart
def check_stock
Services::CheckStock.call(cart: cart)
end
def create_purchase
Services::CreatePurchase.call(buyer: buyer, cart: cart).call
end
def notify
Services::NotifyBuyer.call(buyer: buyer)
end
end
end
end
Dzięki nowym zmianom możemy w przejrzysty sposób zobaczyć, jak zdefiniowane są nasze kroki i możemy zarządzać wynikiem każdego kroku za pomocą funkcji Success() i Failure().
Jesteśmy gotowi do wywołania naszego nowego przypadku użycia w kontrolerze i przygotowania odpowiedzi w zależności od wyniku końcowego.
klasa Kontroler
def purchase
UseCases::Buyer::Purchase.new.call(
buyer: purchase_params[:buyer],
cart: purchase_params[:cart]
) do |result|
result.success do
...
end
result.failure do
...
end
end
...
end
...
koniec
Ten przykład można jeszcze bardziej ulepszyć za pomocą walidacji, ale to wystarczy, aby pokazać moc tego wzorca.
Bądźmy szczerzy. Wzorzec przypadku użycia jest dość prosty i wygląda podobnie do obiektu usługi, ale ten poziom abstrakcji może spowodować duże zmiany w aplikacji.
Wyobraźmy sobie, że nowy programista dołącza do projektu i otwiera folder use_cases, a na pierwszy rzut oka będzie miał listę wszystkich funkcji dostępnych w systemie, a po otwarciu jednego przypadku użycia zobaczy wszystkie niezbędne kroki dla tej funkcji bez zagłębiania się w logikę. To poczucie porządku i kontroli jest główną zaletą tego wzorca.
Zabierz to do swojej skrzynki z narzędziami, a być może w przyszłości zrobisz z tego dobry użytek.