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 như thế nào để có thể tái sử dụng và dễ dàng maintain sau này. Vì vậ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 chúng ta chỉ nên để controller chỉ nên 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_creating unlees user.save send_verify_email success end private attr_reader :user def success ServiceResponse.success(payload: user.as_json} end def error_creating 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 ORM. Nó bao gồm các logic liên quan đến tìm kiếm, filter 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.in_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à một format khác của một thông tin có sẵn ví dụ như các trường date time, bạn hoàn toàn có thể thêm một method vào trong model tương ứng. Tuy nhiên, điều này sẽ làm model của bạn 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>
Presenter
Khác với decorator, presenter được sử dụng để hạn chế logic trong controller hoặc ở ngoài view. 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 %>
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. Đó cũng là lúc bạn nên 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 rất nhiều.
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
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 hiên cách dùng không quá 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
Như bạn có thể thấy, sử dụng serializer giúp cho code trở nên rõ ràng hơn, tăng khả năng tái sử dụng và dễ dàng mở rộng.
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.