# Rails design parttern
### 1. Service object
#### Mục đích
- Để xử lý logic của hành động.
- Không để models / controllers phình to.
- Không để logic phức tạp vào views.
- Khả năng tái sử dụng tốt hơn, dễ hơn trong việc maintain
#### Cách hoạt động
- Nhận input đầu vào params, tham số ....
- Thực hiển xử lý logic, chức năng.
- Trả về result.
#### Ví dụ
- Tính toán lương
- Import/export dữ liệu
### 2. View object(Presenter)
#### Mục đích
- Đóng gói các logic liên quan tới models & view. Giúp có model/view gọn gàng hơn.
- Dễ dàng viết test hơn vì sử dụng class riêng.
- Hỗ trợ view không dài dòng, phức tạp.
#### Ví dụ
```erb
<p>
User Full Name: <%= "#{user.first_name} #{user.last_name}" %>
<%= link_to "View More Detail", user, class: "w-75 p-3 text-#{user.active? "orange" : "green"} border-#{user.active? ? "orange" : "green"}" %>
</p>
```
Dài dòng ở view, khó đọc.
=> Gói gọn xử lý trong presenter.
```ruby
class UserPresenter
def initialize user
@user = user
end
def full_name
"#{@user.first_name} #{@user.last_name}"
end
def css_color
@user.active? "orange" : "green"
end
end
```
```erb
<% presenter = UserPresenter.new(user) %>
<p>
User Full Name: <%= presenter.full_name %> <%= link_to "View More Detail", user, class: "w-75 p-3 text-#{presenter.css_color} border-#{presenter.css-color}" %>
</p>
```
### 3. Decorator
#### Mục đích
- Cho phép chùng ta thêm các hành động vào 1 đối tượng cụ thể mà không ảnh hưởng tới các đối tượng khác, tránh phình to model.
- Làm sạch logic/code được viết trong views/model/service ...
#### Ví dụ
```ruby
class CandidateDecorator < Draper::Decorator
delegate_all
def full_name
if first_name.blank? && last_name.blank?
"No name is provided"
else
"#{first_name} #{last_name}"
end
end
def link_to_candidate
link_to candidate.full_name, candidate
end
end
```
```ruby
class CandidatesController < ApplicationController
def index
@candidates = Candidate.all.decorate
end
def show
@candidate = Candidate.find(params[:id]).decorate # Chỉ thêm các method vào object
end
end
```
```erb
<p id="candidate-<% @candidate.id %>"> Candidate information </p>
<p>
<strong>Full name:</strong>
<%= @candidate.link_to_candidate %>
</p>
```
### 4. Query object
#### Mục đích
- Tách query logic ra khỏi controller/model/service và có thể tái sử dụng.
- Trả về *ActiveRecord::Relation*
- Có thể dễ dàng được Test một cách độc lập.
- Chỉ đại diện cho truy vấn database, không phải logic hay thao tác nào đó.
#### Ví dụ
```ruby
class UsersListQuery
def self.inactive
User.where(sign_in_count: 0, verified: false)
end
def self.active
User.where(verified: true).where("users.sign_in_count > 0")
end
def self.most_active
# some more complex query
end
end
```
### 5. Value object
#### Mục đích
- Sử dụng với các object nhỏ đơn giản.
- Đại diện cho giá trị nhưng không phải là một cái gì đó duy nhất trong hệ thống của bạn như kiểu user object.
- Chia nhỏ, cô lập các chức năng. => Dễ dàng viết test.
- Loại bỏ lặp code, cải thiện, clean code. Nhóm các hành động liên quan tới 1 loại dữ liệu vào 1 class.
#### Ví Dụ:
- Không thay đổi giá trị gốc.
- Luôn trả về giá trị.
```ruby
class EmailReport
def initialize(emails)
@emails = emails
end
def data
emails_data = []
emails.each do email|
emails_data << {
username: email.match(/([^@]*)/).to_s,
domain: email.split("@"). last
}
end
emails_data
end
private
attr_reader : emails
end
```
=>
```ruby
class Email
def initialize(email)
@email = email
end
def username
email.match(/([^@]*)/).to_s
end
def domain
email.split("a"). last
end
def to_h
{username: username, domain: domain}
end
private
attr_reader : email
end
```
```ruby
class EmailReport
def initialize(emails: emails),
@emails = emails
end
def data
emails.map {|email| Email.new(email).to_h}
end
private
attr_reader: emails
end
```
### 6. Builder
#### Mục đích
- Giúp cho việc xây dựng đối tượng dễ dàng hơn.
- Đóng gói việc xây dựng đối tượng vào 1 class.
#### Ví dụ
```ruby
class Computer
attr_accessor :display
attr_accessor :motherboard
attr_reader :drives
def initialize(display=:crt, motherboard=Motherboard.new, drives=[])
@motherboard = motherboard
@drives = drives
@display = display
end
end
```
```ruby
class ComputerBuilder
attr_reader :computer
def initialize
@computer = Computer.new
end
def turbo(has_turbo_cpu=true)
@computer.motherboard.cpu = TurboCPU.new
end
def display=(display)
@computer.display=display
end
def memory_size=(size_in_mb)
@computer.motherboard.memory_size = size_in_mb
end
def add_cd(writer=false)
@computer.drives << Drive.new(:cd, 760, writer)
end
def add_dvd(writer=false)
@computer.drives << Drive.new(:dvd, 4000, writer)
end
def add_hard_disk(size_in_mb)
@computer.drives << Drive.new(:hard_disk, size_in_mb, true)
end
end
builder = ComputerBuilder.new
builder.turbo
builder.add_cd(true)
builder.add_dvd
builder.add_hard_disk(100000)
```
### 7. Form Object
#### Mục đích
- Validate 1 object có thể không phải là model.
- Giảm tải code cho model/controller, Có thể tái sử dụng
#### Ví dụ
```ruby
class SearchForm
include ActiveModel::Model
attr_accessor :search_params
validates :search_params, presence: true
end
```
Validate search_params luôn có giá trị được gửi từ client.
### 8. Policy object
Mục đích.
- Định nghĩa quyền truy cập tới action trong controller
- Giúp tiếp cận rõ ràng và dễ dàng quản lý các quyền truy cập.
- Tách biệt phần phân quyền với controller. => dễ đọc hiểu dễ bảo trì, mở rộng.
Ví dụ
```ruby
# app/policies/post_policy.rb
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def create?
user.admin? || user.moderator?
end
def update?
user.admin? || (user.moderator? && post.user_id == user.id)
end
def destroy?
user.admin? || (user.moderator? && post.user_id == user.id)
end
end
```
```ruby
class PostsController < ApplicationController
def create
@post = Post.new(post_params)
authorize @post, :create?
if @post.save
# ...
else
# ...
end
end
def update
@post = Post.find(params[:id])
authorize @post, :update?
if @post.update(post_params)
# ...
else
# ...
end
end
def destroy
@post = Post.find(params[:id])
authorize @post, :destroy?
@post.destroy
# ...
end
# ...
end
```
### 9. Observer
Mục đích
- Cho phép 1 đối tượng theo dõi và phản ứng với sự thay đổi của đối tượng khác.
- Trong Rails, mẫu Observer có thể được thực hiện bằng cách sử dụng các callback của Active Record.
Ví dụ
```ruby=
class PostObserver1 < ActiveRecord::Observer
def after_create(post)
# thực hiện một số thứ sau khi bài viết được tạo
end
end
class PostObserver2 < ActiveRecord::Observer
def after_update(post)
# thực hiện một số thứ sau khi bài viết được cập nhật
end
end
```
```ruby=
config.active_record.observers = [:post_observer1, :post_observer2]
```
Khi một đối tượng Post được tạo hoặc cập nhật, Rails sẽ tự động gọi các phương thức của PostObserver1, PostObserver2.
### 10. Interactor
Mục đích
- Tách logic xử lý phức tạp ra khỏi controller và model trong Rails.
Điểm khác so với service
- "Interactor" thường được triển khai bằng cách sử dụng các thư viện hỗ trợ như interactor-rails, organizer,...
- Đặc trưng cho các tác vụ chứa nhiều bước xử lý liên quan đến việc thực hiện một nhiệm vụ hoàn chỉnh, có thể bao gồm nhiều service và thao tác.
Ví dụ
```ruby=
# app/interactors/create_user_interactor.rb
class CreateUserInteractor
include Interactor
def call
user = User.create(context.user_params)
if user.valid?
context.user = user
else
context.fail!(error: user.errors.full_messages)
end
end
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
result = CreateUserInteractor.call(user_params: params[:user])
if result.success?
redirect_to root_path, notice: 'User created successfully'
else
flash.now[:alert] = result.error.join(', ')
render :new
end
end
end
```
### 11. Adapter
Mục đích
- Adapter thường được sử dụng khi bạn cần tương tác với một thư viện hoặc dịch vụ bên ngoài(API ngoài hệ thống) mà có một interface không tương thích với interface của ứng dụng Rails của bạn.
- giúp tách biệt tác vụ tương tác với các dịch vụ bên ngoài và tăng tính mô đun hóa và bảo trì.
```ruby=
# ExternalServiceAdapter.rb
class ExternalServiceAdapter
def initialize(api)
@api = api
end
def fetch_data
response = @api.get('/data')
# Xử lý response theo cách phù hợp với ứng dụng Rails của bạn
end
end
# ExternalServiceClient.rb
class ExternalServiceClient
def get(endpoint)
# Gửi HTTP request đến dịch vụ ngoại vi và nhận response
end
end
# In your Rails controller
class DataController < ApplicationController
def show
external_api = ExternalServiceClient.new
adapter = ExternalServiceAdapter.new(external_api)
data = adapter.fetch_data
render json: data
end
end
```