1. Value Objects
  2. Wzorzec projektowy: Template Method
  3. Dodatkowe pole form object

1. Value Objects

  1. 7 patterns to refactor fat ActiveRecord models (a jakże 😉 )
  2. Sitepoint: value objects explained with Ruby
  3. Rozdział z książki Rails 4 way
  4. 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 Valuektó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

Dodaj komentarz