REVOLUTION OBIEKTOWE PODEJŚCIE DO ROZWIĄZAŃ KOLEJOWYCH Tomek Jasiulek Twitter: @TomaszJasiulek Github: tomasz-jasiulek
Jul 20, 2015
REVOLUTIONOBIEKTOWE PODEJŚCIE
DO ROZWIĄZAŃ KOLEJOWYCH
Tomek Jasiulek
Twitter : @TomaszJasiulekGithub: tomasz-jasiulek
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
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
FAT MODEL, SKINNY CONTROLLER
• Jeszcze bardziej rozdmuchane modele
• Zgrabniejszy kod kontrolerów
• Przeniesienie problemu w inne miejsce
• No i dalej mamy kłopoty…
OOP NA RATUNEK• SINGLE RESPONSIBILITY
• OPEN-CLOSE
• LISKOV SUBSTITUTION
• INTERFACE SEGREGATION
• DEPENDENCY INVERSION PRINCIPLE
• LAW OF DEMETER
• INHERITANCE
• COMPOSITION
• DELEGATION
IDZIEMY NA DIETĘ… A NASZ CEL TO:• 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
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)
CZEGO BĘDZIE WYMAGAĆ OD NAS DIETA?
• Dyscypliny
• Myślenia
• Implementacji dodatkowych klas - w większości PORO
DEKORATOR
• Tak naprawdę od tego wszystko się zaczyna
• Podstawa wielu innych rozwiązań
• Rózni się tylko intencja programisty - jego zamiar wobec dekorowanego obiektu
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
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
PRESENTER - KLASA BAZOWAclass BasePresenter attr_reader :object, :template
alias_method :h, :template
delegate :t, :current_locale, to: :h
def initialize(object, template) @object = object @template = template endend
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 endend
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) endend
PRESENTER - PRZYKŁADOWA KLASAclass 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 endend
PRESENTER - SZABLON HTML- present event do |presenter| .header = image_tag(presenter.event_type_logo_url, alt: presenter.event_type_name) %h3= link_to presenter.name, event %span.date= presenter.start_date .content %p= presenter.summary .details %p = t '.venue' %strong= presenter.venue %br = t '.organiser' %strong = presenter.organizer %span.more = link_to t('.details'), event
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
SERIALIZER - KREWNIAK PRESENTER’AGEM: 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
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
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! endend
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.)
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
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}" endend
VALUE OBJECT - INNE PRZYKŁADY
• NUMER TELEFONU - 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
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
ZAGADKA?
class Car < ActiveRecord::Base enum air_conditioning: { blank: 0, manual: 1, climatronic: 2 }end
CO ZWRÓCI WYWOŁANIE:
car = Car.newcar.air_conditioning = 'climatronic'car.present?
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)
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') endend
NULL OBJECT - PRZYKŁAD
class NullSidebar < NullObject def sidebar_items SidebarItem.none endend
class NullObject def method_missing(*args, &block) self endend
najprostsza implementacja(ma kilka wad)
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
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
QUERY OBJECT
Sposób na łańcuszek wywołań na relacjach ActiveRecord oraz na inne skomplikowane zapytania do bazy danych.
QUERY OBJECT
@news = News.published .with_translations(locale) .not_promoted .visible .by_position .with_tile .merge(Tile.with_translations(locale)) .limit(limit)
QUERY OBJECT
Zasada jest prosta - wyciągamy skomplikowane zapytanie do osobnej klasy PORO a wynik wywołania udostępniamy na zewnątrz
FAT MODEL SKINNY CONTROLLER?NIE!
KAŻDA KLASA POWINNA SIĘ ZDROWO ODŻYWIAĆ I BYĆ SKINNY
przynajmniej w założeniu… ;)
POMYŚLMY O PROGRAMISTACH, KTÓRZY WEJDĄ PO NAS W NASZ KOD
Zróbmy wszystko, aby liczba przekleństw dążyła do zera ;)
A POWSTANIE LEKKOSTRAWNE I SMACZNE DANIE*
*zakładając że logika biznesowa projektu nie jest zagmatwana ;)