这里写目录标题
软件设计六大原则基本概念
最少依赖(迪米特)
任何一个类只能使用本身的方法和属性及根据参数传递进来的值,每一个类尽量减少对其他类的依赖,减少类藕合,高内聚,利于提高代码复用。
单一职责
一个类应只负责一个职责,各个职责变动不应影响其他职责,提高代码可读性(责任链完美诠释)
接口隔离
即接口类必须最小封装,防止实现类必须实现其不需要的接口。
里氏替换
子类必须可以替代父类,子类必须完全实现父类的抽象方法,不允许重写重载非抽象方法。
开闭原则
对新增开放,对修改闭合,尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化
依赖倒置
高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象,抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
如何设计高可用,高复用,易拓展的软件
六大原则最重要的是——依赖倒置。1-4其实可以统称提高内聚,降低藕合,至于开闭原则更多的是一个结果表现,你如果严格遵循依赖倒置,其实就等同于达成了开闭原则。
高可复用性
一般指上层应用,调用者模块,上层应用主要负责业务逻辑,复用价值高于底层模块。
一个简单的例子
现有一个button按钮类,控制lamp灯泡开关,直接翻译业务,button直接依赖lamp,类图如下:
在新增了一个风扇类,想将button控制风扇,如何做?
大家可能会有复制粘贴一个新的button类(复用度低),又或者在button类内部ifelse,添加对风扇的依赖以达到目的(风扇灯泡放在一起不易维护)。
正确的做法,是将button的直接依赖于底层实现,改为抽象接口ButtonSever,对应lamp风扇实现抽象接口,对应重构类图如下:
无论以后新增控制电脑,控制风扇等,都可直接复用button类,只要这些电脑、风扇等实现ButtonSever接口即可。
高层button不依赖与lamp,而依赖于buttonserve这个抽象接口,底层lamp同样依赖buttonserve,底层lamp是buttonserve的实现。实现依赖倒置。
嵌入式软件如何实现硬件无关
如上例子,拓展开来,button对应我们的嵌入式软件,lamp,fan,pc对应硬件底层。我们不能直接在业务代码里面写ifelse(或编译开关)做插桩式硬件分支控制,我们需要做的就是嵌入式软件中直接依赖硬件实现的地方,更改为依赖硬件抽象,而这一层硬件抽象在业内有一个统一的称呼HAL(Hardware Abstraction Layer),MCU开发、嵌入式linux,嵌入式安卓都有类似的概念。具体硬件通过实现抽象,并且硬件实现类的实例对象不通过用户显式生成,而通过配置文件等方式动态配置进用户程序中(控制反转)。即可较好的实现软件硬件解耦。
控制反转
控制反转最好的实现就是,就是运行时根据配置文件指定的类名动态生成,而不是根据代码显示指定对象,在java中,动态语言先天带有反射机制,可以较优雅的实现该功能,c++没有该机制则需自行实现。
个人常用实现,是使用map或list结构存储加载全部的生成硬件实现类对象的函数指针或者单例,根据配置文件动态加载生成对应对象。
在其次则是,在硬件注册的时候使用编译宏或者ifelse做显式加载了,这种属于比较偷懒的实现。只不过相对而言,只对注册有影响,已经对业务代码侵入少,维护成本可接受。
易用性
类的易用性
提高类内聚,降低类耦合,最少依赖,更改类不会牵一发而动全身。
组件的易用性
该小段指的,是提供组件给用户使用的易用性,市场上的开源组件,主要包括两大类:
- 上层组件(框架容器等,如tomcat,spring,gRPC)
- 下层组件(类库api等)
上层组件,字面意思处于上层,在代码调用上属于调用者的位置,用户代码其实属于底层,用户代码只需按照上层组件接口规范实现其接口,即可使用框架的特性。
而下层组件,处于被调用端,想要使用组件的功能,用户代码必须调用下层组件方法,代码侵入程度高。
所以说相对而言,上层组件的易用性是优于下层组件,业务允许的情况下,相比下层应用,提供上层应用供用户使用。
可拓展性
包括两种业务拓展和接口拓展。
业务可拓展性
上面内容略有提及,开闭原则,业务不依赖底层,依赖接口,业务的拓展根据接口的新实现来完成。再通过配置文件指导业务对象的生成。
接口可拓展性
带入公司业务,假设一种情景,接口类Handle(人脸识别),类HandleA(无口罩人脸),HandleB(有口罩人脸)实现接口类,类图如下。
现在类HandleA,HandleB不在满足业务需求,方法前后,需要同时增加其他处理(环境光预处理等等)做增强,需要外部接口结构保持不变,如何处理?
- 直接修改实现类?复制粘贴复用部分?
- 派生实现类A、B?面向对象无论c++还是java,谷歌华为阿里都推出过开发规范,都要求优先使用组合而不是继承(特指派生),非要使用也不建议超过三层(太多层增加阅读成本),六大原则中也有相关约束。(派生是一个强耦合的概念),并且继承只能从一个派生(多继承命名污染冲突等问题),每个handle都派生导致类爆炸。
比较好的做法:
void EnhancedHandleA::Process()
{
PreProcess();
innerContext.Process();
PostProcess();
}
不要过度设计
设计模式是世界历史开发过程中被人们归纳总结出开的一系列行之有效的方法,是在一些常见场景下六大设计模式的具体体现,是归纳发现的而不是发明出来的,一定要根据具体业务,而不能生搬硬套。可根据具体业务做设计模式改造(比如lombok里面实现的建造者模式),甚至可以做反模式设计,只要利于具体业务的话。
学会在敏捷开发和过度设计中寻找平衡点。
设计的本质就是识别和表达系统难点,找到系统的变化点,并隔离变化点。如上文中的cmd解析处理等,功能迭代必然会添加更多的命令,基本上对任何产品系统都是非常易变的系统节点,在我工作开发过的几个软件开发类似功能基本都是会做抽象分离,多做总结归纳。