1. 什么是SOLID原则?
S.O.L.I.D原则是面向对象设计和编程中几个重要的编码原则首字母的缩写;
SRP:The Single Responsibility Principle,单一职责原则
OCP:The Open Closed Principle,开放封闭原则
LSP:The Liskov Substitution Principle,里氏替换原则
ISP:The Interface Segregation Principle,接口分离原则
DIP:The Dependency Inversion Principle,依赖倒置原则
下面将详细讲解每种原则所代表的意义。
2. SRP(单一职责原则)
当需要修改某个类的时候原因有且只有一个。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。类被修改的几率很大,因此应该专注于单一的功能。如果你把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能,这时就需要新一轮的测试来避免可能出现的问题,非常耗时耗力。
3. OCP(开放封闭原则)
软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。
- 通过增加代码来扩展功能,而不是修改已经存在的代码。
- 若客户模块和服务模块遵循同一个接口来设计,则客户模块可以不关心服务模块的类型,服务模块可以方便扩展服务(代码)。
- OCP支持替换的服务,而不用修改客户模块。
示例如下:
如果现在又多了一种发送信息的方式,比如可以通过微信来发送信息,那么不仅需要增加一个方法sendByWeChat(),还需要在调用它的地方进行修改,违反了OCP原则,更好的方式是抽象出一个Send接口,里面有个send()方法,然后让SendByEmail和SendBySMS去实现它既可。这样即使多了一个通过WeChat发送的请求,那么只要再添加一个SendByWeChat实现类实现Send接口既可。这样就不需要修改已有的接口定义和已实现类,很好的遵循了OCP原则。
如此,即很好的满足了OCP原则。
4. LSP(里氏替换原则)
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。客户模块不应关心服务模块的是如何工作的;同样的接口模块之间,可以在不知道服务模块代码的情况下,进行替换。即接口或父类出现的地方,实现接口的类或子类可以代入。
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
- 子类中可以增加自己特有的方法;
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松;
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
举个简单的例子来说明,我们需要完成一个两数相减的功能,由类A来负责:
后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:
- 两数相减。
- 两数相加,然后再加100。
由于类A已经实现了第一个功能,所以类B继承类A后,只需要再完成第二个功能就可以了,代码如下:
运行,不难发现肯定是无法获得正确的结果的,这就印证了上述观点。
5. ISP(接口分离原则)
不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口要好。
客户模块不应该依赖大的接口,应该裁减为小的接口给客户模块使用,以减少依赖性。如Java中一个类实现多个接口,不同的接口给不用的客户模块使用,而不是提供给客户模块一个大的接口。这里比较好理解,就不举例啦。
6. DIP(依赖注入或倒置原则)
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
- 抽象不应该依赖于细节,细节应该依赖于抽象
先让我们从宏观上来看下,举个例子,我们经常会用到宏观的一种体系结构模式—layer模式,通过层的概念分解和架构系统,比如常见得三层架构等。那么依赖关系应该是自上而下,也就是上层模块依赖于下层模块,而下层模块不依赖于上层,如下图所示。
这应该还是比较容易理解的,因为越底层的模块相对就越稳定,改动也相对越少,而越上层跟需求耦合度越高,改动也会越频繁,所以自上而下的依赖关系使上层发生变更时,不会影响到下层,降低变更带来的风险,保证系统的稳定。上面是立足在整体架构层的基础上的结果,再换个角度,从细节上再分析一下,这里我们暂时只关注UI和Service间的关系,如上面UI和Service这样的依赖关系会有什么样的问题?
- 当需要追加提供一种新的Service时,我们不得不对UI层进行改动,增加了额外的工作。
- 这种改动可能会影响到UI,带来风险。
- 改动后,UI层和Logic层都必须重新再做Unit testing。
那么具体怎么优化依赖关系才能让模块或层间的耦合更低呢?想想前面讲的OCP原则吧,观点是类似的。
我们可以为Service追加一个抽象层,上层UI不依赖于Service的details,UI和Service同时依赖于这个Service的抽象层。如下图是我们的改进后的结果。
这样改进后会有什么好处呢?
- Service进行扩展时,一般情况下不会影响到UI层,UI不需要改动。
- Service进行扩展时,UI层不需要再做Unit testing。