# 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 ```