1. Value Objects
- 7 patterns to refactor fat ActiveRecord models (a jakże 😉 )
- Sitepoint: value objects explained with Ruby
- Rozdział z książki Rails 4 way
- Grok: value objects in Ruby
Kolejny krok z cyklu Xxx Objects, czyli refaktoringu polegającego na wydzielaniu specyficznych rodzajów obiektów, które (konwencyjnie) realizują jakieś założenia.
Attributes equality
Najważniejsze: value objects są to obiekty, których wzajemna relacja równości zależy od ich wartości, a nie identyczności obiektu.
objects whose equality is based on their internal fields rather than their identity
Przez wartości rozumiemy wszystkie atrybuty danego obiektu klasy. Pokażmy to może na przykładzie
class Address attr_reader :street, :number def initialize(street, number) @street = street @number = number end end adr1 = Address.new("dolna", 6) adr2 = Address.new("dolna", 6) adr1 == adr2 => false
Co się stało? Dostajemy false ponieważ stworzone obiekty, mimo faktu, że mają te same wartości atrybutów – są dwoma osobnymi, różnymi obiektami klasy Address
Nie zgadza się to jednak z naszymi założeniami dot. value objects
Aby nasza klasa je spełniała – musimy zdefiniować relację równości w ramach naszej klasy
class Address ... def ==(other) street == other.street && number == other.number end alias :eql? :== end adr1 = Address.new("dolna", 6) adr2 = Address.new("dolna", 6) adr1 == adr2 => true
Super, podstawowe założenie Value objects : spełnione!
Dwa obiekty, reprezentujące te same wartości – są sobie równe
immutable
Kolejnym, bardzo ważnym założeniem, jest niezmienność wartości obiektów
Attributes should be immutable throughout its life cycle
Oznacza to ni mniej ni więcej, że nie powinniśmy mieć możliwości wykonania
adr1.street = "nowa"
W tym momencie, nasza klasa ma zablokowaną taką możliwość (posłużyliśmy się jedynie akcesorem attr_reader). Ciekawe rozwiązanie podpowiada sitepoint – umożliwmy „podstawianie wartości”, ale poprzez tworzenie nowego obiektu naszej klasy
class Address ... def street=(new_street) Address.new(new_street, number) end def number=(new_number) Address.new(street, new_number) end end adr1 = Address.new("dolna", 6) adr2 = (adr1.street="nowa")
Struct.new
Sporym ułatwieniem może okazać się Struct.new, o którym wcześniej wspominaliśmy.
Dziedzicząc ze Struct.new nie musimy ani pisać konstruktora, ani też definiować funkcji porównującej!
class Address < Struct.new(:street, :new) end adr1 = Address.new("dolna", 6) adr2 = Address.new("dolna", 6) adr1 == adr2 => true
Niestety w tym rozwiązaniu aktualny pozostaje problem nadpisywania wartości atrybutów obiektu
Propozycją rozwiązania może być gem Value, który jak sam wskazuje – rozwiązuje dwa problemy:
– Constructors require expected arguments
– Instances are immutable
W ten sposób dziedzicząc po Value.new(:street, :number) zamiast po Struct – nie musimy martwić się nadwyżkowymi akcesorami
Inne korzyści z Value objects?
Muszę przyznać, że z początku nie widziałam „aż takiego” zastosowania obiektów Value objects
Należy jednak pamiętać, że na równości atrybutów – rola Value objects wcale nie musi się kończyć
Fajnym i dającym do myślenia przykładem jest klasa Rating prezentowana w poście na stronie Code Climate
class Rating def self.from_cost(cost) if cost <= 2 new("A") elsif cost <= 4 new("B") elsif cost <= 8 new("C") elsif cost <= 16 new("D") else new("F") end end end
2. Wzorzec projektowy template method
Na podstawie 3go rozdziału Ruby: Wzorce projektowe – Urozmaicanie algorytmów za pomocą wzorca projektowego Template Method
słowo wstępne 😉
Książka, chociaż wydana w 2008, pod niektórymi względami może być uznana za ponadczasową, dlatego myślę, że mimo pozornej nieaktualności warto poświęcić jej odrobinę uwagi.
Wzorzec projektowy – w ogromnym uproszczeniu, są to proponowane rozwiązania jakiegoś popularnego problemu, identyfikacja wspólnych rozwiązań.
Cel – maksymalne uelastycznienie systemów, ograniczenie kosztów wprowadzania potencjalnych zmian
Książka napisana jest w oparciu o tzw. Bandę Czworga (Gang of four) i prezentuje kilkanaście wybranych wzorców projektowych na przykładzie Ruby’ego
Na początku książki znajdziemy 4 podstawowe reguły rządzące wzorcami projektowymi (i ogólnie „dobrym” projektowaniem aplikacji), o których warto wspomnieć
- Oddzielanie elementów podatnych na zmiany od tych niezmiennych
- Programowanie aplikacji pod kątem interfejsu, tzn. operującej na możliwie najogólniejszym (abstrakcyjnym) typie danych
- Posługiwanie się kompozycją zamiast dziedziczeniem (zbyt ścisłe powiązanie, dostęp do wszystkich metod nadklasy)
- Delegacja – przenoszenie odpowiedzialności na inne obiekty
Wzorzec template method
W jakiej sytuacji?
Wzorzec ten przydaje się w sytuacji, kiedy mamy mechanizm, którego działanie jest podobne w różnych przypadkach, jednak na przykład jeden krok w ramach tego mechanizmu jest różny w zależności od przypadku.
„Abstrakcyjna” klasa bazowa
Definiujemy klasę, która będzie stanowiła klasę bazową naszego mechanizmu.
Metoda szkieletowa, szablonowa
W ramach klasy bazowej definiujemy metodę (zwaną szkieletową lub szablonową), która realizuje kolejne kroki naszego mechanizmu
Metody abstrakcyjne, metody zaczepienia
Metody zdefiniowane w ramach klasy bazowej, które są wywoływane w metodzie szkieletowej (szablonowej)
Możemy je definiować z różnym założeniem
– nie określając sposobu ich działania, oczekując, że zostaną osobno „przykryte” w konkretnych przypadkach (w klasie bazowej możemy zgłaszać wyjątki w ramach tych metod, żeby mieć pewność, że zostaną przykryte) => metody abstrakcyjne
– wstępnie określając ich działani (może być „pustym” wywołaniem!), zakładając, że mogą zostać przykryte, ale nie muszą => metody zaczepienia
Podklasy
Żeby dokończyć dzieła musimy w takim razie stworzyć podklasy, czyli te przypadki użycia
Podklasy będą wyglądały fragmentarycznie, bo będą zbierały metody… wydawałoby się nigdzie nie wywołane – ich wywołanie znajduje się w metodzie szablonowej!
Nie przykrywają one metody szablonowej, a jedynie metody abstrakcyjne (obowiązkowo) i metody zaczepienia.
+++
Na koniec warto zwrócić uwagę na dwie rzeczy
- nie należy stosować wzorca template method w przypadku ogromnej liczby niezrozumiałych metod, które będą wymagały przykrycia – staramy się raczej ograniczyć jego stosowanie do mało rozbudowanych mechanizmów
- należy pamiętać o tzw. taktyce ewolucyjnej, czyli założeniu, że najpierw poszukujemy możliwie prostego rozwiązania problemu i dopiero przy dalszym rozwoju aplikacji wprowadzamy rozwiązania takie jak metoda szablonowa – nie komplikujmy prostych sytuacji 🙂
+++
Podsumowanie
Wspominany wzorzec polega na umieszczeniu w klasie bazowej niezmiennych elementów i przenoszeniu tych bardziej wyspecjalizowanych do zaimplementowania w klasach potomnych.
Implementacje mogą być konieczne (musimy przykryć metody niezdefiniowane w klasie bazowej) lub nieobowiązkowe (kiedy metody te są w jakiś sposób zdefiniowane w klasie bazowej).
Efekt?
Przejrzysty kod, elastyczne projekty
3. Dodatkowe pole w form object
Było sporo „teorii”, dlatego na koniec chciałabym podrzucić mały „railsowy trick”, który poznałam stosunkowo niedawno.
Wyobraźmy sobie, że korzystamy z jakiegoś form objecta – formularz zamawiania napojów
#app/forms/baverages_form.rb class BaveragesForm < BaseForm attribute :name, String attribute :count, Integer attribute :alcohol, Boolean, default: false validates :name, presence: true validates :count, presence: true end #app/controllers/order_controller.rb class OrderController < ApplicationController def new @form = BaveragesForm.new end def create @form = BaveragesForm.new(params) if @form.valid? #save order else #render form again end end end #app/views/orders/new.html.erb <%= form_for(@form, url: orders_path) do |f| %> Nazwa: <%= f.text_field :name %> Ilość: <%= f.text_field :count %> Alkohol? <%= f.checkbox :alcohol %> <%= f.submit %> <% end %>
Wydawałoby się, że wszystko wygląda świetnie.
Ale… przydałaby się walidacja pola alcohol w zależności użytkownika, który zamawia!
Jeżeli nie jest zalogowany – zwracamy błąd, że checkbox „alcohol” nie może być zaznaczony. Podobnie – jeżeli jest w wieku < 18 lat (przy założeniu, że w bazie przechowuje informację na temat daty urodzenia zarejestrowanych użytkowników)
Teoretycznie – oba te przypadki możemy obsłużyć „jakoś” w kontrolerze – sprawdzając co przyszło w params i w zależności od usera – zwracać błąd lub dopiero walidować obiekt form
Tak naprawdę jednak – chcielibyśmy dalej trzymać walidację w ramach stworzonego form object (w końcu po to go stworzyliśmy, żeby trzymać tę logikę w jednym miejscu)
class BaveragesForm < BaseForm attribute :name, String attribute :count, Integer attribute :alcohol, Boolean, default: false validates :name, presence: true validates :count, presence: true validate :user_age def user_age return true unless alcohol if !current_user || current_user.date_of_birth > (Date.today - 18.years) errors.add(:alcohol, :not_allowed) end end end
Byłoby idealnie, gdyby… gdyby nie fakt, że z poziomu tego obiektu nie mamy dostępu do current_user!
Nie chcemy robić z tego atrybutu formularza i jakoś go „dopychać”, bo użytkownik sam w sobie wcale nie jest związany z tym formularzem – jest nam „tylko potrzebny do walidacji”
Tu z pomocą przychodzi nasz „chwyt” i metoda .tap
class BaveragesForm < BaseForm attr_accessor :user attribute :name, String attribute :count, Integer attribute :alcohol, Boolean, default: false validates :name, presence: true validates :count, presence: true validate :user_age def user_age return true unless alcohol if !user || user.date_of_birth > (Date.today - 18.years) errors.add(:alcohol, :not_allowed) end end end class OrderController < ApplicationController def new @form = BaveragesForm.new end def create @form = BaveragesForm.new(params).tap do |form| form.user = current_user end if @form.valid? #save order else #render form again end end end
Kluczowe są linijki: 2 oraz 26-28, w których dodajemy do obiektu formularza informację na temat użytkownika oraz akcesor, który pozwala na posłużenie się nią w walidacji.
Pokazany przykład jest być może trywialny, ale pokazuje w jaki sposób (elegancko), można zachować walidację w form object nawet jeżeli jest związana z obiektem nie będącym elementem formualrza