0
0
Lập trình
Admin Team
Admin Teamtechmely

Từ Mô Hình Cồng Kềnh đến Mã Sạch: 5 Mẫu Thiết Kế trong Ruby on Rails

Đăng vào 2 tuần trước

• 10 phút đọc

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 Copy
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 Copy
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 Copy
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 Copy
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 Copy
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 Copy
<!-- 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 Copy
# Gemfile
gem 'draper'

Bước 2 – Tạo Decorator

ruby Copy
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 Copy
class UsersController < ApplicationController
  def show
    @user = User.find(params[:id]).decorate
  end
end

Bước 4 – Sử dụng trong View

html Copy
<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 Copy
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 Copy
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 Copy
# 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 Copy
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 Copy
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 config1config2 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 Copy
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 Copy
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 Copy
# 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 Copy
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 Copy
# 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 Copy
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.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào