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.
Tránh sự trùng lặp mã, thúc đẩy mã sạch hơn và dễ bảo trì 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")
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.
Đâ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:
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.
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.
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
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
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
.Rectangle
và Circle
mở rộng Shape
và triển khai phương thức area
của riêng chúng.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ó.
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:
Bird
định nghĩa một phương thức fly
được dự định để các lớp con triển khai.Sparrow
triển khai fly
một cách chính xác.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:
Bird
cơ sở với một phương thức move
tổng quát.FlyingBird
và NonFlyingBird
.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ì:
move
, duy trì hợp đồng (contract) của lớp cơ sở.fly
, và các loài chim không bay triển khai run
.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.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ì:
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:
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).Book
bao gồm cả hai module, trong khi Audiobook
và Movie
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ó.
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ì:
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:
NotificationSender
): Phụ thuộc vào các trừu tượng (Notification
).EmailNotification
, SMSNotification
): Triển khai trừu tượng.Cách tiếp cận này mang lại một số lợi ích:
NotificationSender
.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.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.
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.