Remote work

Jak z pomocą abstrakcji Trailblazera lepiej uporządkować logikę biznesową aplikacji?

Zdaję sobie sprawę z tego, że język Ruby, który stał się tak popularny dzięki frameworkowi Ruby on Rails, swoje lata świetności powoli ma już za sobą. Wokół tego języka społeczność nadal wprowadza użyteczne i nowatorskie rozwiązania w zakresie developmentu aplikacji internetowych. To właśnie dzięki tej społeczności powstało jedno z lepszych (moim zdaniem) rozwiązań pod względem organizacji kodu aplikacji oraz podziału logiki biznesowej na odpowiedni poziom abstrakcji. Dlatego chciałbym je Wam przedstawić i pokazać, w jaki sposób lepiej zorganizować strukturę aplikacji odpowiadającą za logikę biznesową.


Marcin Lazar. Full-Stack web developer w Appchance, absolwent technikum informatycznego w Pszczynie. Dość szybko postanowił przetestować swoje umiejętności w praktyce rozpoczynając pracę nad komercyjnymi projektami przy tworzeniu aplikacji internetowych jeszcze podczas nauki w technikum. Ciekawość w poznawanie nowych technologii jest jednym z jego motorów napędowych, która pcha go do ciągłego doskonalenia swoich umiejętności.


Na początek trochę o spaghetti

Abyście mogli mnie lepiej zrozumieć, pokażę Wam, na czym polegał mój problem, gdy zająłem się tworzeniem aplikacji internetowych. Moim pierwszym językiem do tworzenia bardziej złożonych aplikacji internetowych był PHP. Na początku byłem zachwycony tym, że mogę połączyć się z bazą danych i pobrać z niej informacje. Potem udało mi się zrobić prostą rejestrację użytkownika. Z tego co pamiętam, byłem bardzo zadowolony z otrzymanego rezultatu. Jednak kiedy wróciłem do rozwijania swojej super-zaawansowanej aplikacji, stwierdziłem, że… napisany kod mogę porównać do spaghetti.

Źródło grafiki: http://bit.ly/2BrnogH

W pliku HTML przeplatał się z językiem PHP i na odwrót. Na dodatek gdzieś pod formularzem znajdował się niedługi skrypt spełniający logikę biznesową rejestracji (zapis danych użytkownika oraz wysłanie maila powitalnego), a przed tym wszystkim walidacja wprowadzonych danych. Patrząc wtedy na ten kod, stwierdziłem, że w taki sposób nie mogą powstawać aplikacje internetowe. Miałem zaledwie prostą rejestrację, a bez głębszej analizy trudno mi było zidentyfikować poszczególne funkcje kodu. Zacząłem więc poszukiwać w Internecie różnych wzorców do rozdzielania kodu w PHP na małe niezależne moduły.

Po długim researchu znalazłem, moim zdaniem, świetny framework PHP, a mianowicie Symfony 2. Oparty był on o architekturę MVC i wzorował się na rozwiązaniach ze świata Ruby on Rails. Pomimo tego, że w aplikacji już miałem zaimplementowany wzorzec MVC, to nie umiałem w pełni wykorzystać jego zalet architektonicznych. Całą logikę biznesową, jaką zawierała moja aplikacja, umieszczałem w kontrolerach, które nigdy nie powinny do tego służyć. Być może uniknąłbym tego błędu, gdyby połowa for internetowych oraz poradników nie promowała właśnie takich antywzorców.

Początki w Ruby on Rails

Nie pamiętam ile czasu spędziłem w świecie PHP, ale pamiętam, że gdy zacząłem odkrywać świat Ruby on Rails byłem bardzo zaskoczony każdymi kolejnymi możliwościami, jakie dawał mi ten framework. Na początku kontynuowałem umieszczanie logiki w kontrolerach, co w zakresie porządkowania struktury aplikacji było małym postępem, ponieważ wszystkie walidacje w architekturze frameworka lądowały w modelu. Po krótkim czasie pisania aplikacji z frameworkiem odkryłem jednak, że istnieje coś takiego jak callbacki w modelach.

Owe callbacki pozwalają w aplikacji wywoływać szereg różnych operacji, np. po zapisaniu lub edycji danego modelu w bazie danych. Byłem bardzo zadowolony z nowych możliwości, które dawał mi ten koncept. Zacząłem przenosić strukturę logiki biznesowej z kontrolerów do modeli. Wtedy jeszcze nie wiedziałem, że postępowałem zgodnie z doktryną fat model, skinny controller. W końcu zacząłem powoli odnosić wrażenie, że piszę już aplikacje, które mają jakąś uporządkowaną strukturę, a każdy skrypt odpowiedzialny za logikę biznesową ma swoje miejsce w aplikacji.

Droga do bałaganu

Z biegiem czasu zacząłem pisać aplikacje implementujące coraz bardziej rozbudowaną logikę biznesową. Teraz już nie wystarczyło wysłać samego maila do użytkownika po rejestracji czy zmienić status wiadomości na odczytaną. Z czasem warunki, przy których powinien uruchomić się callback, stawały się coraz bardziej skomplikowane. Miało to wpływ na czytelność kodu, co utrudniało jego późniejsze zrozumienie przez innych developerów (np. przy rozwoju projektu). Posłużę się niezbyt skomplikowanym przykładem implementacji callbacków i walidacji w modelach railsowych.

