SOLID 设计原则是编写干净代码时需要了解的最重要的设计原则。对于任何程序员来说,牢牢掌握 SOLID 原则是一项不可或缺的技能。它们是开发其他设计模式的基础。
在本文中,我们将使用一些现实生活中的示例来解决 SOLID 设计原则,并了解它们的重要性。
与多态性、抽象和继承一起,SOLID 原则对于擅长面向目标的编程非常重要。
坚实的原则之所以重要有多种原因:
现在让我们通过实际示例详细探讨每个 SOLID 原则。
SOLID原则中的S代表单一责任原则。单一职责原则规定,一个类应该只有一个改变的理由。这限制了我们在项目中纳入额外要求时需要更改的地方的数量。
每个类都应该有一个确切的改变理由。
例如,假设我们正在用 Java 设计一个银行应用程序,其中有一个SavingsAccount
类,该类允许借记、贷记和sendUpdates
等基本操作。 sendUpdate
方法采用名为NotificationMedium
的枚举(如电子邮件、SMS 等)并使用适当的介质发送更新。我们将为此编写代码,如下所示。
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 }
现在,如果您查看上面的SavingsAccount
类,您会发现它可能由于多种原因而发生更改:
SavingsAccount
类的核心逻辑是否有任何变化(如debit
、 credit
等)。
如果银行决定引入新的通知媒介(比如 WhatsApp)。
这违反了 SOLID 原则中的单一职责原则。为了解决这个问题,我们将创建一个单独的类来发送通知。
我们按照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 } } }
现在,由于我们已经重构了代码,如果NotificationMedium
或格式有任何变化,我们将更改Sender
类。但是,如果SavingsAccount
的核心逻辑发生变化, SavingsAccount
类也会发生变化。
这修复了我们在第一个示例中观察到的违规行为。
开放关闭原则指出,我们应该以这样的方式设计类,以便它们对扩展开放(在添加附加功能的情况下),但对修改封闭。关闭修改对我们有两个好处:
要查看开闭原则的示例,让我们看一下购物车的示例(例如在电子商务网站上实现的购物车)。
我将创建一个名为Cart
的类,其中包含您可以添加到其中的Item
列表。根据商品的类型及其税收,我们希望创建一个方法来计算Cart
类中的购物车总价值。
类应该对扩展开放,对修改关闭
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 }
在上面的示例中, calculateCartValue
方法通过迭代购物车内的所有商品并根据商品类型调用逻辑来计算购物车价值。
虽然这段代码看起来是正确的,但它违反了 SOLID 原则。
假设我们需要在计算购物车价值时为不同类型的商品(例如杂货)添加新规则。在这种情况下,我们必须修改原始类Cart
并在其中编写另一个else if
条件来检查Grocery
类型的项目。
然而,只需很少的重构,我们就可以使代码遵循开闭原则。让我们看看如何。
首先,我们将Item
类抽象化,并为不同类型的Item
创建具体类,如下所示。
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; } }
在每个具体的 Item 类(如GroceryItem
、 GiftItem
和ElectronicItem
中实现getValue()
方法,该方法包含税收和价值计算的业务逻辑。
现在,我们将使Cart
类依赖于抽象类Item
并为每个项目调用getValue()
方法,如下所示。
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; } }
现在,在这段重构的代码中,即使引入了新类型的Item
, Cart
类也保持不变。由于多态性,无论items
中的Item
的实际类型是什么,都会调用该类的ArrayList
getValue()
方法。
里氏替换原则指出,在给定的代码中,即使我们用子类的对象替换超类的对象,代码也不应该崩溃。换句话说,当子类继承超类并重写其方法时,应该与超类中方法的行为保持一致。
例如,如果我们创建以下类Vehicle
和两个类Car
和Bicycle
类。现在,假设我们在 Vehicle 类中创建一个名为startEngine()
的方法,它可以在Car
类中重写,但在Bicycle
类中将不受支持,因为Bicycle
没有引擎(请参见下面的代码示例)
子类在重写方法时应与超类的行为保持一致。
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"); } }
现在,假设有一些代码需要车辆类型的对象并依赖于startEngine()
方法。如果在调用该代码段时我们传递Bicycle
对象而不是传递Vehicle
类型的对象,则会导致代码中出现问题。由于Bicycle
(s) 类的方法在调用startEngine()
方法时会抛出异常。这将违反 SOLID 原则(里氏替换原则)
为了解决这个问题,我们可以创建两个类MotorizedVehicle
和NonMotorizedVehicle
,让Car
继承自MotorizedVehicle
类,让Bicycle
继承自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. } }
SOLID 原则中的“I”代表接口隔离原则。
接口隔离原则指出,我们不应该拥有更大的接口来强制实现类来实现未使用的方法,而应该拥有更小的接口并实现类。这样,类仅实现相关方法并保持干净。
将您的接口划分为多个较小的接口,而不是一个大接口。
例如,让我们看一下 Java 中内置的 Collections 框架。在其他数据结构中,Java还提供了LinkedList
和ArrayList
数据结构,
ArrayList
类实现以下接口: Serializable
、 Cloneable
、 Iterable
、 Collection
、 List
和RandomAccess
。
LinkedList
类实现Serializable
、 Cloneable
、 Iterable
、 Collection
、 Deque
、 List
和Queue
。
接口实在是太多了!
Java 开发人员可以将Serializable
、 Cloneable
、 Iterable
、 Collecton
、 List
和RandomAccess
合并到一个接口中,比如IList
接口,而不是拥有这么多接口。现在, ArrayList
和LinkedList
类都可以实现这个新的IList
接口。
但是,由于LinkedList
不支持随机访问,因此它可能实现了RandomAccess
接口中的方法,并且当有人尝试调用它时可能会抛出UnsupportedOperationException
。
然而,这将违反 SOLID 原则中的接口隔离原则,因为它会“强制”LinkedList 类实现RandomAccess
接口内的方法,即使不是必需的。
因此,最好根据公共行为来拆分接口,让每个类实现多个接口,而不是一个大接口。
依赖倒置原则指出上层的类不应该直接依赖于下层的类。这会导致两个级别之间的紧密耦合。
相反,下层类应该提供上层类所依赖的接口。
依赖接口而不是类
例如,让我们继续上面看到的Cart
示例,并对其进行增强以添加一些付款选项。假设我们有两种付款方式DebitCard
和Paypal
。现在,在Cart
类中,我们要向placeOrder
添加一个方法,该方法将计算购物车价值并根据提供的付款启动付款。方法。
为此,我们可以通过将两个付款选项添加为Cart
类中的字段,在上面的Cart
示例中添加依赖项。然而,这会将Cart
类与DebitCard
和Paypal
类紧密耦合。
相反,我们将创建一个Payment
接口,并让DebitCard
和Paypal
类都实现Payment
接口。现在, Cart
类将依赖于Payment
接口,而不是各个支付类型。这使类保持松散耦合。
请参阅下面的代码。
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()); } }
如果您有兴趣了解有关面向对象编程、GoF 设计模式和底层设计面试的更多信息,请查看我评价很高的课程《面向对象编程 + Java 设计模式》