Giới thiệu
Nếu bạn là một lập trình viên Ruby on Rails, có lẽ bạn đang tìm cách viết mã sạch hơn, thông minh hơn. Rails mang đến cho chúng ta một nền tảng tuyệt vời, nhưng khi ứng dụng phát triển, mọi thứ có thể trở nên lộn xộn nhanh chóng với các mô hình cồng kềnh, controller quá tải và các callback như những quả bom hẹn giờ.
Đó là lý do mà các mẫu thiết kế lại ra đời. Chúng không phải là để thêm độ phức tạp - mà là để mang lại cấu trúc, tính linh hoạt và sự rõ ràng cho mã của bạn.
Điều tốt nhất? Bạn không cần phải học tất cả 20+ mẫu. Trong các dự án Rails thực tế, lập trình viên thường chỉ dựa vào một vài mẫu.
Trong bài viết này, chúng ta sẽ khám phá 5 mẫu thiết kế mà bạn thực sự sẽ sử dụng trong Rails - những mẫu giúp tái cấu trúc các mô hình cồng kềnh, đơn giản hóa các controller và giữ cho ứng dụng của bạn sạch sẽ.
1. Mẫu Chiến Lược (Strategy Pattern)
Vấn Đề
Hãy tưởng tượng bạn đang xây dựng một ứng dụng thương mại điện tử trong Rails. Bạn cần tính toán chi phí vận chuyển khác nhau tùy thuộc vào loại giao hàng:
- Giao tiêu chuẩn → rẻ, chậm
- Giao nhanh → nhanh hơn, đắt hơn
- Giao quốc tế → đắt hơn rất nhiều
Cách làm nhanh nhưng bừa bộn là đặt tất cả logic vào một phương thức:
ruby
def calculate_shipping(order, type)
if type == "standard"
order.weight * 5
elsif type == "express"
order.weight * 10
elsif type == "international"
order.weight * 20
else
0
end
end
Điều này hoạt động… nhưng rất lộn xộn. Mỗi loại giao hàng mới nghĩa là bạn phải chỉnh sửa phương thức này và thêm nhiều điều kiện hơn. Mã của bạn trở nên khó bảo trì và kiểm thử.
Giải Pháp
Mẫu Chiến Lược nói rằng: thay vì nhồi nhét nhiều hành vi vào một phương thức, hãy tách mỗi hành vi thành một lớp riêng. Sau đó, chọn chiến lược phù hợp tại thời điểm chạy.
Hãy nghĩ về nó như việc chọn cách di chuyển: Đi bộ, Lái xe, hay Bay. Thay vì nhồi nhét tất cả quy tắc di chuyển vào một hàm, bạn chỉ cần chọn chiến lược: chiến lược đi bộ, chiến lược lái xe, hoặc chiến lược bay.
Ví Dụ trong Rails
Bước 1 – Tạo các chiến lược
ruby
class StandardShipping
def calculate(order)
order.weight * 5
end
end
class ExpressShipping
def calculate(order)
order.weight * 10
end
end
class InternationalShipping
def calculate(order)
order.weight * 20
end
end
Bước 2 – Tạo một máy tính
ruby
class ShippingCalculator
def initialize(strategy)
@strategy = strategy
end
def calculate(order)
@strategy.calculate(order)
end
end
Bước 3 – Sử dụng trong một controller
ruby
class OrdersController < ApplicationController
def shipping_cost
order = Order.find(params[:id])
strategy = case params[:shipping_type]
when "standard" then StandardShipping.new
when "express" then ExpressShipping.new
when "international" then InternationalShipping.new
else StandardShipping.new
end
cost = ShippingCalculator.new(strategy).calculate(order)
render json: { cost: cost }
end
end
Tại Sao Điều Này Hữu Ích?
- Không có các khối if/else lộn xộn.
- Dễ dàng thêm các loại giao hàng mới (chỉ cần tạo một lớp mới).
- Mỗi chiến lược có thể kiểm thử độc lập.
- Các controller/mô hình của bạn giữ được sự sạch sẽ và tập trung.
2. Mẫu Trang Trí (Decorator Pattern) 🎨
Các ứng dụng Rails thường gặp vấn đề với các mô hình cồng kềnh (quá nhiều logic doanh nghiệp) và các view lộn xộn (đầy logic hiển thị điều kiện).
Ví dụ, giả sử bạn có một mô hình User:
ruby
class User < ApplicationRecord
def full_name
"#{first_name} #{last_name}"
end
def formatted_date_of_joining
date_of_joining.strftime("%B %d, %Y")
end
def display_name
admin? ? "Admin: #{full_name}" : full_name
end
end
Vấn Đề
Nhìn bề ngoài, điều này có vẻ ổn. Nhưng theo thời gian, các mô hình tích lũy logic trình bày (như định dạng tên và ngày), điều này thực sự không thuộc về mô hình. Các view cũng trở nên lộn xộn:
html
<!-- users/show.html.erb -->
<p>Welcome, <%= @user.admin? ? "Admin: #{@user.full_name}" : @user.full_name %></p>
<p>Joined on <%= @user.date_of_joining.strftime("%B %d, %Y") %></p>
Giải Pháp - Mẫu Trang Trí
Mẫu Trang Trí nói rằng: Thay vì nhồi nhét logic trình bày vào mô hình hoặc view, hãy tạo một đối tượng trang trí “bao quanh” mô hình và thêm hành vi bổ sung.
Cách Tạo Một Decorator trong Rails
Bước 1 – Thêm Gem Draper
ruby
# Gemfile
gem 'draper'
Bước 2 – Tạo Decorator
ruby
class UserDecorator < Draper::Decorator
delegate_all # cho phép truy cập tất cả các phương thức của User
def full_name
"#{object.first_name} #{object.last_name}"
end
def display_name
object.admin? ? "Admin: #{full_name}" : full_name
end
def formatted_date_of_joining
object.date_of_joining.strftime("%B %d, %Y")
end
end
Bước 3 – Sử dụng trong Controller
ruby
class UsersController < ApplicationController
def show
@user = User.find(params[:id]).decorate
end
end
Bước 4 – Sử dụng trong View
html
<p>Welcome, <%= @user.display_name %></p>
<p>Joined on <%= @user.formatted_date_of_joining %></p>
3. Mẫu Quan Sát (Observer Pattern)
Mẫu Quan Sát là một mẫu thiết kế hành vi. Khi một đối tượng (gọi là Subject) thay đổi trạng thái của nó, tất cả các đối tượng phụ thuộc (Observers) sẽ tự động được thông báo và cập nhật.
Hãy nghĩ đến YouTube:
- Bạn (người dùng) đăng ký một kênh (mẫu quan sát đang hoạt động!)
- Kênh đó chính là chủ thể.
- Mỗi khi kênh tải lên một video mới, tất cả người đăng ký sẽ tự động nhận thông báo.
Ví Dụ trong Rails
Bước 1 – Chủ thể (Post)
ruby
class Post
attr_reader :title, :observers
def initialize(title)
@title = title
@observers = []
end
# Đăng ký một quan sát viên
def add_observer(observer)
@observers << observer
end
# Thông báo tất cả các quan sát viên khi điều gì đó xảy ra
def publish
puts "Publishing post: #{title}"
@observers.each { |observer| observer.update(self) }
end
end
Bước 2 – Các quan sát viên
ruby
class EmailNotifier
def update(post)
puts "EmailNotifier: A new post titled '#{post.title}' was published!"
end
end
class Logger
def update(post)
puts "Logger: Post '#{post.title}' has been published."
end
end
Bước 3 – Sử dụng mẫu
ruby
# Tạo post
post = Post.new("Observer Pattern in Rails")
# Thêm các quan sát viên
post.add_observer(EmailNotifier.new)
post.add_observer(Logger.new)
# Xuất bản post
post.publish
Kết Quả
plaintext
Publishing post: Observer Pattern in Rails
EmailNotifier: A new post titled 'Observer Pattern in Rails' was published!
Logger: Post 'Observer Pattern in Rails' has been published.
Tại Sao Điều Này Hoạt Động
- Post không cần biết các quan sát viên làm gì - nó chỉ thông báo cho họ.
- Các quan sát viên (EmailNotifier, Logger) tự quản lý logic của riêng họ.
- Thêm các quan sát viên mới rất dễ: chỉ cần
post.add_observer(NewObserver.new).
4. Mẫu Đơn (Singleton Pattern) 🔒
Đôi khi trong một ứng dụng, bạn chỉ muốn có một thể hiện của một lớp.
Ví Dụ
- Bạn không muốn 10 logger khác nhau viết các tệp khác nhau, bạn muốn một logger được sử dụng ở khắp mọi nơi.
- Bạn không muốn nhiều trình tải cấu hình - chỉ cần một quản lý cấu hình cho toàn bộ ứng dụng.
- Bạn không muốn các đối tượng cache trùng lặp - một cache nên được chia sẻ.
Mẫu Đơn đảm bảo:
- Chỉ một thể hiện của một lớp có thể tồn tại.
- Bạn có thể truy cập thể hiện đó toàn cầu trong ứng dụng của bạn.
Ví Dụ trong Ruby
ruby
require 'singleton'
class AppConfig
include Singleton
attr_accessor :settings
def initialize
@settings = { app_name: "MyApp", version: "1.0" }
end
end
# Sử dụng
config1 = AppConfig.instance
config2 = AppConfig.instance
config1.settings[:app_name] = "MySuperApp"
puts config2.settings[:app_name]
# => "MySuperApp" (cùng một thể hiện!)
Lưu Ý
Nhận thấy rằng config1 và config2 là cùng một đối tượng. Nếu bạn cố gắng AppConfig.new, Ruby sẽ ném ra lỗi - bạn phải sử dụng .instance.
Cách Sử Dụng Singleton trong Rails
Ví Dụ 1: Logger của Rails
Rails đã sử dụng Singleton!
ruby
Rails.logger.info "User signed up"
Không quan trọng bạn gọi Rails.logger ở đâu, đó là cùng một thể hiện logger. Hãy tưởng tượng sự hỗn loạn nếu mỗi controller tạo một tệp logger riêng.
Ví Dụ 2: Cache Store
ruby
Rails.cache.write("foo", "bar")
Rails.cache.read("foo") # => "bar"
Rails.cache là một Singleton. Cùng một cache ở khắp mọi nơi trong ứng dụng.
Ví Dụ 3: Cấu Hình Toàn Cầu
Bạn có thể tạo Singleton của riêng bạn cho các cài đặt toàn ứng dụng:
ruby
# app/services/global_config.rb
require 'singleton'
class GlobalConfig
include Singleton
def db_connection_string
ENV["DB_CONNECTION"]
end
end
# sử dụng ở bất kỳ đâu
GlobalConfig.instance.db_connection_string
5. Mẫu Mặt Phẳng (Facade Pattern)
Hãy tưởng tượng bạn đang xây dựng một cổng việc làm (giống như Naukri/LinkedIn).
Khi một nhà tuyển dụng đăng một công việc mới, nhiều việc cần phải xảy ra:
- Lưu công việc vào cơ sở dữ liệu.
- Thông báo cho các ứng viên đã đăng ký.
- Gửi email xác nhận cho nhà tuyển dụng.
- Cập nhật hoạt động để báo cáo.
- Đưa công việc vào các nền tảng bên thứ ba.
Nếu bạn nhồi nhét tất cả điều này vào JobsController, nó có thể trông như thế này:
ruby
class JobsController < ApplicationController
def create
@job = Job.new(job_params)
if @job.save
CandidateNotifier.notify(@job)
RecruiterMailer.job_posted(@job).deliver_later
Activity.track("job_posted", @job.id)
ThirdPartyPoster.push(@job)
redirect_to @job, notice: "Job posted successfully!"
else
render :new
end
end
end
Vấn Đề
- Controller đang làm quá nhiều.
- Khó kiểm thử (bạn sẽ cần stub email, hoạt động, bên thứ ba, v.v. mỗi lần).
- Nếu yêu cầu thay đổi (ví dụ: cũng đăng lên Slack), bạn sẽ phải cập nhật controller này, làm cho nó càng lộn xộn hơn.
Giải Pháp: Mẫu Mặt Phẳng
Thay vì để controller biết tất cả các chi tiết, chúng ta tạo một lớp Facade ẩn đi sự phức tạp.
Bước 1: Tạo một Facade
ruby
# app/facades/job_posting_facade.rb
class JobPostingFacade
def self.post_job(job_params)
job = Job.new(job_params)
return nil unless job.save
CandidateNotifier.notify(job)
RecruiterMailer.job_posted(job).deliver_later
Activity.track("job_posted", job.id)
ThirdPartyPoster.push(job)
job
end
end
Bước 2: Sử dụng Facade trong Controller
ruby
class JobsController < ApplicationController
def create
@job = JobPostingFacade.post_job(job_params)
if @job
redirect_to @job, notice: "Job posted successfully!"
else
render :new
end
end
end
Tại Sao Điều Này Tốt Hơn
- Controller sạch sẽ: nó chỉ biết "đăng một công việc".
- Quy trình đăng ký được tập trung: tất cả các bước sống trong JobPostingFacade.
- Nếu logic doanh nghiệp thay đổi (ví dụ: không còn đăng bên thứ ba), bạn chỉ cần chỉnh sửa facade.
- Dễ dàng kiểm thử: bạn có thể kiểm thử JobPostingFacade một cách riêng biệt.
Tóm Tắt / Những Điều Quan Trọng
- Mẫu Chiến Lược → Lựa chọn các hành vi khác nhau mà không cần if-else lộn xộn.
- Mẫu Trang Trí → Thêm các tính năng bổ sung cho đối tượng mà không thay đổi mã gốc.
- Mẫu Quan Sát → Tự động thông báo cho nhiều phần của ứng dụng khi có điều gì đó xảy ra.
- Mẫu Đơn → Đảm bảo chỉ có một thể hiện của một tài nguyên tồn tại.
- Mẫu Mặt Phẳng → Đơn giản hóa các quy trình phức tạp phía sau một giao diện sạch sẽ.
Kết Luận ✨
Các mẫu thiết kế không chỉ là lý thuyết - chúng là những công cụ thực tiễn để làm cho các ứng dụng Rails trở nên dễ bảo trì, có thể kiểm thử và sạch sẽ hơn.
Lần tới khi ứng dụng Rails của bạn cảm thấy lộn xộn, hãy tự hỏi:
- Có một chiến lược nào đang ẩn náu trong tất cả các khối if/else này không?
- Có phải các mô hình đang làm quá nhiều công việc trình bày không?
- Có nhiều phần của ứng dụng tôi đang phản ứng với cùng một thay đổi không?
- Tôi có cần một tài nguyên chia sẻ duy nhất không?
- Tôi có đang lặp lại cùng một quy trình trong các controller không?
Hãy tái cấu trúc với 5 mẫu thiết kế này, và mã của bạn sẽ cảm ơn bạn.