设计原则
SOLID包含五个原则:单一职责,开闭原则,里氏替换原则,接口隔离原则,依赖反转原则
单一原则(Single Responsibility Principle)- 指导代码实现
定义
一个类只完成一个职责或者功能,如果包含了两个不相干的功能,需要拆分成多个类
特点
- 视角不同的多变性:从一个角度看可能是合理的,但是从其他角度可能就是不合理的,要结合实际的业务发展场景来看,不满足条件的话就可以继续拆分
优点
- 高内聚性:遵循单一原则可以使类或模块的职责更加清晰和具体,使其更容易理解和维护
- 可维护性 : 当一个类或模块只负责一个单一的职责时,它的变化引起的影响范围较小
- 可测试性 : 单一职责使得类或模块的行为更加明确,更容易进行单元测试。每个测试只需要关注一个特定的职责,测试用例的编写和维护变得更加简单。
缺点
- 职责界定困难 : 职责的界定可能会存在主观性和模糊性,导致错误的划分,进而降低了代码的可读性
- 过度拆分: 过于追求单一原则,可能导致类或模块数量的增加,从而增加了代码的复杂性和理解的难度
过度拆分的反例:一个Gson的序列化和反序列化为例,如果将这两个函数拆分成两个类,虽然类的职责更加清晰了,但是代码的可读性变得很差,而且修改了一个类中的序列化方法,但是忘记修改另一个类中的反序列化方法,还容易导致序列化反序列化不对齐报错,降低了可维护性
判断条件
- 代码体量:类中的代码函数,属性更多
- 命名:难以给类起一个合适的名字或者业务名词概括
- 类的依赖:类依赖的其他类过多(因为引入一个类要引入很多相关类)
- 处理逻辑拆分:是否包含的私有方法过多,考虑将私有方法独立到新的类中,并设置新的public方法
- 存在大量热属性:类中的大量方法都是在操作几个属性,这种可以将共性的高频使用属性抽离出来
最佳实践
- 先写一个粗粒度的类,满足业务要求,持续拆分持续重构
- 可以通过类的行数,私有和公有方法,高频使用的变量和属性
最后再加一些对未来产品需求方向的延伸性预测,需要和产品沟通,增加业务产品思维
开闭原则(Open Closed Principle)- 指导后续逻辑扩展
定义
主旨:指导复杂逻辑扩展
对扩展开放,对修改关闭。 添加一个新的功能应该是在已有代码上扩展代码,而非修改已有代码(注意是并非完全的隔绝代码修改,而是以最小的修改代价完成新功能的开发)
优点
- 代码扩展性更好 : 将代码中的不确定性上浮,对后续的修改和扩展更友好
- 提升代码的可维护性:不用对已有的代码进行侵入,减少引入新的 bug;不用修改原先的单测,减少单测的维护工作
- 有效解决一个函数参数过多且职责不单一的问题
缺点
- 代码可阅读性下降 :容易过度设计, 可能将一个功能在有限的操作逻辑拆成复杂的逻辑
判断条件
- 新增的代码没有破坏原有代码的正常运行
- 新增的代码没有影响原来单元测试
如果原有一个检查逻辑 check() 有部分检查逻辑,随着业务的扩展要加检查逻辑则需要修改这个函数的实现(甚至调整入参),带来两个问题
- 调用这个 check()函数的代码都需要修改
- 这个 check()原先覆盖的单测也需要重改
最佳实践 - 一个通过XXXHandler的例子
-
将函数的需要的入参整合抽象在一起
-
将一段复杂逻辑中可以独立拆分的逻辑拆成多个handler
-
整体逻辑入口需要维护List
-
包含add和remove handler逻辑
-
入口函数调用实现是遍历List依次执行
-
里氏替换原则(Liskov Substitution Principle)- 指导规范继承
主旨:规范继承。 按照协议来设计(子类要遵循父类的协议)
定义
描述了继承关系的行为准则, 子类对象能够替换能够替换父类对象出现的任何地方,保证原来程序的逻辑行为不变或者正确性不被破坏
判断条件
用于指导关系中的子类是如何设计的,子类要保证替换父类的时候,不改变原有程序逻辑,以及不破坏原有程序的正确性,具体包含以下几类:
- 子类违背父类声明要实现的功能
- 子类违背父类对输入,输出,异常的约定
- 子类违背父类注释中所罗列的任何特殊情况说明(比如逻辑中父类逻辑不能金额透支,子类逻辑允许金额透支)
举个🌰
- 父类的方法是transaction(),不会有校验后错误返回
- 但是子类继承父类的 transaction(),加上了对 appid 和 name 的校验,如果校验出错后要抛出错误,就破坏了里氏替换原则
优点
- 提升可扩展性:遵循里氏替换原则可以使代码更加灵活和可扩展。新的派生类可以无缝替换基类,而不需要修改已有的代码,从而减少了代码对于继承关系的耦合性;
- 提升代码的可复用性:通过继承关系,可以将通用的代码逻辑放在基类中,派生类可以共享基类的功能和行为,从而提高了代码的可复用性;
- 提高代码的可读性和可维护性:遵循里氏替换原则可以使代码的结构更加清晰和易于理解。派生类的行为和基类保持一致,使得代码更易于维护和调试(可以复用测试逻辑)
缺点
- 存在滥用继承的问题:继承应该基于真正的"is-a"关系,而不是为了代码重用而强行使用继承;
- 存在违反单一职责原则的可能:在尝试满足里氏替换原则的同时,可能会导致派生类承担过多的责任,违反了单一职责原则
解决方法 - 验证方式
-
用父类的单测去验证子类是否可以跑的通过
接口隔离原则(Interface Segregation Principle)- 指导接口拆分
定义
一个类对于其他类的依赖应该建立在最小的接口集合上,而不是依赖于不需要的接口
优点
- 降低类之间的耦合度:遵循接口隔离原则可以将一个庞大臃肿的接口拆分成多个精细的接口,类之间的依赖关系更加清晰明确
- 促进接口的复用:接口隔离原则鼓励设计出精细、小巧的接口,这些接口可以被多个类所复用。
缺点
- 过度设计导致的接口的粒度划分可能过细:过度遵循接口隔离原则可能导致接口的粒度过细,接口数量增多,增加了代码的复杂性和维护成本。
判断条件 & 最佳实践
-
一组API接口:其中的接口是不是有些模块不应该或者无需使用,按业务拆成多组接口
-
单个API接口:看看调用者是不是只依赖一个接口的部分功能,把包含多个逻辑的函数拆分成几个粒度更小的函数
依赖倒置原则(Dependency Inversion Principle) - 指导模块的调用关系
主旨:指导框架中模块的调用关系。
定义
高层模块(服务调用者)不依赖低层模块(服务提供者),他们共同依赖同一个抽象。抽象不依赖具体实现细节,具体实现细节依赖抽象
举个🌰:Tomcat为高层模块,Web应用程序为低层模块,Tomcat没有直接依赖Web应用程序的实现,两者都依赖Sevlet规范,且Sevlet规范不依赖两者的实现细节。
优点
- 解耦合:依赖倒置原则能够减少模块之间的直接依赖,提高代码的灵活性和可维护,比如扩展外面的依赖,但是父类不需要修改;
- 可测试性:依赖倒置原则使得模块的依赖可以通过抽象接口进行Mooc,便于进行单元测试
缺点
- 增加抽象层次:依赖倒置原则通常需要引入抽象接口和抽象类,增加了代码的抽象层次,可能增加了代码量和复杂性。在设计时需要权衡好抽象的粒度和设计的复杂性。
解决方案 - 判断条件 & 最佳实践
-
后续扩展性考虑: 如果高层模块的功能经常变化或需要支持多种具体实现,那么使用依赖倒置可以提供更灵活的扩展和替换。
-
可测性较差:如果需要对模块进行单元测试或模块测试,使用依赖倒置可以方便地通过替换依赖对象来进行测试。
-
高低模块依赖方向问题:检查依赖关系的方向后发现高层模块直接依赖于低层模块的具体实现,那么需要依赖倒置重构。
依赖倒置原则的一种实现 - 控制反转 Inversion of Control
定义
- 控制:对程序流程的控制
- 反转:整个流程的控制权从程序员反转到了框架
控制反转通过将对象的创建和依赖关系的管理交给外部容器来完成,从而实现了模块之间的解耦。
依赖注入(Dependency Injection)
- 定义:不在类内部通过new()创建依赖类对象,而是将依赖类的对象在外面创建好后,通过构造函数和函数参数方式传递进来
依赖注入框架(DI Framework)
-
定义:程序员配置类所需要传入的类对象以及类之间的依赖关系,框架通过自动化方式实现自动创建对象、管理对象生命周期、依赖注入操作
-
应用场景:Android的Dagger和ButterKnife框架都是基于DI框架实现的
补充 - Android类获取依赖的三种方式
-
直接在类中通过new构造
-
从框架API中获取(例如Android API getSystemService(),getContext(),内部也是获取的单例)
-
以参数提供(依赖注入),具体分两种
- 构造函数注入
- 以字段的setter注入(activity 和 fragment的初始化,因为生命周期交由Android系统的,由系统去创建和销毁)