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
Shapelà lớp trừu tượng (abstract) và định nghĩa một giao diện (interface) với phương thứcarea. - Các lớp
RectanglevàCirclemở rộngShapevà triển khai phương thứcareacủ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
Shapemà 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ứcflyđược dự định để các lớp con triển khai. - Lớp
Sparrowtriển khaiflymột cách chính xác. - Lớp
Ostrichvi phạm LSP vì nó không thể thực hiện hành viflymà mộtBirdmong đợ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
Birdcơ sở với một phương thứcmovetổng quát. - Chúng ta tạo hai lớp con:
FlyingBirdvàNonFlyingBird. FlyingBirdtriển khaimovethànhfly, trong khiNonFlyingBirdtriển khai nó thànhrun.Sparrowthừa kế từFlyingBirdvà triển khaifly.Ostrichthừa kế từNonFlyingBirdvà triển khairun.
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 khairun. - Chúng ta có thể sử dụng bất kỳ lớp con nào của
Birdở nơi mong đợi mộtBird, và phương thứcmovesẽ 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
Borrowablebuộc tất cả các lớp phải triển khaireturn_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:
Borrowablexử lý việc mượn, vàPhysicalItemxử 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:
Bookbao gồm cả hai module, trong khiAudiobookvàMoviechỉ bao gồmBorrowable.
Đ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
NotificationSenderbị 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
NotificationSendermộ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ệ
- DRY và KISS 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 DRY và KISS: 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.
