Plusy i minusy używania wywołań zwrotnych dla logiki domeny w Rails

Co widzisz jako plusy i minusy używania wywołań zwrotnych dla logiki domeny? (Mówię o Rails i/lub projektach Ruby.)

Aby rozpocząć dyskusję, chciałem wspomnieć o tym cytacie ze strony Mongoid na callbacks :

Używanie wywołań zwrotnych dla logiki domeny jest złą praktyką projektową i może prowadzić do nieoczekiwane błędy, które trudno debugować, gdy wywołania zwrotne w łańcuchu zatrzymują się egzekucja. To jest nasza rekomendacja, aby używać ich tylko do Przekrój poprzeczny obawy, takie jak ustawianie w kolejce zadań w tle.

Chciałbym usłyszeć argument lub obronę tego twierdzenia. Czy ma on zastosowanie tylko do aplikacji Mongo-backed? Czy jest przeznaczony do stosowania w technologiach bazodanowych?

Wydaje się, że Przewodnik Ruby on Rails po Walidacjach ActiveRecord i wywołaniach zwrotnych może się nie zgadzać, przynajmniej jeśli chodzi o relacyjne bazy danych. Weźmy ten przykład:

class Order < ActiveRecord::Base
  before_save :normalize_card_number, :if => :paid_with_card?
end

Moim zdaniem jest to doskonały przykład prostego wywołania zwrotnego, który implementuje logikę domeny. Wydaje się szybki i skuteczny. Gdybym skorzystał z Rady Mongoida, to gdzie by ta logika poszła?

Author: David J., 2012-06-14

6 answers

Bardzo lubię używać wywołań zwrotnych dla małych klas. Uważam, że sprawia, że klasa jest bardzo czytelna, np. coś w stylu

before_save :ensure_values_are_calculated_correctly
before_save :down_case_titles
before_save :update_cache

Jest natychmiast jasne, co się dzieje.

Uważam nawet, że można to przetestować; mogę sprawdzić, czy same metody działają, i mogę przetestować każde wywołanie zwrotne osobno.

Mocno wierzę, że wywołania zwrotne w klasie powinny być używane tylko dla aspektów, które należą do klasy. Jeśli chcesz wyzwalać zdarzenia przy zapisie, np. wysyłanie wiadomości, Jeśli obiekt jest w pewnym stanie, czyli zalogowaniu, użyłbym obserwatora. Jest to zgodne z zasadą jednolitej odpowiedzialności.

Callbacks

Przewaga wywołań zwrotnych:

    Wszystko jest w jednym miejscu, dzięki czemu jest to łatwe]}
  • bardzo czytelny kod

Wady wywołań zwrotnych:

    Ponieważ wszystko jest jednym miejscem, łatwo jest złamać zasadę jednej odpowiedzialności]}
  • może zrobić dla ciężkich klas
  • co dzieje się, jeśli jeden callback nie powiedzie? nadal podąża za łańcuchem? Podpowiedź: upewnij się, że wywołania zwrotne nigdy nie zawiodą lub w inny sposób Ustaw stan modelu na nieprawidłowy.

Obserwatorzy

Przewaga obserwatorów

  • Bardzo czysty kod, można zrobić kilka obserwatorów dla tej samej klasy, każdy robi inną rzecz
  • wykonywanie obserwatorów nie jest sprzężone

Wada obserwatorów

  • na początku może być dziwnie, jak zachowanie jest wyzwalane (patrz w obserwatora!)

Podsumowanie

W skrócie:

  • użyj wywołań zwrotnych dla prostych, związanych z modelem rzeczy (obliczone wartości, wartości domyślne, walidacje)
  • używaj obserwatorów do bardziej przekrojowych zachowań (np. wysyłania poczty, propagowania stanu, ...)

I jak zawsze: wszelkie rady należy przyjmować z przymrużeniem oka. Ale z mojego doświadczenia obserwatorzy skalują się naprawdę dobrze (i są też mało znani).

Nadzieja to pomaga.

 28
Author: nathanvda,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-29 21:54:19

