paint-brush
Java 中的 SOLID 原则:初学者指南经过@ps06756
14,604 讀數
14,604 讀數

Java 中的 SOLID 原则:初学者指南

经过 Pratik Singhal14m2024/03/22
Read on Terminal Reader

太長; 讀書

SOLID 原则是开发可扩展软件所必需的面向对象编程原则。原则是: S:单一职责原则 O:开/闭原理 L:里氏替换原理 一:接口隔离原则 D:依赖倒置原则
featured image - Java 中的 SOLID 原则:初学者指南
Pratik Singhal HackerNoon profile picture
0-item

SOLID 设计原则是编写干净代码时需要了解的最重要的设计原则。对于任何程序员来说,牢牢掌握 SOLID 原则是一项不可或缺的技能。它们是开发其他设计模式的基础。


在本文中,我们将使用一些现实生活中的示例来解决 SOLID 设计原则,并了解它们的重要性。


与多态性、抽象和继承一起,SOLID 原则对于擅长面向目标的编程非常重要。

为什么坚实的原则很重要?

坚实的原则之所以重要有多种原因:


  • SOLID 原则使我们能够编写干净且可维护的代码:当我们开始一个新项目时,最初代码质量很好,因为我们实现的功能集有限。然而,随着我们合并更多的功能,代码开始变得混乱。


  • SOLID 原则建立在抽象、多态性和继承的基础上,并导致常见用例的设计模式。了解这些设计模式有助于在编程中实现常见用例。


  • SOLID 原则帮助我们编写干净的代码,从而提高代码的可测试性。这是因为代码是模块化且松散耦合的。每个模块都可以独立开发、独立测试。


现在让我们通过实际示例详细探讨每个 SOLID 原则。

1. 单一职责原则

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类,您会发现它可能由于多种原因而发生更改:


  1. SavingsAccount类的核心逻辑是否有任何变化(如debitcredit等)。


  2. 如果银行决定引入新的通知媒介(比如 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类也会发生变化。


这修复了我们在第一个示例中观察到的违规行为。

2. 开闭原理

开放关闭原则指出,我们应该以这样的方式设计类,以便它们对扩展开放(在添加附加功能的情况下),但对修改封闭。关闭修改对我们有两个好处:


  • 很多时候,原始的类源甚至可能不可用。它可能是您的项目消耗的依赖项。


  • 保持原始类不变可以减少出现错误的机会。因为可能还有其他类依赖于我们要修改的类。


要查看开闭原则的示例,让我们看一下购物车的示例(例如在电子商务网站上实现的购物车)。


我将创建一个名为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 类(如GroceryItemGiftItemElectronicItem中实现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; } }


现在,在这段重构的代码中,即使引入了新类型的ItemCart类也保持不变。由于多态性,无论items中的Item的实际类型是什么,都会调用该类的ArrayList getValue()方法。

3.里氏替换原理

里氏替换原则指出,在给定的代码中,即使我们用子类的对象替换超类的对象,代码也不应该崩溃。换句话说,当子类继承超类并重写其方法时,应该与超类中方法的行为保持一致。


例如,如果我们创建以下类Vehicle和两个类CarBicycle类。现在,假设我们在 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 原则(里氏替换原则)


为了解决这个问题,我们可以创建两个类MotorizedVehicleNonMotorizedVehicle ,让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. } }


4. 接口隔离原则

SOLID 原则中的“I”代表接口隔离原则。


接口隔离原则指出,我们不应该拥有更大的接口来强制实现类来实现未使用的方法,而应该拥有更小的接口并实现类。这样,类仅实现相关方法并保持干净。

将您的接口划分为多个较小的接口,而不是一个大接口。

例如,让我们看一下 Java 中内置的 Collections 框架。在其他数据结构中,Java还提供了LinkedListArrayList数据结构,


ArrayList类实现以下接口: SerializableCloneableIterableCollectionListRandomAccess

LinkedList类实现SerializableCloneableIterableCollectionDequeListQueue


接口实在是太多了!


Java 开发人员可以将SerializableCloneableIterableCollectonListRandomAccess合并到一个接口中,比如IList接口,而不是拥有这么多接口。现在, ArrayListLinkedList类都可以实现这个新的IList接口。


但是,由于LinkedList不支持随机访问,因此它可能实现了RandomAccess接口中的方法,并且当有人尝试调用它时可能会抛出UnsupportedOperationException


然而,这将违反 SOLID 原则中的接口隔离原则,因为它会“强制”LinkedList 类实现RandomAccess接口内的方法,即使不是必需的。


因此,最好根据公共行为来拆分接口,让每个类实现多个接口,而不是一个大接口。

依赖倒置原则

依赖倒置原则指出上层的类不应该直接依赖于下层的类。这会导致两个级别之间的紧密耦合。


相反,下层类应该提供上层类所依赖的接口。


依赖接口而不是类


例如,让我们继续上面看到的Cart示例,并对其进行增强以添加一些付款选项。假设我们有两种付款方式DebitCardPaypal 。现在,在Cart类中,我们要向placeOrder添加一个方法,该方法将计算购物车价值并根据提供的付款启动付款。方法。


为此,我们可以通过将两个付款选项添加为Cart类中的字段,在上面的Cart示例中添加依赖项。然而,这会将Cart类与DebitCardPaypal类紧密耦合。


相反,我们将创建一个Payment接口,并让DebitCardPaypal类都实现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 设计模式》