SOLID 原则

本贴最后更新于 503 天前,其中的信息可能已经时移俗易

一、什么是 SOLID 原则?

SOLID 是让软件设计更易于理解、更加灵活和更易于维护的五个原则的简称。同时我们要知道的是有原则是件好事,但是也要时刻从实用的角度来考量,与生活中所有事情一样,盲目遵守这些原则可能会弊大于利。

二、详细介绍

1、单一职责原则(SRP)

单一职责原则(Single Responsibility Principle),它的定义是:修改一个类的原因只能有一个。简单地说:接口职责应该单一,不要承担过多的职责。 用生活中肯德基的例子来举例:负责前台收银的服务员,就不要去餐厅收盘子。负责餐厅收盘子的就不要去做汉堡。

尽量让每个类只负责软件中的一个功能,并将该功能完全封装(你也可称之为隐藏)在该类中。这条原则的主要目的是减少复杂度。你不需要费尽心机地去构思如何仅用 200 行代码来实现复杂设计,实际上完全可以使用十几个清晰的方法。

单一职责适用于接口、类,同时也适用于方法。例如我们需要修改用户密码,有两种方式可以实现,一种是用「修改用户信息接口」实现修改密码,一种是新起一个接口来实现修改密码功能。在单一职责原则的指导下,一个方法只承担一个职能,所以我们应该新起一个接口来实现修改密码的功能。

2、开闭原则(OCP)

开闭原则(Open Closed Principle),它的定义是:一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。简单地说:就是当别人要修改软件功能的时候,使得他不能修改我们原有代码,只能新增代码实现软件功能修改的目的。

如果一个类已经完成开发、测试和审核工作,而且属于某个框架或者可被其他类的代码直接使用的话,对其代码进行修改就是有风险的。你可以创建一个子类并重写原始类的部分内容以完成不同的行为,而不是直接对原始类的代码进行修改。这样你既可以达成自己的目标,但同时又无需修改已有的原始类客户端。**

示例

这段代码模拟的是对于水果剥皮的处理程序。如果是苹果,那么是一种拨皮方法;如果是香蕉,则是另一种剥皮方法。如果以后还需要处理其他水果,那么就会在后面加上很多 if else 语句,最终会让整个方法变得又臭又长。如果恰好这个水果中的不同品种有不同的剥皮方法,那么这里面又会有很多层嵌套。

if(type == apple){
    //deal with apple 
} else if (type == banana){
    //deal with banana
} else if (type == ......){
    //......
}

可以看得出来,上面这样的代码并没有满足「对拓展开放,对修改封闭」的原则。每次需要新增一种水果,都可以直接在原来的代码上进行修改。久而久之,整个代码块就会变得又臭又长。**

如果我们对剥水果皮这件事情做一个抽象,剥苹果皮是一个具体的实现,剥香蕉皮是一个具体的实现,那么写出的代码会是这样的:

public interface PeelOff {
    void peelOff();
}

public class ApplePeelOff implement PeelOff{
    void peelOff(){
        //deal with apple
    }
}

public class BananaPeelOff implement PeelOff{
    void peelOff(){
        //deal with banan
    }
}

public class PeelOffFactory{
    private Map<String, PeelOff> map = new HashMap();

    private init(){
        //init all the Class that implements PeelOff interface 
        //初始化所有实现PeelOff接口的类
      }
}

.....

public static void main(){
    String type = "apple";
    PeelOff peelOff = PeelOffFactory.getPeelOff(type);  //get ApplePeelOff Class Instance.
    peelOff.pealOff();
}

上面这种实现方式使得别人无法修改我们的代码,为什么?

因为当需要对西瓜剥皮的时候,他会发现他只能新增一个类实现 PeelOff 接口,而无法再原来的代码上修改。这样就实现了「对拓展开放,对修改封闭」的原则。

3、里氏替换原则(LSP)

里氏替换原则(LSP)的定义是:所有引用基类的地方必须能透明地使用其子类的对象。简单地说:所有父类能出现的地方,子类就可以出现,并且替换了也不会出现任何错误。 例如下面 Parent 类出现的地方,可以替换成 Son 类,其中 Son 是 Parent 的子类。

Parent obj = new Son();
等价于
Son son  = new Son();

这样的例子在 Java 语言中是非常常见的,但其核心要点是:替换了也不会出现任何的错误。这就要求子类的所有相同方法,都必须遵循父类的约定,否则当父类替换为子类时就会出错。 这样说可能还是有点抽象,我举个例子。

public class Parent{
    // 定义只能扔出空指针异常
    public void hello throw NullPointerException(){
    }
}
public class Son extends Parent{
    public void hello throw NullPointerException(){
        // 子类实现时却扔出所有异常
        throw Exception;
    }
}

上面的代码中,父类对于 hello 方法的定义是只能扔出空指针异常,但子类覆盖父类的方法时,却扔出了其他异常,违背了父类的约定。那么当父类出现的地方,换成了子类,那么必然会出错。