class User < ApplicationRecord
  before_validation :downcase_email, on: :create
  after_create :send_welcome_email, if: domain_exist?
  after_update :send_info_to_admin
 
  validates :email, format: { with: /A([^@s]+)@((?:[-a-z0-9]+.)+[a-z]{2,})z/i}
  validates :password, length: { in: 6..20 }
 
  private
 
  def send_welcome_email
    MyAwesomeMailer.welcome_user(self).deliver_later
  end
 
  def send_info_to_admin
    MyAwesomeMailer.admin(self).deliver_later
  end
 
  def domain_exist?
    PingEmailService.call(email)
  end
end

Na pierwszy rzut oka wygląda wszystko OK. Po bliższej analizie można jednak zauważyć, że logika biznesowa realizowana podczas tworzenia obiektu przeplata się z tą tworzoną po jego edytowaniu. Dostrzeżemy również, że implementacja naszego kodu logiki w tym miejscu staje się monolityczna wraz z całym modelem. Model ten tak naprawdę powinien reprezentować abstrakcję obiektu z bazy danych i jego powiązań z innymi strukturami modeli. Również walidacje w nim zawarte są ściśle powiązane z naszym modelem. Zastanówmy się, dlaczego może to być problematyczne.

Załóżmy, że w pewnej części aplikacji pozwalamy stworzyć użytkownika bez konieczności podawania imienia, a w innej części formularza podanie imienia jest wymagane. Musimy napisać różnego rodzaju instrukcje sprawdzające, czy mamy w danej operacji wykonać daną walidację. Im więcej dodatkowej logiki walidacyjnej zachodzącej pod pewnymi warunkami, tym większy bałagan w naszych modelach.

W dobrym kierunku

Po pewnym czasie pisania w frameworku Ruby on Rails znalazłem swoją pierwszą pracę, mój mentor wykorzystywał przy tworzeniu aplikacji wzorce o nazwie Form Object. Były to obiekty, które przyjmowały różne dane wejściowe, ale korzystały tylko z tych, które zostały w nich zadeklarowane. Wykonywały również listę operacji do spełnienia wybranej logiki. Jeden obiekt mógł wykonywać tylko jedną operację, czyli np. mógł zapisywać coś do bazy albo coś edytować. Już wtedy podobało mi się to podejście, w którym obiekt realizował tylko jedną (!) operację, np. wykonując tworzenie obiektu, nie zawierał logiki do edycji. Jednak w tym rozwiązaniu walidacje nadal pozostawały w naszym modelu, co było już prawie zadowalające…

Nie musiałem długo czekać, bo dowiedziałem się, że istnieje framework o nazwie Trailblazer, o którym pisano, że zawiera ciekawe rozwiązanie w zakresie porządkowania logiki biznesowej. Dlatego zacząłem dowiadywać się czegoś więcej na ten temat.

Trailblazer – informacje

Dowiedziałem się, że Trailblazer to typowy framework, który do podstawowego wzoru MVC dodaje kolejne poziomy abstrakcji. Ma on o wiele lepsze rozwiązania technologiczne od railsów oraz zdefiniowaną wewnętrzną konwencję, której należy się trzymać. O Trailblazerze można dużo napisać, jednak artykuł ten nie jest tutorialem, który pokazuje, jak należy pisać kod. Moim celem jest pokazanie organizacji struktury aplikacji i logiki biznesowej kodu. Przedstawię zatem zalety implementacji Trailblazera oraz przykłady wykorzystania go.

Do zaimplementowania logiki biznesowej wystarczą nam tylko dwie abstrakcje, które implementuje Trailblazer:

  • Kontrakt – obiekt, który zawiera definicję niezbędnych danych, których oczekujemy w danej operacji, oraz jest odpowiedzialny za ich walidację.
  • Operacja – zbiór kroków potrzebnych do obsługi konkretnej akcji biznesowej, np. CreatePost.

Struktura folderów i nazewnictwo w Trailblazerze

Jak wcześniej wspomniałem, Trailblazer ma zdefiniowaną konwencję. Cała logika biznesowa aplikacji powinna być zawarta w folderze concepts, który znajduje się w głównym katalogu aplikacji (app). W podkatalogach znajdują się foldery, które swoimi nazwami reprezentują nasze modele. W katalogu o nazwie contracts znajdują się kontrakty, których nazwy odpowiadają nazwom operacji. Jak łatwo się domyślić, w katalogu o nazwie operations znajdują się wszystkie operacje.

struktura folderów i nazewnictwo Trailblazera

Operacje

Operacja jest sercem architektury Trailblazer. Obejmuje ona zadania polegające m.in.: na wyszukiwaniu lub tworzeniu nowego modelu i sprawdzeniu poprawności danych przychodzących przy użyciu obiektu kontraktu. Poniższy przykład obrazuje budowę operacji, która tworzy nowy model Post, równocześnie spełniając po kolei zadania, zgodnie z naszą logiką biznesową.

