paint-brush
Nguyên tắc RẮN trong Java: Hướng dẫn cho người mới bắt đầutừ tác giả@ps06756
14,605 lượt đọc
14,605 lượt đọc

Nguyên tắc RẮN trong Java: Hướng dẫn cho người mới bắt đầu

từ tác giả Pratik Singhal14m2024/03/22
Read on Terminal Reader

dài quá đọc không nổi

Nguyên tắc RẮN là các nguyên tắc lập trình hướng đối tượng cần thiết để phát triển phần mềm có thể mở rộng. Các nguyên tắc là: S: Nguyên tắc trách nhiệm duy nhất O: Nguyên lý đóng/mở L: Nguyên tắc thay thế Liskov I: Nguyên tắc phân chia giao diện D: Nguyên tắc đảo ngược phụ thuộc
featured image - Nguyên tắc RẮN trong Java: Hướng dẫn cho người mới bắt đầu
Pratik Singhal HackerNoon profile picture
0-item

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.

Tại sao các nguyên tắc RẮN lại quan trọng?

Nguyên tắc RẮN rất quan trọng vì nhiều lý do:


  • Nguyên tắc RẮN cho phép chúng ta viết mã sạch và có thể bảo trì: Khi chúng ta bắt đầu một dự án mới, ban đầu chất lượng mã rất tốt vì chúng ta triển khai một số tính năng hạn chế. Tuy nhiên, khi chúng tôi kết hợp nhiều tính năng hơn, mã bắt đầu trở nên lộn xộn.


  • Nguyên tắc RẮN được xây dựng dựa trên nền tảng Trừu tượng, Đa hình và Kế thừa và dẫn đến các mẫu thiết kế cho các trường hợp sử dụng phổ biến. Hiểu các mẫu thiết kế này giúp triển khai các trường hợp sử dụng phổ biến trong lập trình.


  • Nguyên tắc RẮN giúp chúng tôi viết mã rõ ràng giúp cải thiện khả năng kiểm tra mã. Điều này là do mã có tính mô-đun và được liên kết lỏng lẻo. Mỗi mô-đun có thể được phát triển độc lập và thử nghiệm độc lập.


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ế.

1. Nguyên tắc trách nhiệm duy nhấ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:


  1. 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.).


  2. 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.

2. Nguyên tắc đóng/mở

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:


  • Nhiều khi, nguồn lớp ban đầu thậm chí có thể không có sẵn. Nó có thể là một phần phụ thuộc được dự án của bạn sử dụng.


  • Giữ nguyên lớp ban đầu sẽ làm giảm khả năng xảy ra lỗi. Vì có thể có các lớp khác phụ thuộc vào lớp mà chúng ta muốn sửa đổi.


Để 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 , GiftItemElectronicItem 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.

3. Nguyên tắc thay thế Liskov

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 CarBicycle . 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 BicycleBicycle 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 MotorizedVehicleNonMotorizedVehicle , đồ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. } }


4. Nguyên tắc phân chia giao diện

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 LinkedListArrayList ,


Lớp ArrayList triển khai (các) giao diện sau: Serializable , Cloneable , Iterable , Collection , ListRandomAccess .

Lớp LinkedList triển khai Serializable , Cloneable , Iterable , Collection , Deque , ListQueue .


Đó 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 , ListRandomAccess vào một giao diện, giả sử giao diện IList . Bây giờ, cả hai lớp ArrayListLinkedList đề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

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 DebitCardPaypal . 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 DebitCardPaypal .


Thay vào đó, chúng tôi sẽ tạo giao diện Payment và yêu cầu cả lớp DebitCardPaypal 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