其实这个例子举得不是很好,因为这个在编译层面可能就有错误。但表达的意思应该是到位了。

而这里的父类的约定,不仅仅指的是语法层面上的约定,还包括实现上的约定。有时候父类会在类注释、方法注释里做了相关约定的说明,当你要覆写父类的方法时,需要弄懂这些约定,否则可能会出现问题。例如子类违背父类声明要实现的功能。比如父类某个排序方法是从小到大来排序,你子类的方法竟然写成了从大到小来排序。

里氏替换原则 LSP 重点强调:对使用者来说,能够使用父类的地方,一定可以使用其子类,并且预期结果是一致的。

4、接口隔离原则(ISP)

接口隔离原则(Interface Segregation Principle)的定义是:类间的依赖关系应该建立在最小的接口上。简单地说:接口的内容一定要尽可能地小,能有多小就多小。

尽量缩小接口的范围,使得客户端的类不必实现其不需要的行为。继承只允许类拥有一个超类,但是它并不限制类可同时实现的接口的数量。因此,我们不需要将大量无关的方法塞进单个接口。我们可将其拆分为更精细的接口,如有需要可在单个类中实现所有接口,某些类也可只实现其中的一个接口。

举个例子来说,我们经常会给别人提供服务,而服务调用方可能有很多个。很多时候我们会提供一个统一的接口给不同的调用方,但有些时候调用方 A 只使用 1、2、3 这三个方法,其他方法根本不用。调用方 B 只使用 4、5 两个方法,其他都不用。按照接口隔离原则的意思是,你应该把 1、2、3 抽离出来作为一个接口,4、5 抽离出来作为一个接口,这样接口之间就隔离开来了。

那么为什么要这么做呢?我想这是为了隔离变化吧! 想想看,如果我们把 1、2、3、4、5 放在一起,那么当我们修改了 A 调用方才用到 的 1 方法,此时虽然 B 调用方根本没用到 1 方法,但是调用方 B 也会有发生问题的风险。而如果我们把 1、2、3 和 4、5 隔离成两个接口了,我们修改 1 方法,绝对不会影响到 4、5 方法。

除了改动导致的变化风险之外,其实还会有其他问题,例如:调用方 A 抱怨,为什么我只用 1、2、3 方法,你还要写上 4、5 方法,增加我的理解成本。调用方 B 同样会有这样的困惑。

在软件设计中,ISP 提倡不要将一个大而全的接口扔给使用者,而是将每个使用者关注的接口进行隔离。

5、依赖倒置原则(DIP)

依赖倒置原则(Dependence Inversion Principle)的定义是:高层模块不应该依赖底层模块,两者都应该依赖其抽象。抽象不应该依赖细节,即接口或抽象类不依赖于实现类。细节应该依赖抽象,即实现类应该依赖于接口或抽象类。简单地说,就是说我们应该面向接口编程。通过抽象成接口,使各个类的实现彼此独立,实现类之间的松耦合。

如果我们每个人都能通过接口编程,那么我们只需要约定好接口定义,我们就可以很好地合作了。软件设计的 DIP 提倡使用者依赖一个抽象的服务接口,而不是去依赖一个具体的服务执行者,从依赖具体实现转向到依赖抽象接口,倒置过来。

示例

在本例中,高层次的预算报告类(BudgetReport)使用低层次的数据库类(MySQLDatabase)来读取和保存其数据。这意味着低层次类中的任何改变(例如当数据库服务器发布新版本时)都可能会影响到高层次的类,但高层次的类不应关注数据存储的细节。

01SOLID 原则.jpg

修改前:高层次的类依赖于低层次的类

要解决这个问题,我们可以创建一个描述读写操作的高层接口,并让报告类使用该接口代替低层次的类。然后我们可以修改或扩展低层次的原始类来实现业务逻辑声明的读写接口。

02SOLID 原则.jpg

修改后:低层次的类依赖于高层次的抽象

其结果是原始的依赖关系被倒置:现在低层次的类依赖于高层次的抽象。

三、SOLID 原则的本质

  • 单一职责是所有设计原则的基础,开闭原则是设计的终极目标。
  • 里氏替换原则强调的是子类替换父类后程序运行时的正确性,它用来帮助实现开闭原则。
  • 而接口隔离原则用来帮助实现里氏替换原则,同时它也体现了单一职责。
  • 依赖倒置原则是过程式编程与面向对象编程的分水岭,同时它也被用来指导接口隔离原则。

03SOLID 原则.jpg

简单地说:依赖倒置原则告诉我们要面向接口编程。当我们面向接口编程之后,接口隔离原则和单一职责原则又告诉我们要注意职责的划分,不要什么东西都塞在一起。当我们职责捋得差不多的时候,里氏替换原则告诉我们在使用继承的时候,要注意遵守父类的约定。而上面说的这四个原则,它们的最终目标都是为了实现开闭原则。

四、参考资料

超易懂!原来 SOLID 原则要这么理解

深入设计模式

1 操作
luodiab 在 2023-08-07 14:30:22 更新了该帖

相关帖子

回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...