说到SOLID原则,相信有几年工作经验的朋友都有一个大概的印象,只是不知道它是什么。即便是一些工作十几年的朋友,对SOLID原则的理解还是很肤浅。今天我们就来聊聊SOLID原则以及它们之间的关系。01什么是SOLID原则?SOLID原则实际上是用来指导软件设计的。它分为五个设计原则,分别是:单一职责原则(SRP)开闭原则(OCP)里氏替换原则(LSP)接口隔离原则(ISP)依赖倒置原则(DIP)单一职责原则(SRP)单一职责原则(SingleResponsibilityPrinciple),它的定义是:类的变化应该有一个且只有一个原因。简单的说:接口职责要单一,不要承担过多的职责。以生活中的肯德基为例:前台负责收银的服务员不应该去餐厅收盘子。不要让餐厅负责收盘子的汉堡包。单一职责适用于接口、类,也适用于方法。比如我们需要修改用户密码,有两种方法。一种是使用“修改用户信息界面”修改密码,另一种是新建界面修改密码。在单一职责原则的指导下,一个方法只承担一个功能,因此我们应该新建一个接口来实现修改密码的功能。单一职责原则的重点是职责的划分,很多时候不是一成不变的,需要根据实际情况来确定。职责单一可以降低类的复杂度,明确类与类之间的职责,提高代码的可读性,更易于维护。但它的缺点也很明显,就是对技术人员的要求高,有时难以分清职责。当我们设计一个类时,我们可以从一个粗粒度的类开始。当业务发展到一定规模后,我们发现这个粗粒度类的方法和属性太多,经常被修改。重构之后,把这个类拆分成更细粒度的类,也就是所谓的持续重构。开闭原则(OCP)开闭原则(OpenClosedPrinciple),其定义为:一个软件实体,如类、模块、函数等,应该对扩展开放,对修改关闭。简单来说:当别人要修改软件功能时,他不能修改我们原来的代码,只能添加新的代码来达到修改软件功能的目的。这听起来有点玄乎,我举个例子。此代码模拟剥水果的处理程序。如果是苹果,那么削皮的方法只有一种;如果是香蕉,还有另一种剥皮方法。如果以后需要处理其他水果,后面还要加一大堆ifelse语句,最终导致整个方法又臭又长。如果恰好这种水果不同品种的剥皮方式不同,那么里面就会有很多层的嵌套。if(type==apple){//dealwithapple}elseif(type==banana){//dealwithbanana}elseif(type==...){//...}可以看出,上面的代码不满足“对扩展开放,对修改关闭”的原则。每次需要添加新的水果,直接修改原代码即可。久而久之,整个代码块就会变得又臭又长。如果我们对剥水果做一个抽象,苹果剥皮是一个具体的实现,香蕉剥皮是一个具体的实现,那么写出来的代码就会是这样的:}publicclassBananaPeelOffimplementPeelOff{voidpeelOff(){//dealwithbanan}}publicclassPeelOffFactory{privateMapmap=newHashMap();privateinit(){//initalltheClassthatimplementsPeelidOffinterface(statlicicOffinterface}}vo....){Stringtype="apple";PeelOffpeelOff=PeelOffFactory.getPeelOff(type);//getApplePeelOffClassInstance.peelOff.pealOff();}上面的实现使得别人无法修改我们的代码,为什么?因为当西瓜需要去皮的时候,他会发现只能新增一个类来实现PeelOff接口,而不能修改原来的代码。这样就实现了“对扩展开放,对修改关闭”的原则。里氏替换原则(LSP)里氏替换原则(LSP)的定义是:所有对基类的引用必须能够透明地使用其子类的对象。简单来说:父类能出现的地方,子类就可以出现,替换了就不会出错。比如下面出现Parent类的地方,可以换成Son类,其中Son是Parent的子类。Parentobj=newSon();相当于Sonson=newSon();这样的例子在Java语言中很常见,但核心的一点是:替换了就不会报错。这就要求子类所有相同的方法都必须遵循父类的约定,否则父类被子类替换时会出错。这个可能还是有点抽象,我举个例子。publicclassParent{//定义只能抛出空指针异常publicvoidhellothrowNullPointerException(){}}publicclassSoextendsParent{publicvoidhellothrowNullPointerException(){//子类实现时抛出所有异常throwException;}}在上面的代码中,父类为hello方法的定义是只能抛空指针异常,但是当子类重写父类的方法时抛出其他异常,违反了父类的约定。那么当父类出现的地方换成了子类,那么难免会出问题。其实这个例子不是很好,因为在编译层面可能会出现错误。但是意思要到位。这里的父类约定不仅仅指语法层面的约定,还包括实现上的约定。有时父类会在类注解和方法注解中解释相关约定。当你想重写父类的方法时,你需要了解这些约定,否则可能会出现问题。例如,子类违反了父类声明的函数。比如父类的一个排序方法是从小到大排序的,但是你子类的方法实际上写的是从大到小排序。LiskovSubstitutionPrincipleLSP强调:对于用户来说,可以使用父类的地方,就必须使用它的子类,预期的结果是一致的。接口隔离原则(ISP)接口隔离原则(InterfaceSegregationPrinciple)的定义是:类之间的依赖关系应该建立在最小的接口上。简单的说:界面的内容一定要尽可能小,越小越好。比如我们经常给别人提供服务,可能有很多服务调用者。很多时候我们会为不同的调用者提供一个统一的接口,但是有时候调用者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提倡不把一个大而全的接口扔给用户,而是把每个用户关心的接口隔离开来。依赖倒置原则(DIP)依赖倒置原则(DependenceInversionPrinciple)的定义是:高层模块不应该依赖低层模块,两者都应该依赖于它的抽象。抽象不应依赖于细节,即接口或抽象类不应依赖于实现类。细节应该依赖于抽象,即实现类不应该依赖于接口或抽象类。简单的说就是我们要对接口进行编程。通过抽象成一个接口,使得各个类的实现相互独立,实现了类与类之间的松耦合。如果我们每个人都可以通过接口进行编程,那么我们只需要在接口定义上达成一致,我们就可以很好地合作。软件设计的DIP提倡用户依赖抽象的服务接口,而不是依赖具体的服务执行者,从依赖具体实现转向依赖抽象接口,反之亦然。02SOLID原则的本质我们终于讲完了SOLID原则的五个原则。不过这么一通,好像明白了,但又好像什么都不记得了。那么我们来看看它们之间的关系。ThoughtWorks上有篇文章写的很不错。它说:单一职责是一切设计原则的基础,开闭原则是设计的最终目标。里氏替换原则强调子类替换父类后程序运行时的正确性,用于帮助实现开闭原则。接口隔离原则在帮助实现里氏替换原则的同时,也体现了单一职责。依赖倒置原则是过程式编程和面向对象编程的分水岭,也被用来指导接口隔离原则。图片l来自ThoughtWorks简单的说:DependencyInversionPrinciple告诉我们要面向接口编程。我们编程到接口后,接口隔离原则和单一职责原则告诉我们要注意职责划分,不要什么都塞在一起。当我们的职责差不多的时候,里氏代换原则告诉我们,在使用继承的时候,一定要注意遵守父类的契约。上述四项原则的最终目的是实现开闭原则。参考资料写了这么多年代码,你真的了解SOLID吗?-知道如何理解SOLID原则吗?-ThoughtWorksInsights重构的七大罪过-ThoughtWorksInsights代码关注。转载本文请联系陈淑仪公众号。