+1

Ứng Dụng Nguyên Tắc SOLID Trong Ruby on Rails

Trong phát triển phần mềm, đặc biệt là lập trình hướng đối tượng (OOP), việc viết mã dễ hiểu, dễ bảo trì và dễ mở rộng là điều sống còn. Đây là lý do tại sao nguyên tắc SOLID ra đời.

SOLID là một tập hợp các nguyên tắc thiết kế phần mềm giúp bạn xây dựng các hệ thống dễ quản lý, mở rộng và kiểm thử. Bộ nguyên tắc này được giới thiệu bởi Robert C. Martin (Uncle Bob), và là kim chỉ nam cho lập trình viên chuyên nghiệp.

Trong bài viết này, mình sẽ:

  • Giải thích từng nguyên tắc trong SOLID
  • Cung cấp ví dụ thực tế áp dụng trong Ruby on Rails

SOLID là gì?

SOLID là một tập hợp 5 nguyên tắc quan trọng trong lập trình hướng đối tượng giúp:

  • Code dễ hiểu, dễ mở rộng, dễ kiểm thử
  • Giảm rủi ro bug khi thêm tính năng mới
  • Tăng tuổi thọ phần mềm

SOLID là từ viết tắt của 5 nguyên tắc:

  • S - Single Responsibility Principle (Nguyên tắc Trách nhiệm Duy nhất)
  • O - Open/Closed Principle (Nguyên tắc Mở/Rộng nhưng Đóng/Sửa đổi)
  • L - Liskov Substitution Principle (Nguyên tắc Thay thế Liskov)
  • I - Interface Segregation Principle (Nguyên tắc Tách giao diện)
  • D - Dependency Inversion Principle (Nguyên tắc Đảo ngược Sự phụ thuộc)

1. Single Responsibility Principle (SRP)

Mỗi class chỉ nên có một lý do để thay đổi.

Hãy tưởng tượng bạn có một đầu bếp làm cả: nấu ăn, rửa chén, đi chợ. Nếu có thay đổi gì ở việc đi chợ → bạn phải sửa cả đầu bếp!

Giải pháp: Tách riêng từng vai trò ra.

Anti-pattern:

# app/models/order.rb
class Order < ApplicationRecord
  def calculate_total
    # Tính tổng tiền
  end

  def send_invoice_email
    # Gửi email hoá đơn
  end
end

Ở đây, class Order vừa xử lý nghiệp vụ tính tổng tiền, vừa gửi email => vi phạm SRP.

Refactor theo SRP:

# app/services/order_calculator.rb
class OrderCalculator
  def initialize(order)
    @order = order
  end

  def total
    # Tính tổng tiền
  end
end

# app/mailers/order_mailer.rb
class OrderMailer < ApplicationMailer
  def invoice_email(order)
    # Gửi email hóa đơn
  end
end

→ Mỗi class làm đúng một việc duy nhất, dễ test, dễ thay đổi.

2. Open/Closed Principle (OCP)

Class nên có thể mở rộng được nhưng không phải với việc sửa đổi code cũ.

Khi bạn muốn thêm tính năng mới, bạn nên thêm code, không sửa code cũ. Điều này giúp tránh làm hỏng các logic đang hoạt động tốt.

Anti-pattern:

# app/models/payment.rb
class Payment
  def process(type)
    if type == "paypal"
      process_paypal
    elsif type == "stripe"
      process_stripe
    end
  end
end

→ Cứ mỗi lần thêm phương thức thanh toán mới, ta phải sửa process., dễ gây lỗi.

Refactor theo OCP (dùng Strategy Pattern):

# app/strategies/payment_strategy.rb
class PaymentStrategy
  def process
    raise NotImplementedError
  end
end

class PayPalPayment < PaymentStrategy
  def process
    # Xử lý PayPal
  end
end

class StripePayment < PaymentStrategy
  def process
    # Xử lý Stripe
  end
end

# app/services/payment_processor.rb
class PaymentProcessor
  def initialize(strategy)
    @strategy = strategy
  end

  def process
    @strategy.process
  end
end

→ Giờ chỉ cần tạo class mới, không cần đụng code cũ.

3. Liskov Substitution Principle (LSP)

Subclasses phải có thể thay thế superclass mà không làm sai hành vi chương trình.

Nếu bạn thay thế object của class cha bằng object của class con mà hệ thống vẫn hoạt động bình thường → bạn tuân thủ LSP.

Anti-pattern:

class Bird
  def fly
    # bay
  end
end

class Penguin < Bird
  def fly
    raise "Penguins can't fly!"
  end
end

→ Dùng Penguin thay cho Bird làm chương trình lỗi ngay.

Refactor đúng LSP:

class Bird; end

class FlyingBird < Bird
  def fly
    # bay
  end
end

class Penguin < Bird
  # Không có fly
end

→ Giờ Penguin không bị ép "bay" nữa.

4. Interface Segregation Principle (ISP)

Không nên bắt class phụ thuộc vào những method mà nó không dùng.

Đừng bắt một đối tượng phải "làm mọi thứ", nên tách nhỏ trách nhiệm thành các module riêng biệt.

Ruby không có interface như Java, nhưng có thể áp dụng qua các role/module nhỏ gọn.

Tách interface bằng module:

module Exportable
  def export_csv
    # xuất CSV
  end
end

module Printable
  def print_pdf
    # xuất PDF
  end
end

class Invoice
  include Exportable
end

class Report
  include Printable
end

→ Mỗi class chỉ sử dụng đúng module nó cần.

5. Dependency Inversion Principle (DIP)

High-level module không nên phụ thuộc vào low-level module, cả hai nên phụ thuộc vào abstraction.

Thay vì hard-code việc sử dụng một class cụ thể → hãy inject nó từ bên ngoài, điều này giúp code dễ thay đổi, dễ test (mocking).

Anti-pattern:

class ReportService
  def generate
    pdf = PDFGenerator.new
    pdf.render
  end
end

→ ReportService bị dính chặt với PDFGenerator.

Refactor với Dependency Injection:

class ReportService
  def initialize(generator)
    @generator = generator
  end

  def generate
    @generator.render
  end
end

class PDFGenerator
  def render
    # render PDF
  end
end

class HTMLGenerator
  def render
    # render HTML
  end
end

# Sử dụng:
report = ReportService.new(PDFGenerator.new)
report.generate

→ ReportService giờ có thể dùng bất kỳ định dạng nào.

Tổng kết

Nguyên tắc Ý nghĩa ngắn gọn Mục tiêu
SRP Mỗi class làm một việc Dễ bảo trì
OCP Mở rộng không sửa code cũ Tránh lỗi khi thêm tính năng
LSP Dùng class con thay cho cha không lỗi Tăng tính kế thừa
ISP Chỉ dùng những gì cần Tránh rối code
DIP Phụ thuộc vào abstraction Dễ test, mở rộng linh hoạt

Nguyên tắc SOLID không chỉ là lý thuyết, mà là công cụ mạnh mẽ giúp bạn:

  • Viết code rõ ràng, dễ đọc
  • Dễ dàng mở rộng, bảo trì
  • Tăng tính module hóa và kiểm thử

Trong Rails, SOLID thường được thể hiện qua service objects, form objects, decorators, và concerns, giúp bạn tránh nhồi nhét logic vào model/controller.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.