Nguyên tắc thiết kế RẮN là những nguyên tắc thiết kế quan trọng nhất bạn cần biết để viết mã sạch. Nắm vững các nguyên tắc SOLID là một kỹ năng không thể thiếu đối với bất kỳ lập trình viên nào. Chúng là nền tảng để phát triển các mẫu thiết kế khác.
Trong bài viết này, chúng tôi sẽ giải quyết các nguyên tắc thiết kế SOLID bằng cách sử dụng một số ví dụ thực tế và hiểu tầm quan trọng của chúng.
Cùng với Đa hình, Trừu tượng và Kế thừa, Nguyên tắc RẮN thực sự quan trọng để giỏi lập trình hướng mục tiêu.
Nguyên tắc RẮN rất quan trọng vì nhiều lý do:
Bây giờ chúng ta hãy khám phá chi tiết từng nguyên tắc RẮN bằng các ví dụ thực tế.
S trong nguyên tắc SOLID là viết tắt của Nguyên tắc Trách nhiệm Duy nhất. Nguyên tắc Trách nhiệm duy nhất nêu rõ rằng một lớp chỉ nên có một lý do duy nhất để thay đổi. Điều này giới hạn số lượng vị trí chúng tôi cần thực hiện thay đổi khi kết hợp các yêu cầu bổ sung trong dự án của mình.
Mỗi lớp nên có chính xác một lý do để thay đổi.
Ví dụ: giả sử chúng tôi đang thiết kế một ứng dụng ngân hàng bằng Java trong đó chúng tôi có lớp SavingsAccount
cho phép các hoạt động cơ bản như ghi nợ, tín dụng và sendUpdates
. Phương thức sendUpdate
nhận một enum có tên là NotificationMedium
(như Email, SMS, v.v.) và gửi bản cập nhật bằng phương tiện thích hợp. Chúng tôi sẽ viết mã cho điều đó như hiển thị bên dưới.
public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void sendNotification(NotificationMedium medium) { if (medium == NotificationMedium.SMS) { // Send SMS here } else if (medium == NotificationMedium.EMAIL) { // Send Email here } } }
public enum NotificationMedium { SMS, EMAIL }
Bây giờ, nếu bạn nhìn vào lớp SavingsAccount
ở trên, nó có thể thay đổi do nhiều lý do:
Nếu có bất kỳ thay đổi nào về logic cốt lõi của lớp SavingsAccount
(như debit
, credit
, v.v.).
Nếu ngân hàng quyết định giới thiệu phương tiện Thông báo mới (giả sử WhatsApp).
Đây là hành vi vi phạm Nguyên tắc Trách nhiệm duy nhất trong Nguyên tắc RẮN. Để khắc phục, chúng tôi sẽ tạo một lớp riêng để gửi thông báo.
Hãy cấu trúc lại mã ở trên theo Nguyên tắc SOLID
public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void printBalance() { System.out.println("Name: " + name+ " Account Balance: " + balance); } public void sendNotification(Medium medium) { Sender.sendNotification(medium, this); } }
public enum NotificationMedium { SMS, EMAIL }
public class Sender { public static void sendNotification(NotificationMedium medium, SavingsAccount account) { // extract account data from the account object if (medium == NotificationMedium.SMS) { //logic to send SMS here } else if (medium == NotificationMedium.EMAIL) { // logic to send Email here } } }
Bây giờ, vì chúng tôi đã cấu trúc lại mã nên nếu có bất kỳ thay đổi nào về NotificationMedium
hoặc định dạng, chúng tôi sẽ thay đổi lớp Sender
. Tuy nhiên, nếu có sự thay đổi về logic cốt lõi của SavingsAccount
thì sẽ có những thay đổi trong lớp SavingsAccount
.
Điều này khắc phục vi phạm mà chúng tôi quan sát thấy trong ví dụ đầu tiên.
Nguyên tắc Đóng mở nêu rõ rằng chúng ta nên thiết kế các lớp theo cách sao cho chúng có thể mở để mở rộng (trong trường hợp thêm các tính năng bổ sung) nhưng đóng để sửa đổi. Việc đóng cửa để sửa đổi giúp chúng tôi theo hai cách:
Để xem ví dụ về Nguyên tắc Đóng mở, chúng ta hãy xem ví dụ về giỏ hàng (chẳng hạn như giỏ hàng được triển khai trên các trang web thương mại điện tử).
Tôi sẽ tạo một lớp có tên là Cart
, lớp này sẽ chứa danh Item
mà bạn có thể thêm vào đó. Tùy thuộc vào loại mặt hàng và mức thuế đánh vào mặt hàng đó, chúng tôi muốn tạo một phương thức tính tổng giá trị giỏ hàng bên trong lớp Cart
.
Các lớp học nên được mở để mở rộng và đóng cửa để sửa đổi
import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart() { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { if (item.getItemType() == GIFT) { // 8% tax on gift + 2% gift wrap cost value += (item.getValue()*1.08) + item.getValue()*0.02 } else if (item.getItemType() == ItemType.ELECTRONIC_ITEM) { value += (item.getValue()*1.11); } else { value += item.getValue()*1.10; } } return value; } }
@Getter @Setter public abstract class Item { protected double price; private ItemType itemType; public double getValue() { return price; } }
public enum ItemType { ELECTRONIC, GIFT }
Trong ví dụ trên, phương thức calculateCartValue
tính toán giá trị giỏ hàng bằng cách lặp lại tất cả các mặt hàng bên trong giỏ hàng và gọi logic dựa trên loại mặt hàng.
Mặc dù mã này có vẻ đúng nhưng nó vi phạm Nguyên tắc RẮN.
Giả sử chúng ta cần thêm quy tắc mới cho một loại mặt hàng khác (chẳng hạn như Cửa hàng tạp hóa) trong khi tính giá trị giỏ hàng. Trong trường hợp đó, chúng ta sẽ phải sửa đổi lớp ban đầu Cart
và viết một điều kiện else if
bên trong nó để kiểm tra các mặt hàng thuộc loại Grocery
.
Tuy nhiên, chỉ cần tái cấu trúc một chút, chúng ta có thể làm cho mã tuân thủ nguyên tắc đóng/mở. Hãy xem làm thế nào.
Đầu tiên, chúng ta sẽ tạo lớp Item
trừu tượng và tạo các lớp cụ thể cho các loại Item
khác nhau như dưới đây.
public abstract class Item { protected double price; public double getValue() { return price; } }
public class ElectronicItem extends Item { public ElectronicItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.11; } }
public class GiftItem extends Item { public GiftItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.08 + super.getValue()*0.02; } }
public class GroceryItem extends Item { public GroceryItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.03; } }
Bên trong mỗi lớp Item cụ thể như GroceryItem
, GiftItem
và ElectronicItem
triển khai phương thức getValue()
chứa logic nghiệp vụ để tính thuế và giá trị.
Bây giờ, chúng ta sẽ làm cho lớp Cart
phụ thuộc vào lớp trừu tượng Item
và gọi phương thức getValue()
cho từng mục như dưới đây.
import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } }
Bây giờ, trong mã được tái cấu trúc này, ngay cả khi các loại Item
mới được giới thiệu, lớp Cart
vẫn không thay đổi. Do tính đa hình, bất kể loại Item
bên trong items
ArrayList
là gì, phương thức getValue()
của lớp đó sẽ được gọi.
Nguyên tắc thay thế của Liskov nói rằng trong một mã nhất định, ngay cả khi chúng ta thay thế đối tượng của siêu lớp bằng đối tượng của lớp con, mã sẽ không bị hỏng. Nói cách khác, khi một lớp con kế thừa một siêu lớp và ghi đè các phương thức của nó, nó phải duy trì tính nhất quán với hành vi của phương thức trong siêu lớp.
Ví dụ: nếu chúng ta tạo các lớp sau Vehicle
và hai lớp Car
và Bicycle
. Bây giờ, giả sử chúng ta tạo một phương thức có tên là startEngine()
trong lớp Xe, nó có thể bị ghi đè trong lớp Car
hơi, nhưng nó sẽ không được hỗ trợ trong lớp Bicycle
vì Bicycle
không có động cơ (xem mẫu mã bên dưới)
Lớp con phải duy trì tính nhất quán với hành vi của lớp cha khi ghi đè các phương thức.
class Vehicle { public void startEngine() { // start engine of the vehicle } } class Car extends Vehicle { @Override public void startEngine() { // Start Engine } } class Bicycle extends Vehicle { @Override public void startEngine(){ throw new UnsupportedOperationException("Bicycle doesn't have engine"); } }
Bây giờ, giả sử có một số mã mong đợi một đối tượng thuộc loại phương tiện và dựa vào phương thức startEngine()
. Nếu trong khi gọi đoạn mã đó thay vì truyền một đối tượng thuộc loại Vehicle
mà chúng ta truyền một đối tượng Bicycle
, điều đó sẽ dẫn đến các vấn đề trong mã. Vì phương thức của (các) lớp Bicycle
sẽ đưa ra một ngoại lệ khi phương thức startEngine()
được gọi. Điều này sẽ vi phạm Nguyên tắc RẮN (nguyên tắc thay thế của Liskov)
Để giải quyết vấn đề này, chúng ta có thể tạo hai lớp MotorizedVehicle
và NonMotorizedVehicle
, đồng thời có Car
kế thừa từ lớp MotorizedVehicle
và có Bicycle
kế thừa từ NonMotorizedVehicle
class Vehicle { } class MotorizedVehicle extends Vehicle { public void startEngine() { // start engine here } } class Car extends MotorizedVehicle { @Override public void startEngine() { // Start Engine } } class NonMotorizedVehicle extends Vehicle { public void startRiding() { // Start without engine } } class Bicycle extends NonMotorizedVehicle { @Override public void startRiding(){ // Start riding without the engine. } }
Chữ “I” trong Nguyên tắc SOLID là viết tắt của Nguyên tắc phân chia giao diện.
Nguyên tắc phân tách Giao diện nêu rõ rằng thay vì có các giao diện lớn hơn buộc các lớp triển khai phải triển khai các phương thức không sử dụng, chúng ta nên có các giao diện nhỏ hơn và triển khai các lớp. Bằng cách này, các lớp chỉ thực hiện các phương thức có liên quan và vẫn sạch sẽ.
Chia giao diện của bạn thành nhiều giao diện nhỏ hơn thay vì một giao diện lớn.
Ví dụ: hãy xem khung công tác Bộ sưu tập tích hợp sẵn trong Java. Trong số các cấu trúc dữ liệu khác, Java cũng cung cấp cấu trúc dữ liệu LinkedList
và ArrayList
,
Lớp ArrayList
triển khai (các) giao diện sau: Serializable
, Cloneable
, Iterable
, Collection
, List
và RandomAccess
.
Lớp LinkedList
triển khai Serializable
, Cloneable
, Iterable
, Collection
, Deque
, List
và Queue
.
Đó là khá nhiều giao diện!
Thay vì có quá nhiều giao diện, các nhà phát triển Java có thể kết hợp Serializable
, Cloneable
, Iterable
, Collecton
, List
và RandomAccess
vào một giao diện, giả sử giao diện IList
. Bây giờ, cả hai lớp ArrayList
và LinkedList
đều có thể triển khai giao diện IList
mới này.
Tuy nhiên, vì LinkedList
không hỗ trợ truy cập ngẫu nhiên nên nó có thể đã triển khai các phương thức trong giao diện RandomAccess
và có thể ném ra UnsupportedOperationException
khi ai đó cố gắng gọi nó.
Tuy nhiên, điều đó sẽ vi phạm nguyên tắc Phân chia giao diện trong Nguyên tắc SOLID vì nó sẽ “ép buộc” lớp LinkedList triển khai các phương thức bên trong giao diện RandomAccess
mặc dù không bắt buộc.
Vì vậy, tốt hơn nên chia giao diện dựa trên hành vi chung và để mỗi lớp triển khai nhiều giao diện thay vì một giao diện lớn.
Nguyên tắc đảo ngược phụ thuộc nêu rõ rằng các lớp ở cấp trên không nên phụ thuộc trực tiếp vào các lớp ở cấp thấp hơn. Điều này gây ra sự kết hợp chặt chẽ giữa hai cấp độ.
Thay vào đó, các lớp cấp thấp hơn nên cung cấp một giao diện mà các lớp cấp cao hơn sẽ phụ thuộc vào.
Phụ thuộc vào giao diện hơn là các lớp
Ví dụ: hãy tiếp tục với ví dụ Cart
mà chúng ta đã thấy ở trên và nâng cao nó để thêm một số tùy chọn thanh toán. Giả sử chúng tôi có hai loại tùy chọn thanh toán với DebitCard
và Paypal
. Bây giờ, trong lớp Cart
, chúng ta muốn thêm một phương thức vào placeOrder
để tính giá trị giỏ hàng và bắt đầu thanh toán dựa trên khoản thanh toán được cung cấp. phương pháp.
Để làm điều này, chúng ta có thể thêm phần phụ thuộc vào ví dụ Cart
ở trên bằng cách thêm hai tùy chọn thanh toán làm các trường bên trong lớp Cart
. Tuy nhiên, điều đó sẽ kết hợp chặt chẽ giữa lớp Cart
với lớp DebitCard
và Paypal
.
Thay vào đó, chúng tôi sẽ tạo giao diện Payment
và yêu cầu cả lớp DebitCard
và Paypal
triển khai giao diện Payment
. Bây giờ, lớp Cart
sẽ phụ thuộc vào giao diện Payment
chứ không phụ thuộc vào các loại thanh toán riêng lẻ. Điều này giữ cho các lớp được liên kết lỏng lẻo.
Xem mã bên dưới.
public interface Payment { void doPayment(double amount); } public class PaypalPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate paypal payment } } public class DebitCardPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate payment via debit card } } import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; private Payment paymentOption; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); this.paymentOption = paymentOption; } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } public void placeOrder() { this.paymentOption.doPayment(calculateCartValue()); } }
Nếu bạn muốn tìm hiểu thêm về lập trình hướng đối tượng, các mẫu thiết kế GoF và các cuộc phỏng vấn thiết kế cấp thấp, thì hãy xem khóa học được đánh giá cao của tôi Lập trình hướng đối tượng + Các mẫu thiết kế Java