单一职责
【一句话描述】:
一个类或一个模块只负责完成一个职责(或功能),即保持“高内聚、低耦合”。
【详解】
不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。 但是在判断功能是否单一时,需要根据实际使用情况做出判断。
这里有一些可以参考的原则,来判断类是否满足单一职责:
- 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
- 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
- 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
- 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
- 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。
开闭原则
【一句话描述】:
一个类或者模块在实现的时候需要保持对扩展开放、对修改关闭。
【详解】
这个原则看起来很简单,但是实践时并不容易,因为编写代码时总是会有地方需要修改。在应用这个原则的时候,要修改影响的范围具体分析。比如增加了一个类的字段,对这个类而言是一处修改;但是如果这种修改没有改变这个类本身的其它方法、或者依赖这个类的其它类在使用这个类时并没有改动,可以算作是扩展。这里可以根据是否有破坏原有代码的正常使用,破坏原有的单元测试来分析。
开闭原则本质上是讲代码的扩展性,如果一个代码在编写时就充分考虑了未来的扩展性,进行了一定的抽象、预先留好了扩展点,那么在未来需求变动时,能够不修改代码的整体接口、只在特定地方进行增量修改,那么可以认为这部分代码是符合了对扩展开放、对修改关闭。
里式替换
【一句话描述】
子类在重写父类函数时,可以改变内部逻辑,但是不能改变父类函数原有的约定。
【详解】
里式替换是用来指导继承关系中,子类该如何设计的一个原则,先前所说的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
子类改变父类函数约定的一些情况如下:
-
子类违反父类的功能逻辑
比如父类的一个排序方法、是按照创建时间升序排列的,但是子类重写时改成了按照更新时间降序排列的
-
子类违背父类对于入参、出参、异常的约定
比如对于同一函数,父类在输入为空时,抛异常;而子类在输入为为空时,返回空。
-
子类违背父类的特殊说明
比如父类定义的函数规定只能查询余额,但是子类在实现时除了可以查询余额、还能更新余额。
接口隔离
【一句话描述】
接口隔离是指调用方在使用接口时,不需要去依赖它本身不需要的接口。
【详解】
接口隔离的英文解释是“Client should not be forced to depend upon interfaces that they do not use.”,即客户端不应该被依赖它不需要的接口。这里可以从三个方面来进行理解,一是对一个接口而言、二是对接口中的某个函数而言、三是对于OOP中的接口概念。
- 把接口理解为一组API接口集合
对于一个接口而言,如果这个接口里面有部分函数、只有部分情况才会被调用方使用,那么可以考虑将这部分函数抽取出来作为另外一个接口,让两者隔离开。 - 把接口理解成一个接口函数
对于一个接口的函数而言,有些函数的功能大而全,但是调用方多数情况下只使用其中的一部分功能,这种情况下可以把原函数拆分成几个函数,让每个函数的功能单一。 - 把接口理解成OOP中的接口
这部分和多用组合、少用继承很类似。有些时候我们会定义一个大而全的接口,让子类去实现这个大而全的接口,但是子类实际上只需要实现接口的部分函数、并不需要完整实现,因此可以根据功能将这个大而全的接口拆分成多个子接口,让子类自己去实现这些接口。
依赖反转
【一句话描述】
依赖反转是一个设计原则,是指高层模块不依赖底层模块,高层模块和低层模块应该通过抽象来相互依赖。
【详解】
高层模块是指调用者,底层模块就是被调用者,在它们两者之间需要加入一个中间层进行抽象,从而让高层和底层隔离,避免直接依赖,从而在修改时都不会对对方造成影响。这里举2个例子来说明:Java的web程序可以在Tomcat中运行,这里Tomcat就是高层模块、web应用程序是低层模块,两者没有直接依赖关系,但是都依赖"servlet"规范;Java应用程序访问数据库驱动时,并不是直接使用驱动的,而是通过JDBC这个中间层的抽象来访问数据库。
而Spring中有控制反转的功能,因此这里集中讲解一下控制反转、依赖注入。
-
控制反转
控制反转,英文名称为Inversion Of Control,是一种比较笼统的设计思想,一般用来指导框架层面的设计。这里所说的“控制”,指的是对程序执行流程的控制;有“反转”就有“正转”,“正转”即是之前由程序员控制程序的执行流程,“反转”后将程序的执行流程交由框架来控制。 -
依赖注入
依赖注入,即Dependency Injection,是控制反转的一种实现方式。Spring的IOC容器就是通过依赖注入的方式,动态创建对象。
KISS\YAGNI原则
【一句话描述】
kiss原则,即keep it simple and stupid,主要是在编写函数时,要尽量保持简单、提高可读性;YAGNI 原则的英文全称是:You Ain’t Gonna Need It,意思是不要去编写当前用不到的代码,不要过度设计。
【详解】
这两个原则易懂,但是评价标准比较主观,不做过多解释。
DRY
【一句话描述】
即do not repeat yourself,提高代码的复用性。
【详解】
项目中时长会发现重复的代码,根据代码重复情况分为以下几种情况:
- 实现逻辑重复
实现逻辑相同,但是作用的类型、对象、语义是不同的函数,可以不提取公共方法,避免后续修改的时候出问题。 - 功能语义重复
名字不同,但是完成的功能是一样的;这种情况要统一用一个方法,把多余的方法给去掉。 - 代码执行重复
代码里面有部分逻辑重复或多余,比如判断语句在函数及其子函数被重复调用;这种情况是需要合并方法、修改的。
提高代码复用性的方法
-
减少代码耦合
-
满足单一职责原则
让类、接口能够提高高内聚、低耦合特性。
-
模块化
要将功能独立的代码,封装成模块
-
业务与非业务逻辑分离
将与业务无关的代码抽取出来,与业务代码分离,抽取成类库、公共组件。
-
通用代码下沉
通用的代码应该尽量放在底层
-
继承、多态、抽象、封装
通过继承来复用父类的公共方法,通过多态来动态替换一段代码中的逻辑、让代码复用。抽象与封装更多是一种思路,越抽象、越不依赖具体实现的代码,越容易复用。
-
应用模板等设计模式