Trong bài viết này, bạn sẽ khám phá các nguyên tắc SOLID của lập trình hướng đối tượng, được áp dụng trong một hệ thống đặt đồ ăn trực tuyến ngoài đời thực. Mỗi nguyên tắc sẽ đi kèm với giải thích rõ ràng, lý do tại sao nó quan trọng, và ví dụ thực tế bằng Java.
SOLID là gì?
SOLID là từ viết tắt của 5 nguyên tắc thiết kế trong lập trình hướng đối tượng:
- S: Single Responsibility Principle (Nguyên tắc trách nhiệm đơn)
- O: Open/Closed Principle (Nguyên tắc mở rộng/đóng gói)
- L: Liskov Substitution Principle (Nguyên tắc thay thế Liskov)
- I: Interface Segregation Principle (Nguyên tắc phân tách giao diện)
- D: Dependency Inversion Principle (Nguyên tắc đảo ngược phụ thuộc)
Hãy cùng đi qua từng nguyên tắc với ví dụ bằng Java 👇
Nguyên tắc Trách nhiệm Đơn (Single Responsibility Principle - SRP)
Nguyên tắc này nói rằng một thành phần nên chỉ có một mục đích hoặc trách nhiệm rõ ràng. Nó nên tập trung vào một chức năng hoặc hành vi cụ thể và tránh làm nhiều nhiệm vụ không liên quan.
❌ Ví dụ chưa đúng:
public class OrderService { public void placeOrder() { /* ... */ } public void generateInvoice() { /* ... */ } public void sendConfirmationEmail() { /* ... */ }
}
Lớp này làm quá nhiều việc: đặt hàng, xuất hóa đơn và gửi email thông báo.
✅ Nên sửa lại thế này:
public class OrderService { public void placeOrder() { /* ... */ }
} public class InvoiceService { public void generateInvoice() { /* ... */ }
} public class NotificationService { public void sendConfirmationEmail() { /* ... */ }
}
Giờ đây, mỗi lớp có một trách nhiệm rõ ràng và cụ thể.
Nguyên tắc Mở rộng/Đóng gói (Open/Closed Principle - OCP)
Nguyên tắc này khuyến khích các thành phần có thể mở rộng (thêm hành vi mới) nhưng đóng với việc sửa đổi (không cần sửa mã cũ). Điều này giúp hệ thống dễ bảo trì và phát triển.
Giả sử bạn muốn hỗ trợ nhiều phương thức thanh toán.
❌ Ví dụ chưa đúng:
public class PaymentService { public void pay(String method) { if (method.equals("CARD")) { // card payment logic } else if (method.equals("UPI")) { // UPI payment logic } }
}
Nếu muốn thêm phương thức mới (ví dụ: ví điện tử), phải sửa lại lớp này.
✅ Nên sửa lại thế này:
Sử dụng tính đa hình để mở rộng:
public interface PaymentMethod { void pay(double amount);
} public class CardPayment implements PaymentMethod { public void pay(double amount) { // card payment logic }
} public class UpiPayment implements PaymentMethod { public void pay(double amount) { // UPI payment logic }
} public class PaymentService { public void processPayment(PaymentMethod paymentMethod, double amount) { paymentMethod.pay(amount); }
}
Bây giờ, bạn có thể thêm phương thức thanh toán mới mà không cần sửa lớp PaymentService
.
Nguyên tắc Thay thế Liskov (Liskov Substitution Principle - LSP)
Một lớp con phải có thể thay thế lớp cha mà không gây lỗi hoặc hành vi không mong đợi.
❌ Ví dụ chưa đúng:
public class DeliveryVehicle { public void startEngine() { /* ... */ }
} public class Bicycle extends DeliveryVehicle { public void startEngine() { throw new UnsupportedOperationException("Bicycles don't have engines"); }
}
Lớp Bicycle
không thực hiện đúng hợp đồng của DeliveryVehicle
, vi phạm nguyên tắc LSP.
✅ Nên sửa lại thế này:
Chia hành vi một cách hợp lý hơn:
public abstract class DeliveryMode { public abstract void deliver();
} public class BikeDelivery extends DeliveryMode { public void deliver() { System.out.println("Delivering via bike"); }
} public class ScooterDelivery extends DeliveryMode { public void deliver() { System.out.println("Delivering via scooter"); }
}
Giờ đây mọi phương thức giao hàng đều tuân thủ hành vi dự kiến.
Nguyên tắc Phân tách Giao diện (Interface Segregation Principle - ISP)
Giao diện nên tập trung vào nhu cầu cụ thể của từng đối tượng sử dụng, tránh giao diện quá lớn khiến các lớp phải cài đặt những phương thức không cần thiết.
❌ Ví dụ chưa đúng:
public interface RestaurantPartner { void acceptOrder(); void provideNutritionalInfo(); void handleFeedback();
} public class SmallLocalDiner implements RestaurantPartner { public void acceptOrder() { /* ... */ } public void provideNutritionalInfo() { throw new UnsupportedOperationException(); } public void handleFeedback() { /* ... */ }
}
Quán ăn nhỏ không cung cấp thông tin dinh dưỡng, nhưng vẫn phải cài đặt phương thức đó.
✅ Nên sửa lại thế này:
Tách giao diện:
public interface OrderAcceptance { void acceptOrder();
} public interface FeedbackHandler { void handleFeedback();
} public interface NutritionInfoProvider { void provideNutritionalInfo();
} public class SmallLocalDiner implements OrderAcceptance, FeedbackHandler { public void acceptOrder() { /* ... */ } public void handleFeedback() { /* ... */ }
}
Mỗi quán ăn chỉ cần cài đặt giao diện phù hợp với chức năng của mình.
Nguyên tắc Đảo ngược Phụ thuộc (Dependency Inversion Principle - DIP)
Các module cấp cao không nên phụ thuộc vào cài đặt cụ thể, mà nên phụ thuộc vào abstraction (trừu tượng).
❌ Ví dụ chưa đúng:
public class OrderNotifier { private EmailService emailService = new EmailService(); public void notifyCustomer(String message) { emailService.sendEmail(message); }
}
Lớp OrderNotifier
phụ thuộc chặt vào EmailService
. Muốn thay đổi sang SMS phải sửa mã nguồn.
✅ Nên sửa lại thế này:
Giới thiệu interface trừu tượng:
// Notifier.java (Abstraction)
public interface Notifier { void notify(String message);
} // EmailService.java (Low-level implementation)
public class EmailService implements Notifier { public void notify(String message) { System.out.println("Email: " + message); }
} // SmsService.java (Low-level implementation)
public class SmsService implements Notifier { public void notify(String message) { System.out.println("SMS: " + message); }
} // OrderNotifier.java (High-level module)
public class OrderNotifier { private Notifier notifier; // Dependency injection (through constructor) public OrderNotifier(Notifier notifier) { this.notifier = notifier; } public void notifyCustomer(String message) { notifier.notify(message); // Use the abstraction (Notifier) }
} // Main.java
public class Main { public static void main(String[] args) { Notifier emailService = new EmailService(); // Can easily switch to SmsService OrderNotifier orderNotifier = new OrderNotifier(emailService); // Inject dependency orderNotifier.notifyCustomer("Order Shipped!"); }
}
Giờ đây OrderNotifier
chỉ phụ thuộc vào Notifier
(giao diện), không quan tâm chi tiết bên dưới là email, SMS hay bất kỳ hình thức nào khác.
Kết luận
Các nguyên tắc SOLID không chỉ là lý thuyết — mà là công cụ thực tế để viết mã sạch, có thể mở rộng và dễ bảo trì.
Thông qua ví dụ ứng dụng đặt đồ ăn thực tế, ta thấy:
- SRP giúp chia nhỏ logic theo chức năng
- OCP giúp dễ mở rộng chức năng mới
- LSP đảm bảo kế thừa đúng chuẩn
- ISP giúp giao diện gọn gàng, rõ ràng
- DIP tách biệt giữa logic cấp cao và chi tiết cấp thấp
Áp dụng SOLID không phải để làm mọi thứ phức tạp hơn, mà là để viết mã tốt hơn, lâu bền hơn với thời gian.
Hy vọng bài viết cung cấp thông tin hữu ích cho các bạn!