+4

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.

image.png

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-serializeractive_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.

Blog: https://dongoclam.github.io


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí