Viết mã sạch hơn, dễ bảo trì với các nguyên tắc DRY, KISS & SOLID

Đang gặp khó khăn với mã lộn xộn, khó hiểu? Hãy học 3 nguyên tắc thiết kế quan trọng (DRY, KISS, SOLID) để viết mã sạch, dễ bảo trì khi bạn bắt đầu lập trình.
Viết mã sạch hơn, dễ bảo trì với các nguyên tắc DRY, KISS & SOLID

Khi bắt đầu học lập trình, điều quan trọng là phải hiểu một số nguyên tắc thiết kế chính sẽ giúp bạn viết mã tốt hơn và sạch hơn. Hãy xem xét ba nguyên tắc quan trọng: DRY, KISS, và SOLID.

DRY (Đừng Lặp Lại Chính Mình - Don't Repeat Yourself)

Tránh sự trùng lặp mã, thúc đẩy mã sạch hơn và dễ bảo trì hơn.

  • Tránh sao chép và dán mã.
  • Nếu bạn thấy mình viết cùng một thứ nhiều lần, hãy tạo một hàm thay thế.
  • Điều này làm cho mã của bạn dễ bảo trì và cập nhật hơn.

Ví dụ xấu (lặp lại mã):

def greet_john
  puts "Hello, John!"
  puts "How are you today?"
end

def greet_mary
  puts "Hello, Mary!"
  puts "How are you today?"
end

Ví dụ tốt hơn (sử dụng một phương thức):

def greet(name)
  puts "Hello, #{name}!"
  puts "How are you today?"
end

greet("John")
greet("Mary")

KISS (Giữ Đơn Giản, Ngốc Nghếch - Keep It Simple, Stupid)

Nhấn mạnh vào sự đơn giản và rõ ràng trong thiết kế mã, làm cho nó dễ hiểu và sửa đổi.

  • Viết mã rõ ràng, dễ hiểu
  • Tránh sự phức tạp không cần thiết
  • Giải pháp đơn giản thường là tốt nhất

Đây là ví dụ với một lỗi có thể được tối ưu hóa theo KISS:

def is_weekend?(day)
  if day == "Saturday" || day == "Sunday"
    puts "It's the weekend!"
    return true
  else
    puts "It's a weekday."
    return false
  end
end

# Usage (assuming today is Wednesday)
day = "Wednesday"
is_weekend?(day)

Mã này hoạt động, nhưng nó hơi dài dòng và lặp lại. Nó in thông điệp trong hàm mà có thể không phải lúc nào cũng mong muốn.

Tối ưu hóa theo KISS:

def is_weekend?(day)
  day == "Saturday" || day == "Sunday"
end

# Usage (assuming today is Wednesday)
day = "Wednesday"
if is_weekend?(day)
  puts "It's the weekend!"
else
  puts "It's a weekday."
end

Đây là cách nó tuân theo KISS:

  • Loại bỏ thông điệp không cần thiết: Hàm không còn in thông điệp, giữ nó tập trung vào logic.
  • Đơn giản hóa logic: Các câu lệnh trả về đã được loại bỏ vì Ruby ngầm trả về biểu thức cuối cùng được đánh giá.
  • Sử dụng sạch hơn: Logic cho việc in thông điệp được tách ra khỏi hàm, làm cho mã dễ đọc và bảo trì hơn.

Mã tối ưu hóa này đạt được cùng một chức năng (kiểm tra xem có phải là cuối tuần không) với ít dòng hơn và tránh sự phức tạp không cần thiết trong hàm.

SOLID

Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion: Một tập hợp năm nguyên tắc giúp mã hướng đối tượng được cấu trúc tốt, dễ bảo trì và linh hoạt.

Nguyên tắc Trách nhiệm Đơn lẻ (Single Responsibility Principle)

Mỗi phần mã của bạn nên làm một việc duy nhất tốt.
Ví dụ xấu (nhiều trách nhiệm):

class User
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def save_to_database
    # Mã để lưu người dùng vào cơ sở dữ liệu
  end

  def generate_report
    # Mã để tạo báo cáo
  end
end

Ví dụ tốt hơn:

class User
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

class UserDatabase
  def save_user(user)
    # Mã để lưu người dùng vào cơ sở dữ liệu
  end
end

class ReportGenerator
  def generate_user_report(user)
    # Mã để tạo báo cáo
  end
end

Nguyên tắc Mở/Đóng (Open-Closed Principle)

Mã của bạn (lớp, mô-đun, hàm, v.v.) nên mở để mở rộng nhưng đóng để sửa đổi. Điều này có nghĩa là bạn nên có thể mở rộng hành vi của một lớp mà không cần thay đổi mã hiện có của nó.

class Shape
  def area
    raise NotImplementedError, "Subclass must implement abstract method"
  end
end

class Rectangle < Shape
  def initialize(width, height)
    @width = width
    @height = height
  end

  def area
    @width * @height
  end
end

class Circle < Shape
  def initialize(radius)
    @radius = radius
  end

  def area
    Math::PI * @radius**2
  end
end
  • Lớp Shape là lớp trừu tượng (abstract) và định nghĩa một giao diện (interface) với phương thức area.
  • Các lớp RectangleCircle mở rộng Shape và triển khai phương thức area của riêng chúng.
  • Các Shape mới có thể được thêm bằng cách tạo các lớp con mới của Shape mà không cần thay đổi mã hiện có.

Cấu trúc này cho phép bạn thêm các Shape mới mà không cần thay đổi các lớp Shape, Rectangle hoặc Circle hiện có.

Nguyên tắc Thay thế của Liskov (Liskov Substitution Principle)

Các lớp con nên có thể thay thế các lớp cha của chúng mà không làm hỏng mã. Nói cách khác, một lớp con nên có thể làm mọi thứ mà lớp cha của nó có thể làm.

class Bird
  def fly
    raise NotImplementedError, "Subclass must implement abstract method"
  end
end

class Sparrow < Bird
  def fly
    puts "Sparrow flying"
  end
end

class Ostrich < Bird
  def fly
    raise "Ostriches can't fly"  # Điều này vi phạm LSP
  end
end

Trong mã đã cho:

  • Lớp Bird định nghĩa một phương thức fly được dự định để các lớp con triển khai.
  • Lớp Sparrow triển khai fly một cách chính xác.
  • Lớp Ostrich vi phạm LSP vì nó không thể thực hiện hành vi fly mà một Bird mong đợi.

Vấn đề ở đây là lớp Bird giả định rằng tất cả các loài chim đều có thể fly, điều này không đúng trong thực tế. Để điều chỉnh mã này theo LSP, chúng ta cần thiết kế lại hệ thống lớp của chúng ta để đại diện tốt hơn cho các khả năng của các loại chim khác nhau.

Đây là cách chúng ta có thể chỉnh sửa mã để tuân thủ LSP:

class Bird
  def move
    raise NotImplementedError, "Subclass must implement abstract method"
  end
end

class FlyingBird < Bird
  def move
    fly
  end

  def fly
    raise NotImplementedError, "Subclass must implement abstract method"
  end
end

class NonFlyingBird < Bird
  def move
    run
  end

  def run
    raise NotImplementedError, "Subclass must implement abstract method"
  end
end

class Sparrow < FlyingBird
  def fly
    puts "Sparrow flying"
  end
end

class Ostrich < NonFlyingBird
  def run
    puts "Ostrich running"
  end
end

Trong phiên bản đã chỉnh sửa này:

  • Chúng ta có một lớp Bird cơ sở với một phương thức move tổng quát.
  • Chúng ta tạo hai lớp con: FlyingBirdNonFlyingBird.
  • FlyingBird triển khai move thành fly, trong khi NonFlyingBird triển khai nó thành run.
  • Sparrow thừa kế từ FlyingBird và triển khai fly.
  • Ostrich thừa kế từ NonFlyingBird và triển khai run.

Bây giờ, hệ thống lớp này tuân theo LSP vì:

  • Tất cả các loài chim đều có thể move, duy trì hợp đồng (contract) của lớp cơ sở.
  • Các loài chim bay triển khai fly, và các loài chim không bay triển khai run.
  • Chúng ta có thể sử dụng bất kỳ lớp con nào của Bird ở nơi mong đợi một Bird, và phương thức move sẽ hoạt động chính xác cho tất cả chúng.

Nguyên tắc Tách biệt Giao diện (Interface Segregation Principle)

Nhiều giao diện cụ thể tốt hơn một giao diện tổng quát.

Tình huống: Hãy tưởng tượng một hệ thống quản lý thư viện với các loại phương tiện khác nhau (sách, sách nói, phim). Chúng ta muốn đại diện cho chúng bằng các lớp và định nghĩa các chức năng để mượn và trả lại các mục.
Không có ISP:

module Borrowable
  def borrow
    raise NotImplementedError, "Classes must implement borrow method"
  end

  def return_item
    raise NotImplementedError, "Classes must implement return_item method"
  end
end

class Book
  include Borrowable

  def borrow
    puts "Borrowing a book"
  end

  def return_item
    puts "Returning a book"
  end
end

class Audiobook
  include Borrowable

  def borrow
    puts "Borrowing an audiobook (download or physical copy)"
  end

  def return_item
    puts "Returning an audiobook"
  end
end

class Movie
  include Borrowable

  def borrow
    puts "Borrowing a movie (rental or streaming)"
  end

  def return_item
    puts "Returning a movie"
  end
end

Mã này hoạt động, nhưng vi phạm ISP vì:

  • Module Borrowable buộc tất cả các lớp phải triển khai return_item, mặc dù phim có thể không có mục vật lý để trả lại (tùy thuộc vào việc thuê hoặc phát trực tuyến).

Với ISP:

module Borrowable
  def borrow
    raise NotImplementedError, "Classes must implement borrow method"
  end
end

module PhysicalItem
  def return_item
    raise NotImplementedError, "Classes must implement return_item method"
  end
end

class Book
  include Borrowable
  include PhysicalItem

  def borrow
    puts "Borrowing a book"
  end

  def return_item
    puts "Returning a book"
  end
end

class Audiobook
  include Borrowable

  def borrow
    puts "Borrowing an audiobook (download or physical copy)"
  end
  # Không có phương thức return_item vì có thể không áp dụng
end

class Movie
  include Borrowable

  def borrow
    puts "Borrowing a movie (rental or streaming)"
  end
  # Không có phương thức return_item vì có thể không áp dụng
end

Cách tiếp cận này tuân theo ISP bằng cách:

  • Module nhỏ, tập trung: Borrowable xử lý việc mượn, và PhysicalItem xử lý việc trả lại mục vật lý (tùy chọn cho một số lớp).
  • Các lớp phụ thuộc vào những gì chúng cần: Book bao gồm cả hai module, trong khi AudiobookMovie chỉ bao gồm Borrowable.

Điều này cải thiện khả năng bảo trì và linh hoạt của mã. Bạn có thể thêm các loại phương tiện mới (ví dụ: sách điện tử) với các chức năng mượn cụ thể mà không cần sửa đổi các lớp hiện có.

Nguyên tắc Đảo ngược Phụ thuộc (Dependency Inversion Principle)

Phụ thuộc vào các trừu tượng, không phải các triển khai cụ thể. Nguyên tắc Đảo ngược Phụ thuộc (DIP) nói rằng các phần cao cấp của mã của bạn không nên phụ thuộc vào các chi tiết cụ thể của các phần cấp thấp hơn. Thay vào đó, cả hai nên phụ thuộc vào một "bản thiết kế" chung (giao diện). Điều này cho phép bạn thay đổi hoặc thay thế các phần cấp thấp mà không ảnh hưởng đến các phần cao cấp, giữ cho mã của bạn linh hoạt và dễ bảo trì.

Tình huống: Hãy tưởng tượng một hệ thống thông báo có thể gửi các loại thông báo khác nhau (email, SMS, thông báo đẩy - push). Chúng ta muốn tách biệt bộ gửi thông báo khỏi loại thông báo cụ thể.
Không có DIP:

class NotificationSender
  def initialize(recipient)
    @recipient = recipient
  end

  def send_email_notification(message)
    puts "Sending email notification to #{@recipient} with message: #{message}"
  end

  def send_sms_notification(message)
    puts "Sending SMS notification to #{@recipient} with message: #{message}"
  end
end

# Sử dụng
sender = NotificationSender.new("[email protected]")
sender.send_email_notification("Welcome message!")

Mã này hoạt động, nhưng vi phạm DIP vì:

  • Lớp NotificationSender bị gắn chặt với các loại thông báo cụ thể (email, SMS). Thêm một loại thông báo mới (ví dụ: đẩy - push) yêu cầu sửa đổi lớp này.

Với DIP:

interface Notification
  def send(recipient, message)
    raise NotImplementedError, "Subclasses must implement send method"
  end
end

class EmailNotification
  include Notification

  def send(recipient, message)
    puts "Sending email notification to #{recipient} with message: #{message}"
  end
end

class SMSNotification
  include Notification

  def send(recipient, message)
    puts "Sending SMS notification to #{recipient} with message: #{message}"
  end
end

# Thêm nhiều lớp thông báo như PushNotification (cấu trúc tương tự)

class NotificationSender
  def initialize(notification)
    @notification = notification
  end

  def send_notification(recipient, message)
    @notification.send(recipient, message)
  end
end

email_notification = EmailNotification.new
sms_notification = SMSNotification.new

sender = NotificationSender.new(email_notification)
sender.send_notification("[email protected]", "Welcome message!")

# Sử dụng bộ gửi với các loại thông báo khác nhau bằng cách inject
sender = NotificationSender.new(sms_notification)
sender.send_notification("+84877698945", "Your order has shipped!")

Cách nó tuân theo DIP:

  • Các module cao cấp (NotificationSender): Phụ thuộc vào các trừu tượng (Notification).
  • Các module cấp thấp (EmailNotification, SMSNotification): Triển khai trừu tượng.
  • Chi tiết (Loại Thông báo Cụ thể): Được inject qua constructor, làm cho sender không phụ thuộc vào các lớp thông báo cụ thể.

Cách tiếp cận này mang lại một số lợi ích:

  • Linh hoạt: Dễ dàng thêm các loại thông báo mới mà không cần sửa đổi NotificationSender.
  • Khả năng kiểm tra: Dễ dàng kiểm tra NotificationSender một cách độc lập bằng cách mô phỏng các triển khai thông báo khác nhau.
  • Khả năng bảo trì: Mã trở nên mô-đun hơn và dễ hiểu hơn.

Bằng cách tuân theo DIP, bạn tạo ra một hệ thống thông báo linh hoạt và dễ bảo trì hơn.

Mối Quan Hệ

  • DRYKISS thường bổ sung cho nhau: Việc loại bỏ sự dư thừa (DRY) thường dẫn đến mã đơn giản hơn (KISS).
  • Nguyên tắc SOLID xây dựng dựa trên DRYKISS: Mã được thiết kế tốt theo SOLID tránh sự trùng lặp và thúc đẩy sự đơn giản.

Ưu Điểm và Nhược Điểm

Ưu Điểm

  • DRY: Giảm lỗi, đơn giản hóa việc bảo trì và cải thiện khả năng đọc mã.
  • KISS: Dễ dàng gỡ lỗi, phát triển nhanh hơn và ít khả năng hiểu sai.
  • SOLID: Thúc đẩy tính mô-đun, khả năng tái sử dụng, gắn kết lỏng lẻo và dễ dàng kiểm tra.

Nhược Điểm

  • DRY: Có thể dẫn đến việc thiết kế quá mức hoặc tạo ra các trừu tượng phức tạp cho các nhiệm vụ rất đơn giản.
  • KISS: Có thể mang tính chủ quan và hạn chế chức năng trong các ứng dụng phức tạp.
  • SOLID: Yêu cầu nhiều kế hoạch và nỗ lực ban đầu hơn so với các phương pháp đơn giản hơn.

Nhớ rằng, những nguyên tắc này là các hướng dẫn, không phải là quy tắc cứng nhắc. Hãy sử dụng sự phán đoán của bạn để tìm ra sự cân bằng phù hợp dựa trên độ phức tạp và yêu cầu của dự án của bạn. Những nguyên tắc này là các hướng dẫn để giúp bạn viết mã tốt hơn. Khi bạn thực hành, chúng sẽ trở nên tự nhiên hơn để áp dụng.