EDIT: połączyłem moje odpowiedzi na zalecenia niektórych osób tutaj.

Podsumowanie

Na podstawie lektury i przemyśleń doszedłem do kilku (wstępnych) stwierdzeń tego, w co wierzę:

  1. Stwierdzenie "używanie wywołań zwrotnych dla logiki domeny jest złą praktyką projektową" jest fałszywe, jak napisano. To zawyża punkt. Wywołania zwrotne mogą być dobrym miejscem dla logiki domeny, odpowiednio używane. Pytanie nie powinno być Jeśli logika modelu domeny powinna go in callbacks, it is what kind of domain logic ma sens, aby wejść.

  2. Stwierdzenie " używanie wywołań zwrotnych dla logiki domeny ... może prowadzić do nieoczekiwanych błędów, które są trudne do debugowania, gdy wywołania zwrotne w wykonaniu zatrzymania łańcucha " jest prawdą.

  3. Tak, wywołania zwrotne mogą powodować reakcje łańcuchowe, które wpływają na inne obiekty. Do tego stopnia, że nie można tego przetestować, jest to problem.

  4. Tak, powinieneś być w stanie przetestować swoją logikę biznesową bez konieczności aby zapisać obiekt w bazie danych.

  5. Jeśli wywołania zwrotne jednego obiektu są zbyt rozdęte dla Twojej wrażliwości, istnieją alternatywne projekty do rozważenia, w tym (a) obserwatorzy lub (b) klasy pomocnicze. Mogą one w prosty sposób obsługiwać operacje wielu obiektów.

  6. Rada" aby używać tylko [wywołania zwrotne] dla problemów przekrojowych, takich jak kolejkowanie w tle pracy " jest intrygująca, ale zawyżone. (Przejrzałem przekrojowe obawy , aby sprawdzić, czy może przeoczyłem coś.)

Chcę również podzielić się niektórymi z moich reakcji na posty na blogu, które czytałem, że dyskusja na ten temat:

Reakcje na "wywołania ActiveRecord zrujnowały mi życie"

Post Mathiasa Meyera z 2010 roku, Callbacks ActiveRecord zrujnował mi życie , oferuje jedną perspektywę. Pisze:

