Ruby on Rails vs Node JS – pros and cons
If we want to create an application or other web solution, we have a wide range of possibilities when it comes to the available technology. Over the […]
Kurs Ruby on Rails – Lekcja 7 – Warstwy abstrakcji
Warstwa abstrakcji jest to zestaw funkcji, które pomagają nam ukryć złożoność danego procesu.
Wyobraźmy sobie, że tworzymy aplikację dla biblioteki. Zaczynamy kodować funkcjonalność dodawania książek, ale szybko okazuję się, że przy jej dodawaniu chcemy mieć więcej funkcji, np. dodawanie autora, jeśli podany nie istnieje.
Więc zamiast tworzyć rozbudowane warunki w kontrolerze, możemy całą tą funkcjonalność zenkapsulować w warstwie abstrakcji.
W takim wypadku nasz kontroler zamiast wyglądać tak:
class BooksController < ApplicationController
def create
author = Author.find_by(name: author_params[:name])
if author
book_params.merge(author_id: author.id)
else
new_author = Author.create(author_params)
book_params.merge(author_id: new_author.id)
end
Book.create(book_params)
end
end
Będzie wyglądał po wywołaniu w nim funkcjonalności, która nam to wszystko ukryje, tak:
class BooksController < ApplicationController
def create
CreateBook.new(params).call
end
end
gdzie wywołujemy odpowiednią warstwę abstrakcji, dzieląc wszystko na mniejsze części. Dodatkowo, jeśli byśmy musieli wykorzystać tą samą funkcjonalność w innym miejscu w naszej aplikacji, wystarczy wtedy wykorzystać ten sam element. Sprzyja to regule DRY (Don’t Repeat Yourself), która to wspomaga ulepszyć naszą aplikację oraz zmniejszyć ilość miejsc, w których mogą wystąpić błędy.
Dzięki temu sprawiamy, że:
Dla naszej aplikacji Railsowej możemy wyróżnić następujące warstwy abstrakcji:
Service Objects
Query Objects
Command Objects
Presenters/Decorators
Wszystkie z nich możemy określić jako PORO (Plain Old Ruby Object), czyli obiekty służące pojedynczym akcjom wymaganym przez domenę biznesową.
Service Object
ma za zadanie wykonanie akcji logiki biznesowej (np. wysłanie maila), bądź obliczeń, które zostaną wykorzystane w innych częściach aplikacji.
Query Object
jest to obiekt, którego zadaniem jest wysyłanie zapytań (z ang. query) do bazy danych.
Command Object
, służy do zapisu/aktualizacji obiektów w bazie danych.
Prezentery/Dekoratory
służą układaniu dostarczonych danych w odpowiednią formę jaką chcemy wysłać do innej aplikacji, bądź pokazać użytkownikowi.
Zacznijmy od stworzenia Service Object
, który posłuży nam w sortowaniu postów po dacie ich aktualizacji.
Zaczynamy od utworzenia nowego folderu services
w folderze app
. Następnie w naszym nowym folderze utworzymy sobie kolejny folder o nazwie posts
. Tworzymy go w celu uporządkowania naszych funkcjonalności w kontekście w jakim chcemy je wykorzystywać. Teraz w naszym folderze posts
tworzymy plik sort.rb
.
Czyli już z samych nazw obiektów możemy się domyśleć, że app/services/posts/sort.rb
będzie Service Objectem odpowiadającym za sortowanie postów.
Przejdźmy więc do samego pliku i jego struktury.
class Posts::Sort
def initialize(posts)
@posts = posts
end
def call
sort
end
private
def sort
@posts.order(:updated_at)
end
end
Teraz wytłumaczmy sobie po kolei co tutaj się dzieje. Przypominam, że w języku Ruby wszystko traktujemy jako obiekt. I z tego powodu dodaliśmy metodę initialize. Dzięki tej metodzie będziemy mogli przy tworzeniu obiektu przekazać wymagane przez nas parametry Posts::Sort.new(Post.all)
. Następnie w tej metodzie przypisujemy nasze argumenty do zmiennych instancji (te z @), byśmy mogli z nich korzystać w całej naszej klasie.
Metoda call
ma na celu wywołanie naszej wyabstrahowanej akcji (tej którą wrzucamy do naszego serwisu). Z założenia chcemy udostępniać na zewnątrz tej warstwy abstrakcji tylko możliwość wywołania całej akcji, bez dostępu do poszczególnych metod. Dlatego w metodzie call
wywołujemy metodę prywatną sort
.
Czyli wykorzystanie tego serwisu będzie wyglądało następująco Posts::Sort.new(Post.all).call
.
Teraz wyobraźmy sobie, że chcemy wyświetlać posortowane posty, ale tylko z ostatnich 30 dni. W takim przypadku przyda nam się Query Object
, który będzie nam wyciągać tylko takie posty.
Podobnie jak w przypadku Service Objectu
, musimy stworzyć odpowiednie foldery. Stwórzmy więc plik w ścieżce app/queries/posts/recent.rb
class Posts::Recent
RECENT_DAYS = 30
def call
recent_posts
end
private
def recent_posts
Post.where('updated_at > ?', Time.now - RECENT_DAYS.days)
end
end
W tym wypadku, nie potrzebujemy przekazywać żadnych parametrów, więc metoda initialize
jest nam zbędna. Kolejną większą różnicą, w porównaniu z poprzednim serwisem jest stała RECENT_DAYS
. Umiejscowiliśmy tę wartość w stałej oraz na samej górze pliku, by łatwo było ją znaleźć i zmodyfikować.
W tym wypadku również jeżeli nie potrzebujemy metody initialize, możemy metodę call
oraz recent_posts
zmienić w metody klasy, dzięki czemu nie będziemy musieli inicjalizować nowego obiektu.
class Posts::Recent
RECENT_DAYS = 30
def self.call
recent_posts
end
private
def self.recent_posts
Post.where('updated_at > ?', Time.now - RECENT_DAYS.days)
end
end
Teraz wywołanie tego pliku będzie wyglądało następująco Posts::Recent.call
.
Teraz wyobraźmy sobie, że przy okazji tworzenia postu chcemy automatycznie utworzyć dla niego generyczny komentarz.
Stwórzmy plik w ścieżce app/commands/posts/create.rb
.
class Posts::Create
def initialize(params)
@params = params
end
def call
create_post_with_comment
end
private
def create_post_with_comment
post = Post.create(@params)
Comment.create(post_id: post.id, user_id: post.user_id, content: 'Generyczny komentarz')
end
end
Aby znaleźć zastosowanie dla prezentera w naszej aplikacji, załóżmy, że chcemy by tytuł naszego postu był zawsze wyświetlany wielkimi literami.
Tworzymy plik w ścieżce app/presenters/post_presenter.rb
.
Tutaj już bez folderu dla post, ponieważ nie mamy mniejszego kontekstu dla naszego prezentera.
Możemy tutaj stworzyć klasę podobną jak przy poprzednich obiektach
class PostPresenter
def initialize(post)
@post = post
end
def title
@post.title.upcase
end
end
ale możemy wykorzystać rozwiązanie, które dostarcza nam język Ruby, a mianowicie SimpleDelegator
.
SimpleDelegator
przekaże wszystkie metody czy atrybuty do konstruktora naszej klasy (metody initialize).
class PostPresenter < SimpleDelegator
def parsed_title
title.upcase
end
end
Zmieniliśmy tylko nazwę metody, by nie nadpisywać title
, ponieważ dzięki wykorzystaniu SimpleDelegator
wszystkie metody, których nie zdefinowaliśmy w pliku zostaną wydelegowane do obiektu z metody initialize
.
I taki prezenter wykorzystamy w następujący sposób.
Post.new(Post.last).parsed_title
Przejdźmy do naszego PostController
i wykorzystajmy nasze obiekty w odpowiednich miejscach.
class PostsController < ApplicationController
def index
@posts = Posts::Sort.new(Posts::Recent.call).call
end
...
def create
Posts::Create.new(post_params).call
redirect_to posts_path
end
...
end
Jak możemy zauważyć, nasz command zmniejszył ilość wykorzystanych linijek, lecz przy okazji zabrał nam możliwość wyświetlania błędów oraz przekierowywania do formularza w przypadku ich wystąpienia. Naprawmy to korzystając z obsługi wyjątków.
class Posts::Create
def initialize(params)
@params = params
end
def call
create_post_with_comment
end
private
def create_post_with_comment
post = Post.create!(@params)
Comment.create(post_id: post.id, user_id: post.user_id, content: 'Generyczny komentarz')
end
end
Musimy dodać do metody create dla postu !
, by wymuszały zwrócenie wyjątku. Teraz pozostaje nam go tylko złapać.
class PostsController < ApplicationController
def index
@posts = Posts::Sort.new(Posts::Recent.call).call
end
...
def create
Posts::Create.new(post_params).call
redirect_to posts_path
rescue ActiveRecord::RecordInvalid => invalid
@post = invalid.record
render :new
end
...
end
I może w tym wypadku nie skróciliśmy naszej akcji create
w znaczny sposób, ale gdy będziemy chcieli ją rozbudować to pracujemy wtedy tylko na naszym commandzie.
!!! (Aby poprawnie utworzyć obiekt klasy Post
musimy wcześniej utworzyć sobie chociaż jednego użytkownika) !!!
Pozostaje nam teraz tylko wykorzystanie prezentera. Przejdźmy do app/views/posts/show.html.erb
i go tam użyjmy.
<% presenter = PostPresenter.new(@post) %>
Title: <%= presenter.parsed_title %>
Body: <%= presenter.body %>
app/views/posts/show.html.erb
posortowane komentarze dla danego postu.https://ruby-doc.org/stdlib2.5.1/libdoc/delegate/rdoc/SimpleDelegator.html
https://api.rubyonrails.org/classes/ActiveRecord/RecordInvalid.html
https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-create-21
Przeczytaj również o...
If we want to create an application or other web solution, we have a wide range of possibilities when it comes to the available technology. Over the […]
Managing human resources gets more complicated, especially when new people join your company and even routine tasks become a challenge. If you want to be efficient, maintain […]