class Post::Operation
  class Create < ::ApplicationOperation
    step Model(Post, :new)
    step :assign_user!
    step ApplicationOperation::Contract::Build(
      constant: ::Post::Contract::Create
    )
    step ApplicationOperation::Contract::Validate()
    step ApplicationOperation::Contract::Persist()
    step :send_welcome_email

    def send_welcome_email(params, **)
       MyAwesomeMailer.send_welcome_email(params[‘model’]).delivery_later
    end

    def assign_user!(params, **)
       params['model'].user = params['current_resource']
    end
  end
end

Poszczególne fragmenty logiki są wykonywane przez stepy (kroki), co moim zdaniem jest genialnym rozwiązaniem – wystarczy na nie spojrzeć i od razu widzimy, co po kolei jest realizowane w naszej operacji. Warto w tym punkcie wspomnieć, że jeżeli dany krok się nie powiedzie, cała operacja jest przerywana.

W pierwszym kroku tworzymy nowy model Post, a w następnym przypisujemy aktualnie zalogowanego użytkownika jako jego właściciela. Aby przypisać dane, które wysłaliśmy do naszej operacji do naszego nowo utworzonego modelu, musimy je najpierw zwalidować. Tak jak pisałem wcześniej, właśnie do tego służą kontrakty. Kontrakt określa właściwości i zasady walidacji operacji. To obiekt zainicjowany przez operację w celu sprawdzenia poprawności danych, które zostały jej przekazane. Za jego pośrednictwem z danych wejściowych najpierw wybierane są konkretne dane, które następnie mogą zostać w nim zwalidowane. W kontrakcie możemy również określić, które dane wejściowe mają zostać przypisane do danego atrybutu naszego modelu. Poniższy przykład przedstawia przykładową implementację kontraktu dla naszej operacji.

module Post::Contract
  class Create < Base
    property :title
    property :description

    validation with: { form: self } do
      required(:title).filled(size?: (5..70))
      required(:description).filled(size?: (5..4096))
    end
  end
end

Na powyższym przykładzie, za pomocą metody property definiujemy wszystkie parametry, jakie chcemy przechwycić, zwalidować, a ostatecznie przypisać do naszego modelu. Jak łatwo się domyślić, w bloku validation mamy definicję reguł walidujących atrybuty kontraktu.

W dalszej kolejności operacji sprawdzamy przy pomocy kontraktu czy parametry, które wysłaliśmy do naszej operacji, są prawidłowe. Jeśli nasz kontrakt przeszedł pomyślnie walidację, oznacza to, że parametry są prawidłowe. Ostatnim krokiem jest przypisanie danych z kontraktu do naszego modelu oraz jego zapis w bazie danych. Brawo, nasza operacja jest gotowa! Teraz możemy użyć jej w dowolnym miejscu aplikacji. Nasze kontrolery wiedzą tylko tyle, ile powinny. Przechwytują żądanie HTTP i kierują je do operacji. Logika aplikacji nie wycieka do frameworka webowego.

Podsumowanie

Trailblazer to, w mojej ocenie, genialny koncept tworzenia struktur aplikacji internetowych. Pomaga organizować duże struktury kodu w małe niezależne moduły. Można powiedzieć, że swoją budową wyręcza nasz wzór MVC z konieczności implementowania w nim logiki aplikacji. Myślę, że warto z niego skorzystać przy większych projektach, które mają rozbudowaną logikę biznesową lub planowany jest ich ciągły rozwój. Dzięki swojej modularności Trailblazer idealnie nadaje się do zaimplementowania go w istniejących już aplikacjach, w których wprowadza się nowe funkcje.

Z mojego punktu widzenia wykorzystanie Trailblazera w małych projektach, gdy przewidujemy, że logika biznesowa raczej nie będzie zbytnio rozbudowana, mija się z celem. Może się okazać, że więcej czasu poświęcimy na poprawne skonfigurowanie, niż na stworzenie działającej części aplikacji.

Zalety Trailblazera:

  • Łatwość rozwijania aplikacji,
  • Łatwość testowania kodu oraz logiki,
  • Dobra organizacja kodu,
  • Możliwość równoległej pracy nad poszczególnymi modułami projektu przez różne zespoły,
  • Modułowa struktura,
  • Możliwość wdrożenia frameworka do istniejącej już aplikacji i dalszy bezkonfliktowy jej rozwój z wykorzystaniem nowej architektury.

Wady Trailblazera:

  • Skomplikowana konfiguracja,
  • Duża liczba procesów odbywających się automatycznie bez ingerencji i wiedzy programisty,
  • Dość wysoki próg wejścia,
  • Skomplikowane walidacje DRY.

Sprawdź projekt, w którym wykorzystaliśmy architekturę Trailblazer: myPhsar.


Artykuł został pierwotnie opublikowany na appchance.pl.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/pomoca-abstrakcji-trailblazera-lepiej-uporzadkowac-logike-biznesowa-aplikacji/" order_type="social" width="100%" count_of_comments="8" ]