前言
从部门老大对于软件设计原则和设计模式的分享,再到近阶段的工作都涉及了软件设计相关的知识,都让我对软件设计这一块难啃的骨头有了小小的认识。本文中的软件设计原则也只简要论述了面向对象的S.O.L.I.D原则 ,设计模式也只介绍了常用的几种。
面向对象的S.O.L.I.D原则
SOLID是五个面向对象编程的重要原则的缩写,它是由Robert C. Martin(Bob大叔)在21世纪初定义的。它是每个开发者必备的基本知识。运用这些原则能让我们写出更优质的代码,能使我们的代码更健壮,更易于维护,有更好的可持续性、扩展性和鲁棒的代码。
高内聚
内聚:一个模块内各个元素彼此结合的紧密程度;高内聚就是每个模块尽可能独立完成自己的功能,不依赖模块外部的代码。
Single Responsibility Principle(SRP) — 单一职责原则
定义
一个类或者模块应该有且只有一个改变的原因
职责单一原则的核心思想是:一个类或模块只负责一个功能,并且只有一个引起它变化的原因。单一职责原则可以看作是低耦合,高内聚在面向对象的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。 职责越多,可能引起它变化的原因就越多,也将导致职责依赖严重,从而使耦合度增大。
代码示例
首先我们先看一个用户信息类(UserInfo):
1 | public interface IUserInfo{ |
上面的代码将业务对象和业务逻辑的内容放到了一个类中,业务对象和业务逻辑都会引起IUserInfo类的变化,上面的代码就违反了单一职责原则。按照SRP原则,我们应该为不同的职责创建不同的类,修改如下:
1 | public interface IUserBro{ |
这样,我们就有了两个类,但是每个类都有单一的职责,我们使它变成了低耦合高内聚。
好处
- 降低耦合性
- 代码易于理解和维护
Interface Segregation Principle(ISP) — 接口隔离原则
定义
使用多个专用的接口比使用一个通用接口好
ISP原则意思是把功能实现在接口中,而不是类中;并且一个类绝不要实现不会用到的接口。不遵循这个原则意味着我们在实现里会依赖很多我们并不需要的方法,但又不得不去定义。所以,实现多个特定的接口比实现一个通用接口要好。一个接口被需要用到的类所定义,所以这个接口不应该有这个类不需要实现的其他方法。
代码示例
我们有一个ICar的接口:
1 | public interface ICar { |
同时也有一个实现ICar接口的Mustang类:
1 | public class Mustang implements ICar { |
现在要添加一个新的车型:一辆DeloRean,可以穿梭时空。
我们修改ICar接口类以满足此需求:
1 | public interface ICar { |
新增DeloRean类实现ICar接口:
1 | public class DeloRean implements ICar { |
同时修改Mustang类:
1 | public class Mustang implements ICar { |
这种情况下,Mustang违反了接口隔离原则,它实现了它不需要的方法。
使用接口隔离的解决方法:
重构ICar接口:
1 | public interface ICar { |
新增ITimeMachine接口:
1 | public interface ITimeMachine { |
重构Mustang(只实现ICar接口)
1 | public class Mustang implements ICar { |
重构DeloRean(同时实现ICar和ITimeMachine)
1 | public class DeloRean implements ICar, ITimeMachine { |
好处
- 系统解耦
- 代码易于重构
低耦合
耦合:模块与模块之间接口的复杂程度,模块之间联系越复杂耦合度越高,牵一发动全身;低耦合就是要尽可能减少模块之间的交互复杂程度。
Open/Closed Principle(OCP) — 开闭原则
定义
软件中的对象(类、模块、函数等等)应该对于扩展是开放的,但是对于修改是封闭的
根据这一原则,一个实体是允许在不改变它的源代码的前提下变更它的行为。
- open for extension:对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
- close for modification:对修改封闭,意味着扩展新的功能行为而不需要也不应该修改现有的代码。
对于面向对象来说,需要你依赖抽象,而不是实现。
代码示例
首先我们有两个不同形状的类:
1 | public class Rectangle{ |
其次我们还有一个类可以画出不同的形状:
1 | public class ShapePrinter{ |
从上面可以看到,当我们每次想画一个新的形状就要修改ShapePrinter类的drawShape方法来接受这个新的形状。这样当形状种类多了之后,ShapePrinter类就会存在大量的1
2
3
4
5
6
7
8
9
按照OCP原则,修改如下:
我们添加一个Shape接口类:
```java
public interface Shape{
void draw();
}
重构Rectangle类和Square类以实现Shape:
1 | public class Rectangle implements Shape{ |
ShapePrinter类的重构:
1 | public void drawShape(Shape shape){ |
好处
- 代码的可维护性和复用性
- 代码会更健壮
Liskov Substitution Principle(LSP) — 里氏替换原则
定义
派生类(子类)对象可以在程式中代替其基类(超类)对象。“Subtypes must be substituable for their base types”
根据定义所说,程序里的对象都应该可以被它的子类实例替换而不用更改程序,另外不应该在代码中出现if/else之类对子类类型进行判断的条件。里氏替换原则是使代码符合开闭原则的一个重要保证。正是由于子类的可替换性才使得父类型的模块在无需修改的情况下就可以扩展。
代码示例
我们有一个Rectangle类:
1 | public class Rectangle { |
还有一个Square类(从数学上讲正方形也是长方形的一种):
1 | public class Square extends Rectangle { |
然后我们写个测试函数:
1 | public class LiskovSubstitutionTest { |
同样的函数却不适用Square:
1 | public class LiskovSubstitutionTest { |
从代码上看出Square并不能正确替代Rectangle类,因为它不遵循Rectangle的行为规则,违反了LSP原则。
解决方法:
用IShape接口来获取面积:
1 | public interface IShape { |
重构Rectangle和Square以实现IShape:
1 | public class Rectangle implements IShape { |
修改函数:
1 | public class LiskovSubstitutionTest { |
好处
- 更高的代码复用性
- 类的层次结构易于理解
Dependency Inversion Principle(DIP) — 依赖倒置原则
定义
高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口
抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口
在面向对象编程领域中,依赖反转原则是指一个特定的类不应该直接依赖于另一个类,但是可以依赖于这个类的抽象(接口)。
附上来自维基百科上的图:
图1中,高层对象A依赖于底层对象B的实现;图2中,把高层对象A对底层对象的需求抽象为一个接口A,底层对象B实现了接口A,这就是依赖反转。
代码示例
我们有个DeliveryDriver类代表着一个司机为快递公司工作:
1 | public class DeliveryDriver{ |
其次有个DeliveryCompany类处理货物装运:
1 | public class DeliveryCompany{ |
在上述代码中,DeliveryCompany创建并使用DeliveryDriver实例,所以DeliveryCompany是一个依赖于低层次类的高层次的类,这就违反了依赖反转原则(在上述代码中DeliveryCompany需要运送货物,必须需要一个DeliveryDriver参与,但如果以后对DeliveryDriver有更多的要求,那我们既要修改DeliveryDriver也要修改上述代码,这样造成的依赖,耦合度高。)
解决方法:
创建DeliveryService接口:
1 | public interface DeliveryService{ |
重构DeliveryDriver类以实现DeliveryService接口:
1 | public class DeliveryDriver implements DeliveryService{ |
重构DeliveryCompany类,使它依赖于一个抽象而不是一个具体的类:
1 | public class DeliveryCompany{ |
好处
- 减少耦合
- 代码更高的复用性