Những Design Patterns bạn nên biết
Khi xây dựng một ứng dụng web, bạn sẽ thường sử dụng framework hay libraries hỗ trợ. Mặc dù bản thân chúng đã có cấu trúc và các rules rõ ràng nhưng trong nhiều trường hợp bạn vẫn phân vân không biết nên viết code ở đâu hay phải làm sao để có thể tái sử dụng và dễ dàng maintain sau này. Dưới đây sẽ là một số design patterns phổ biến mà bạn nên biết. Trong bài viết mình sử dụng Ruby cho các ví dụ, với những ngôn ngữ khác sẽ có cách thể hiện khác nhưng tư tưởng là không thay đổi.
Service Object
Trong mô hình MVC, controller là trung gian giao tiếp giữa model và view. Vì vậy bản thân nó sẽ chứa nhiều logic, tuy nhiên bạn chỉ nên để controller làm đúng nhiệm vụ của nó là giao tiếp còn những logic khác nên được tách ra một service:
class UsersController
def create
user = User.new(user_params)
if user.save
UserMailer.verify_email(user)
render json: {
status: :success,
user: user.as_json,
}
else
render json: {
status: :error,
errors: user.error.messages
}
end
end
end
Khi sử dụng service:
class CreateUserService
def initialize user_params
@user = User.new(user_params)
end
def execute
if user.save
send_verify_email
{
status: :success,
user: user.as_json,
}
else
{
status: :error,
errors: user.errors.messages,
}
end
end
private
attr_reader :user
def send_verify_email
UserMailer.verify_email(user)
end
end
class UsersController
def create
render json: CreateUserService.new(user_params).execute
end
end
ServiceResponse
Trong một dự án có thể sẽ có rất nhiều service, mỗi service lại trả về các kết quả với những format khác nhau. Đó là lúc bạn nên sử dụng ServiceResponse
để thống nhất dữ liệu trả về của các service. Viết lại ví dụ ở trên với service response:
class ServiceResponse
def self.success(message: nil, payload: {})
{
status: :success,
message: message,
payload: payload,
}
end
def self.error(message: nil, payload: {})
{
status: :error,
message: message,
payload: payload,
}
end
end
class CreateUserService
def initialize user_params
@user = User.new(user_params)
end
def execute
return error_response unless user.save
send_verify_email
success_response
end
private
attr_reader :user
def success_response
ServiceResponse.success(payload: user.as_json}
end
def error_response
ServiceResponse.error(payload: user.errors.messages}
end
def send_verify_email
UserMailer.verify_email(user)
end
end
Finder
Là nơi thao tác trực tiếp với DB. Nó sẽ bao gồm các logic liên quan đến search, filter hay sort dữ liệu. Sử dụng finders là một giải pháp giúp hạn chế fat model và dễ dàng tái sử dụng.
class EmployeesController < ApplicationController
def index
@employess = current_user.employees
@employess = @employees.id_in(params[:id]) if params[:id].present?
end
end
class Admin::EmployeesController < Admin::BaseController
def index
@employess = current_admin.employees
@employees = @employees.by_name(params[:name]) if params[:name].present?
@employees = @employees.where(is_blocked: true) if params[:blocked].present?
end
end
Khi sử dụng finder:
class EmployeesFinder
def initialize(manager, params)
@manager = manager
@params = params
end
def execute
employees = manager.employees
employees = filter_by_id(employees)
employees = filter_by_name(employees)
employees = filter_by_blocked(employees)
order(employees)
end
private
attr_reader :manager, :params
def filter_by_id(employees)
return employees if params[:id].blank?
employees.ransack(id_in: params[:id]).result
end
def filter_by_name(employees)
return employees if params[:name].blank?
employees.ransack(name_cont: params[:name]).result
end
def filter_by_blocked(employees)
return employees unless params[:blocked]
employees.where(is_blocked: true)
end
def order(employees)
return employees unless params[:sort]
employees.ransack(sort: params[:sort]).result
end
end
class EmployeesController < ApplicationController
def index
@employess = EmployeesFinder.new(current_user, params).execute
end
end
class Admin::EmployeesController < Admin::BaseController
def index
@employess = EmployeesFinder.new(current_admin, params).execute
end
end
Decorator
Khi muốn hiển thị các thông tin khác của một record, hoặc đơn giản là format một thông tin có sẵn (ví dụ như các dữ liệu date time), bạn hoàn toàn có thể thêm method mới trong model. Tuy nhiên, điều này sẽ làm model ngày một phình to. Đây là lúc bạn nên sử dụng decorator.
class User < ApplicationRecord
def formatted_created_at
created_at.strftime("YYYY/MM/DD")
end
end
<div class="user">
<p class="user-name"><%= @user.name %></p>
<span class="user-created-at"><%= @user.formatted_created_at %></span>
</div>
Sau khi sử dụng decorator:
class UserDecorator
def formatted_created_at
created_at.strftime("YYYY/MM/DD")
end
end
<div class="user">
<p class="user-name"><%= @user.name %></p>
<span class="user-created-at"><%= @user.decorate.formatted_created_at %></span>
</div>
Model chỉ nên chứa logic liên quan đến các thay đổi trong DB, sử dụng decorator là cách giúp bạn gom những logic không liên quan khác vào một nơi để dễ dàng quản lý.
Presenter
Khác với decorator, presenter được sử dụng để hạn chế logic trong controller và ở ngoài view. Với những controller chứa nhiều logic phức tạp, số lượng các biến instance variable được tạo ra sẽ càng nhiều. Nếu sử dụng chúng ở ngoài view lâu dần sẽ khó quản lý và phát triển sau này. Hãy cùng xem qua ví dụ sau đây:
class PostsController < ApplicationController
before_action :load_post, only: :show
def show
@related_posts = Post.related_posts_for(@post)
@latest_comments = @post.comments.latest.limit(10)
end
def load_post
@post = Post.find params[:id]
end
end
Ở ngoài view:
<h1><%= @post.title %></h1>
<% if current_user == @post.author %>
<button>Edit</button>
<% end %>
<p class="post-content"><%= @post.content %></p>
<div class="comments"><%= render @latest_comments %></div>
<%= render @related_posts %>
Lúc này bạn nên nghĩ đến việc sử dụng presenter:
class PostPresenter
attr_reader :post, :current_user
def initialize current_user, post
@current_user = current_user
@post = post
end
def related_posts
Post.related_posts_for(post)
end
def latest_comments
post.comments.latest.limit(10)
end
def can_edit?
current_user == post.author
end
end
class PostsController < ApplicationController
before_action :load_post, only: :show
def show
@presenter = PostPresenter.new(current_user, @post)
end
def load_post
@post = Post.find params[:id]
end
end
<h1><%= @presenter.post.title %></h1>
<%= content_tag :button, "Edit" if @presenter.can_edit? %>
<p class="post-content"><%= @presenter.post.content %></p>
<div class="comments"><%= render @presenter.latest_comments %></div>
<%= render @presenter.related_posts %>
Như bạn thấy, số lượng instance variable đã giảm, logic ở controller và view cũng đã rõ ràng hơn.
Serializer
Khác với presenter, serializer thường được sử dụng để build response cho API. Hãy cùng xem ví dụ sau để hiểu hơn về nó:
class Api::PostsController < Api::BaseController
before_action :load_post, only: :show
def show
render json: {
posts: @post.as_json(
only: [:id, :title, :content],
include: {
author: {
only: [:id, :name],
methods: [:age]
},
comments: {
only: [:id, :content],
author: {
only: [:id, :name],
methods: [:age]
}
}
}
)
}
end
def load_post
@post = Post.find params[:id]
end
end
Với Ruby, các thư viện hỗ trợ serializer có thể kể đến như jsonapi-serializer và active_model_serializers. Mỗi thư viện đều có ưu và nhược điểm riêng, tuy nhiên cách dùng không có nhiều khác biệt. Ví dụ dưới đây sử dụng Active Model Serializer:
class UserSerializer < ActiveModel::Serializer
attributes :id, :name
attribute :age do
Time.zone.year - object.birthday.year
end
end
class CommentSerializer < ActiveModel::Serializer
attributes :id, :content
belongs_to :author, serializer: UserSerializer
end
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :content
belongs_to :author, serializer: UserSerializer
has_many :comments
end
class Api::PostsController < Api::BaseController
before_action :load_post, only: :show
def show
render json: @post, serializer: PostSerializer
end
def load_post
@post = Post.find params[:id]
end
end
Bạn có thể thấy, sử dụng serializer giúp cho code trở nên rõ ràng hơn, dữ liệu trong response sẽ luôn có một cấu trúc nhất quán. Điều này giúp bạn dễ dàng tạo API document, thuận lợi cho các team phát triển phía client như front-end hay mobile.
Conclusion
Vừa rồi là một số pattern thường được sử dụng trong lập trình web, lựa chọn đúng pattern giúp cho source code bạn rõ ràng thống nhất cũng như dễ dàng maintain và mở rộng sau này.
All rights reserved