Kiedy zacząłem dodawać walidacje i wywołania zwrotne do modelu w aplikacji Rails [... To było złe. Czułem się, jakbym dodawał kod, który nie powinien bądź tam, to sprawia, że wszystko jest o wiele bardziej skomplikowane i zamienia jawny w ukryty kod.

Uważam, że to ostatnie twierdzenie "zamienia jawny w ukryty kod" jest, cóż, niesprawiedliwym oczekiwaniem. Mówimy tu o szynach, prawda?! Tak duża część wartości dodanej polega na tym, że Rails robi rzeczy "magicznie", np. bez konieczności jawnego wykonywania tego przez dewelopera. Czy nie wydaje się dziwne cieszyć się owocami Rails, a mimo to krytykować ukryty kod?

Kod, który jest uruchamiane tylko w zależności od stanu trwałości obiektu.

Zgadzam się, że to brzmi niesmacznie.

Kod, który jest trudny do przetestowania, ponieważ musisz zapisać obiekt, aby przetestować części swojej logiki biznesowej.

Tak, to sprawia, że testowanie jest powolne i trudne.

Podsumowując, myślę, że Mathias dodaje trochę ciekawego paliwa do ognia, choć nie uważam, że to wszystko jest fascynujące.

Reakcje na " szalony, heretycki i niesamowity: sposób, w jaki Napisz Aplikacje Rails "

W poście Jamesa Golicka z 2010 roku, Crazy, Heretical, and Awesome: the Way I Write Rails Apps, pisze:

Również łączenie całej logiki biznesowej z obiektami trwałości może mieć dziwne skutki uboczne. W naszej aplikacji, gdy coś jest tworzone, wywołanie zwrotne after_create generuje wpis w logach, które są używane do wytworzenia kanału aktywności. Co zrobić, jeśli chcę utworzyć obiekt bez logowania - powiedzmy, w konsoli? Nie mogę. i są małżeństwem na zawsze i na całą wieczność.

Później dociera do korzenia:

Rozwiązanie jest właściwie dość proste. Uproszczone wyjaśnienie problemu polega na tym, że naruszyliśmy zasadę jednej odpowiedzialności. Więc użyjemy standardowych technik obiektowych, aby oddzielić obawy naszej logiki modelu.

Naprawdę doceniam to, że moderuje swoje rady, mówiąc, kiedy ma zastosowanie, a kiedy ma nie:

Prawda jest taka, że w prostej aplikacji otyłe obiekty wytrwałości mogą nigdy nie zaszkodzić. Kiedy sprawy stają się trochę bardziej skomplikowane niż operacje CRUD, te rzeczy zaczynają się piętrzyć i stają się punktami bólu.

 9
Author: David J.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-20 17:11:13

To pytanie tutaj (Ignoruj błędy walidacji w rspec) jest doskonałym powodem, dla którego nie umieszczać logiki w wywołaniach zwrotnych: Testowalność.

Twój kod Może mieć tendencję do rozwijania wielu zależności w czasie, gdzie zaczynasz dodawać unless Rails.test? do swoich metod.

Zalecam tylko zachowanie logiki formatowania w wywołaniu zwrotnym before_validation i przenoszenie rzeczy, które dotykają wielu klas do obiektu usługowego.

Więc w Twoim przypadku, chciałbym przenieść normalize_card_number to a before_validation, a następnie można sprawdzić, czy numer karty jest znormalizowany.

Ale gdybyś musiał gdzieś wyjść i utworzyć PaymentProfile, zrobiłbym to w innym obiekcie workflow service:

class CreatesCustomer
  def create(new_customer_object)
    return new_customer_object unless new_customer_object.valid?
    ActiveRecord::Base.transaction do
      new_customer_object.save!
      PaymentProfile.create!(new_customer_object)
    end
    new_customer_object
  end
end

Możesz następnie łatwo przetestować pewne warunki, na przykład jeśli nie są poprawne, jeśli zapis nie nastąpi lub jeśli bramka płatności wyświetli wyjątek.

 2
Author: Jesse Wolgamott,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:39

Moim zdaniem, najlepszy scenariusz za pomocą wywołań zwrotnych jest wtedy, gdy metoda odpalania go nie ma nic wspólnego z tym, co jest wykonywane w samym wywołania zwrotnego. Na przykład dobry before_save :do_something nie powinien wykonywać kodu związanego z zapisem . To raczej jak powinien działać Obserwator .

Ludzie używają wywołań zwrotnych tylko do wysuszenia kodu. Nie jest źle, ale może prowadzić do skomplikowanego i trudnego w utrzymaniu kodu, ponieważ czytanie metody save nie powie Ci Wszystkiego, jeśli nie uwaga wywoływane jest wywołanie zwrotne. Myślę, że ważne jest, aby jawny kod (szczególnie w Ruby i Rails, gdzie tak dużo magii się dzieje).

Wszystko co związane zzapisywaniem powinno znajdować się w metodzie save. Jeśli na przykład wywołanie zwrotne ma mieć pewność, że użytkownik jest uwierzytelniony, co nie ma związku z zapisem , to jest to dobry scenariusz wywołania zwrotnego.

 2
Author: Wawa Loo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-19 12:26:33

Avdi Grimm ma kilka świetnych przykładów w swojej książce Object On Rails .

Znajdziesz tutaj i tutaj dlaczego nie wybiera opcji callback i jak można się tego pozbyć po prostu przez nadpisanie odpowiedniej metody ActiveRecord.

W Twoim przypadku skończysz z czymś takim jak:

class Order < ActiveRecord::Base

  def save(*)
    normalize_card_number if paid_with_card?
    super
  end

  private

  def normalize_card_number
    #do something and assign self.card_number = "XXX"
  end
end

[UPDATE after your comment "this is still callback"]

Kiedy mówimy o wywołaniach zwrotnych dla logiki domeny, Rozumiem ActiveRecord wywołania zwrotne, proszę poprawić mnie, jeśli uważasz, że cytat z mongoid referer do czegoś innego, jeśli istnieje "projekt callback" gdzieś go nie znalazłem.

Myślę, że ActiveRecord callbacks są, dla większości (całe?) część nic więcej niż cukier składniowy, którego możesz pozbyć się na moim poprzednim przykładzie.

Po pierwsze, zgadzam się, że ta metoda wywołania kryje za sobą logikę : dla kogoś, kto nie jest zaznajomiony z ActiveRecord, będzie musiał nauczyć się go zrozumieć kod, z wersją powyżej, to jest łatwo zrozumiałe i testowalne.

Co może być najgorsze z wywołaniem ActiveRecord jego "powszechnym używaniem" lub "uczuciem odsprzęgania", które mogą wytworzyć. Wersja callback może wydawać się miły na początku, ale jak będzie dodać więcej wywołań zwrotnych, będzie trudniej zrozumieć kod (w jakiej kolejności są one ładowane, które można zatrzymać przepływ realizacji, itp...)i przetestować (logika domeny jest sprzężona z logiką ActiveRecord persistence).

Kiedy czytam mój przykład poniżej, źle się czuję z tym kodem, to zapach. Wierzę, że prawdopodobnie nie skończy się z tym kodem, jeśli robisz TDD/BDD i, jeśli zapomnisz o ActiveRecord, myślę, że po prostu napisałbyś metodę card_number=. Mam nadzieję, że ten przykład jest wystarczająco dobry, aby nie wybrać bezpośrednio opcji callback i najpierw pomyśleć o projekcie.

Co do cytatu z MongoId zastanawiam się, dlaczego radzą, aby nie używać callback dla logiki domeny, ale użyć go do kolejkowania zadania w tle. I think queueing background job może być częścią logiki domeny i czasami może być lepiej zaprojektowany z czymś innym niż wywołanie zwrotne (powiedzmy Obserwator).

Wreszcie, jest trochę krytyki na temat tego, jak ActiveRecord jest używany / implementowany z Rail z Obiektowego punktu widzenia projektowania programowania, ta odpowiedź zawiera dobre informacje na jego temat, a znajdziesz je łatwiej. Możesz również sprawdzić wzór projektu datamapper / ruby implementation project który może być zamiennik (ale o ile lepszy) dla ActiveRecord i nie mają jego słabości.

 2
Author: Adrien Coquio,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-12 07:31:17

Myślę, że odpowiedź nie jest zbyt skomplikowana.

Jeśli zamierzasz zbudować system o deterministycznym zachowaniu, oddzwanianie, które zajmują się sprawami związanymi z danymi, takimi jak normalizacja, jest w porządku, a oddzwanianie, które zajmują się logiką biznesową, takie jak wysyłanie wiadomości e-mail z potwierdzeniem, nie jest w porządku.

OOP został spopularyzowany z emergent behavior as a best practice1, z doświadczenia wiem, że Rails się zgadza. Wiele osób, w tym facet, który wprowadził MVC , myślę, że powoduje to niepotrzebny ból w aplikacjach, w których zachowanie uruchomieniowe jest deterministyczne i dobrze znane z wyprzedzeniem.

Jeśli zgadzasz się z praktyką oo emergent behavior, to wzór Active record zachowania sprzężenia z wykresem obiektu danych nie jest taki wielki. Jeśli (tak jak ja) widzisz / odczuwasz ból zrozumienia, debugowania i modyfikowania takich powstających systemów, będziesz chciał zrobić wszystko, co możesz, aby zachowanie było bardziej deterministyczne.

Jak projektować układy OO z właściwą równowagą luźnego sprzężenia i deterministycznego zachowania? Jeśli znasz odpowiedź, Napisz książkę, kupię ją! DCI, Domain-driven design , a ogólniej GOF patterns to początek : -)


  1. http://www.artima.com/articles/dci_vision.html, "gdzie popełniliśmy błąd?". Nie podstawowe źródło, ale zgodne z moim ogólnym zrozumieniem i subiektywne doświadczenie w realizacji założeń.
 1
Author: Woahdae,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-11-11 21:37:17