Prezentacja z meetupu Uszanowanko Programowanko #3 http://www.uszanowanko.pl/rubyonrails
REvolution - czyli o bardziej obiektowym podejściu w rozwiązaniach kolejowych
Framework Ruby on Rails pozwala na szybkie i stosunkowo łatwe tworzenie aplikacji webowych w języku Ruby. Można powiedzieć, że podejście zwane “The Rails Way” w wielu przypadkach zdało swój egzamin. Szybko jednak okazało się, że to podejście nie sprawdza się w przypadku bardziej złożonych systemów. Logika biznesowa w kontrolerach, wypasione modele, logika w szablonach… ogólnie mówiąc chaos. Potrzebna była (r)ewolucja...
Autor: Tomek Jasiulek
6. ZALETY
• Łatwe w użyciu
• Proste do nauki
• Szybki proces developmentu
• Dużo dostępnej informacji - guides,
stack-overflow, dokumentacja
• Sprawdza się w małych projektach
7. WADY
• Helper’y zaśmiecają globalną przestrzeń
• Przerośnięte modele
• Skomplikowany kod kontrolerów
• Łamanie SRP
• Kod Spaghetti w przypadku bardziej
skomplikowanej logiki biznesowej
• Callback hell
8. FAT MODEL, SKINNY CONTROLLER
• Jeszcze bardziej rozdmuchane modele
• Zgrabniejszy kod kontrolerów
• Przeniesienie problemu w inne miejsce
• No i dalej mamy kłopoty…
14. IDZIEMY NA DIETĘ… A NASZ CELTO:
• Klasy modeli ActiveRecord i tak mają dużą odpowiedzialność - nie dokładajmy im
dodaktowego ciężaru - potrzebna im dieta
• Klasy kontrolerów ActionController powinny być zgrabne, by widoczny był przepływ informacji
od żądania do odpowiedzi serwera - niekoniecznie wszystkie szczegóły logiki biznesowej
• Szablony HTML, ERB, HAML - czyste, przejrzyste, bez zbędnej logiki zaciemniającej strukturę
widoku
• Helpery - w railsach i tak jest ich dużo, nie dokładajmy specyficznych zachowań do globalnej
przestrzeni nazw
15. DODATKOWE SKUTKI DIETY
• Łatwiejsze testowanie aplikacji
• Oprogramowanie bardziej odporne na zmiany
• Kod rozdzielony na mniejsze i prostsze do ogarnięcia klasy o
zawężonej odpowiedzialności (klasy PORO)
16. CZEGO BĘDZIE WYMAGAĆ OD NAS
DIETA?
• Dyscypliny
• Myślenia
• Implementacji dodatkowych klas - w
większości PORO
17. DEKORATOR
• Tak naprawdę od tego wszystko się
zaczyna
• Podstawa wielu innych rozwiązań
• Rózni się tylko intencja programisty -
jego zamiar wobec dekorowanego
obiektu
18. DEKORATOR
• Z wykorzystaniem kompozycji dodajemy, zmieniamy lub rozszerzamy
bieżące zachowanie obiektu z poszanowaniem zasady zamknięcia -
dekorowany obiekt pozostaje nietknięty, a jednocześnie otwarty na
rozszerzenie.
• Proste w implementacji w Ruby ze względu na duck typing
19. PRESENTER
• Pozwalają uporządkować kod
odpowiadający za prezentację obiektu
• Odciążają klasy modelowe z metod
związanych z prezentacją danych w
określonych formatach
• Mniej helperów i metod z
przedrostkami w widokach
20. PRESENTER - KLASA BAZOWA
class BasePresenter
attr_reader :object, :template
alias_method :h, :template
delegate :t, :current_locale, to: :h
def initialize(object, template)
@object = object
@template = template
end
end
21. PRESENTER - HELPER
module ApplicationHelper
def present(object, klass = nil)
klass ||= "#{object.class}Presenter".safe_constantize
return unless klass
presenter = klass.new(object, self)
yield presenter if block_given?
presenter
end
end
22.
23. PRESENTER - JAKO MODUŁ
module TaggablePresenter
def tags_section
return if object_tags.empty?
h.content_tag(:section, tags, class: 'keywords')
end
alias_method :keywords_section, :tags_section
def tags
return if object_tags.empty?
h.capture do
h.content_tag(:span) do
h.concat t('.tags') + ' '
h.concat comma_separated_linked_tags
end
end
end
alias_method :keywords, :tags
private
def object_tags
object.tags_for_locale(current_locale)
end
def linked_tags
object_tags.map do |tag|
h.link_to(tag.name, h.search_path(q: tag.name))
end
end
def comma_separated_linked_tags
separate_by_comma(linked_tags)
end
end
24. PRESENTER - PRZYKŁADOWA KLASA
class EventPresenter < BasePresenter
include TaggablePresenter
alias_method :event, :object
delegate :event_type, :venue, :name, to: :event
def summary
to_safe_html event.summary
end
def start_date(date_format = :medium_datetime)
if date_format.eql?(:date)
h.l event.start_date.to_date
else
h.l event.start_date, format: date_format
end
end
def description
to_safe_html event.description
end
def event_type_logo_url
h.image_url("/events/types/#{event_type_name_underscored}.png")
end
def event_type_name
event_type.name
end
def event_type_name_underscored
event_type_name.downcase.tr(' ', '_')
end
def organizer
return 'USZANOWANKO!!!' if event.organised_by_up?
event.organiser_name
end
def event_coverage
if event.event_coverage.present? && event.event_coverage.published?
event.event_coverage
end
end
private
def to_safe_html(text)
text.to_s.html_safe
end
end
27. PRESENTER - PODSUMOWANIE
Zalety:
• Jasne rozdzielenie logiki prezentacji od samej formy prezentacji(szablonu)
• Przejrzysty szablony(erb, haml itd…)
UWAGA!
• Mieszanie odpowiedzialności - presenter nie powinien tworzyć skomplikowanej
struktury HTML a jedynie formatować dane i przygotowywać do prezentacji w formie
np. HTML
28.
29. SERIALIZER - KREWNIAK PRESENTER’A
GEM: ACTIVE_MODEL_SERIALIZERS
Cechy:
• Obiektowe podeście do serializacji
• Prezentuje obiekt w formacie JSON
• Idea podobna, tylko inny efekt końcowy
Zalety:
• Odciążenie modeli oraz kontrolerów od odpowiedzialności za serializację obiektów do formatu JSON
• Przeniesie tej odpowiedzialności do konkretnych klas serializerów, adekwatnych do przypadków użycia
30. SERVICE OBJECT
• Dedykowane klasy realizujące logikę
biznesową
• Wyciągamy logikę biznesową z
kontrolerów i modeli i enkapsulujemy ją
w postaci dedykowanych klas
• Wykorzystanie kompozycji
• Łatwe wykorzystanie klasy w innym
kontekście np. w tasku rake’owym,
workerze
31. SERVICE OBJECT - PRZYKŁAD
class NewsletterSubscription < ActiveRecord::Base
belongs_to :user
validates :email,
presence: true,
uniqueness: true,
format: Devise.email_regexp
validates :accept_terms,
acceptance: { accept: true }
end
class UserSubscriber
attr_accessor :user
def initialize(user)
@user = user
end
def call
subscription = NewsletterSubscription.new
subscription.user = @user
subscription.email = @user.email
subscription.subscribed = true
subscription.accept_terms = true
subscription.subscribe_date = Time.current
subscription.save!
end
end
32. SERVICE OBJECTS - PODSUMOWANIE
• Pozbywamy się zbędnych callbacków z modeli
• Zwiększamy czytelność kodu kontrolerów, dzięki wyodrębnieniu logiki
biznesowej w osobne miejsce
• Modele nie sprawiają niespodzianek np. w przypadku importu
(wysyłanie maili w callbackach itp.)
33.
34. VALUE OBJECT
• Dedykowany klasa, która enkapsuluje wartość
oraz jej specyficzne zachowanie
• Można powiedzieć, że taki wrapper na
prymitywne wartości lub wartości
kalkulowane dynamicznie
• Nie zmieniają swojego wewnętrznego stanu
• Identyfikowany przez wartość
• Pozwala na porównywanie, sortowanie itp.
• Przykład: Fixnum, Range, Money
35. VALUE OBJECT - PRZYKŁAD
class Ratio
include Comparable
extend Forwardable
def_delegators :value, :zero?
attr_reader :teachers, :students, :value
def self.zero
new(0, 0)
end
def initialize(teachers, students)
@teachers = teachers.to_f
@students = students.to_f
if students == 0
@value = 0.0
else
@value = @teachers/@students
end
end
def <=>(other)
value <=> other.value
end
def to_s
"#{teachers.round}:#{students.round}"
end
end
36. VALUE OBJECT - INNE PRZYKŁADY
• NUMERTELEFONU - możliwość wyodrebnienia numeru
kierunkowego, pełnego numeru w postaci łańcucha tekstowego
• ADRES EMAIL - możliwość wyodrębnienia nazwy użytkownika,
domeny lub pełnego w postaci łańcucha tekstowego
37. VALUE OBJECT - PODSUMOWANIE
• Zachowanie pewnej klasy obiektów w jednym miejscu - a nie
rozsiane po całej aplikacji
• Ewentualne zmiany, rozszerzenia wymagają zmian tylko i wyłącznie w
jednej klasie
• Operujemy jeden poziom wyżej - operowanie na prymitywach niesie
za sobą zagrożenia i ograniczenia
38.
39. ZAGADKA?
class Car < ActiveRecord::Base
enum air_conditioning: {
blank: 0,
manual: 1,
climatronic: 2
}
end
CO ZWRÓCI WYWOŁANIE:
car = Car.new
car.air_conditioning = 'climatronic'
car.present?
42. NA RATUNEK NULL OBJECT
• Alernatywa dla: object.nil?, object.kind_of?, object.present?
• Implementuje interfejs obiektu zastępowanego
• Obiekt zastępczy, który udaje obiekt zastępowany, gdy ten nie
istnieje(np. nie ma go w bazie/repozytorium)
43. NULL OBJECT - PRZYKŁAD
class SidebarPresenter < BasePresenter
def render_items
h.capture do
sidebar_items.each do |sidebar_item|
h.concat render_sidebar_item(sidebar_item)
end
end
end
def sidebar
@sidebar ||= home? ? Sidebar.home : Sidebar.primary
@sidebar ||= Domain::NullSidebar.new
end
private
def sidebar_items
sidebar.sidebar_items.includes(:sidebar_item_type)
end
def render_sidebar_item(item)
presenter = SidebarItems::PresenterFactory.create(item, h)
presenter.render_item
end
def home?
h.controller_name.eql?('home')
end
end
44. NULL OBJECT - PRZYKŁAD
class NullSidebar < NullObject
def sidebar_items
SidebarItem.none
end
end
class NullObject
def method_missing(*args, &block)
self
end
end
najprostsza implementacja(ma kilka wad)
45. NULL OBJECT - PODSUMOWANIE
• Nigdy więcej undefined method nazwa_metody for nil:NilClass
• Można skupić się na zadaniu, zamiast ciągłym sprawdzaniu wartości lub tożsamości
obiektów
UWAGA!
• Należy uważać na trudne do wykrycia błędy, która na pierwszy rzut oka wyglądają jak
normalne działanie programu
48. FORM OBJECT
Sposób na ogarnięcie skomplikowanych formularzy i rozprawienie się z
accepts_nested_attributes_for.W skrócie klasa modelowa, ale nie
dziedzicząca po ActiveRecord::Base(tak, takie klasy też istnieją i mogą
być modelami), której obiekty trzymają stan obiektu formularza i
enkapsulują np. możliwe opcje, walidacje w danym przypadku użycia