前言
在之前的编码中,更多的是注重的是代码规范等方面,自以为代码风格方面还是不错的。在开始对自己的代码进行单元测试之后才知道,即使代码风格看着还不错,注释很齐全,但要进行测试还是比较有难度,会导致很难设计测试用例和执行,往往一个类方法测试需要3、4次mock,导致单元测试代码就难以维护。更别说业务代码进行扩展迭代。
目标
写完本篇后希望达到以下几点目标
- 1.对代码范式可以熟记于胸,在日后编码时可以避免大量设计错误
- 2.整理几个简单的标准或约定,提高代码质量
- 3.梳理代码质量判断准则,可以简单、有效判断自己代码质量
代码终极目标
- 1.实现需求
- 2.提高代码质量和可维护性(包括可读性、可扩展性)
设计原则 & 设计模式
软件设计原则: 原则为我们提供指南,它告诉我们什么是对的,什么吃错误的。它不会告诉我们如何解决问题,它仅仅给出一些准则,以便我们可以设计好的软件,避免不良设计。
软件设计模式: 模式是在软件开发过程中总结得出的一些可重用的解决方案,它能解决一些实际的问题。
基本概念
SOLID 是Michael Feathers推荐的便于记忆的首字母简写,它代表了Robert Martin命名的最重要的五个面对对象编码设计原则:
- S: 单一职责原则 Single Responsibility Principle (SRP)
- O: 开发封闭原则 Open/Closed Principle (OCP)
- L: 里氏替换原则 Liskov Substitution Principle (LSP)
- I: 接口隔离原则 Interface Segregation Principle (ISP)
- D: 依赖反转原则 Dependency Inversion Principle (DIP)
名词介绍
- OOD: 面向对象设计(Object-Oriented)
- DIP: 依赖反转(也叫依赖倒置),
软件设计原则
- IOC: 控制反转,
软件设计模式
(Inversion of Control) - DI: 依赖注入(Dependency Injection)
- IOC Container: 控制反转容器,也是依赖注入容器,用来映射依赖,管理对象创建和生存周期
单一职责原则(SRP)
一个对象只为一个元素负责,分清楚 必须做
和 可以做
的事情。每个角色做好必须做的事情就很好了。如果还有一些事情没人做,那就创造角色让他去做。
场景
交警在路边可以去劝阻路边打架斗殴的,在民警未到时也应该去劝阻,但交警就该去劝架了吗?道路交通怎么办?让民警又干什么?在纠纷频发的地方如果只有交警而无民警,那是治安体制有问题没在那里安插民警,而不是交警袖手旁观。
优点
- 1.类的复杂度降低,一个类只负责一个功能,其逻辑要比负责多项功能简单的多。
- 2.类的可读性增强,阅读起来轻松。
- 3.可维护性强,一个易读、简单的类自然也容易维护。
- 4.变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
开放封闭原则(OCP)
对扩展开放,对修改关闭
开放-封闭原则的思想就是设计的时候,尽量让设计的类做好后就不再修改,如果有新的需求,通过新加类的方式来满足,而不去修改现有的类(代码)。那么在实际的项目开发中,是否能做到绝对的对修改关闭呢?答案一般也是否定的。既然这样,那么就要求我们在开发前,去找出变化点,然后针对变化点构造抽象,隔离出这些变化。由此可见,实现开闭原则关键是抽象。
场景
当你想增加自己的御寒能力只用在身体外加衣服而非做个开胸手术。软件体也一样,观察人体这个造物者的完美之作,把它的规律用在软件体上,就可以造出更完美的软件。好的设计可以让你在为系统新增功能时添加新代码即可而无需修改老代码。
优点
- 1.具有灵活性,通过拓展一个功能模块即可实现功能的扩充,不需修改内部代码。
- 2.具有稳定性,表现在基本功能类不允许被修改,使得被破坏的程度大大下降。
里氏替换原则(LSP)
可以使用任何派生类替换基类,子类可以扩展父类的功能,但不能改变父类原有的功能,即程序逻辑不变。
尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承。
场景
为何要遵循这个原则?比如说QData Standard,T5是T4的下一代,T4可以完成的功能、达到的性能,T5也可以达到,在不考虑硬件、停机等情况下,T5是可以直接替代T4提供服务的。
优点
- 1.代码共享,减少创建类的工作量,每个子类都拥有父类的所有属性和方法。
- 2.提高代码的可重用性。
- 3.提高代码的可扩张性。
- 4.提高产品或项目的开放性。
缺点
- 1.继承是入侵性的,拥有父类的属性和方法。
- 2.降低代码的灵活性,必须拥有父类的属性和方法。
- 3.增强耦合性,父类属性或方法改变,需要考虑子类。
里氏替换原则与多态关联?
拿交学费的例子来说,学校通知交学费(基类中定义交学费抽象方法),多态体现在不同学院(派生类)的同学交的学费是不一样的,里氏替换原则体现在所有学院同学做的都是交学费这件事情。
接口隔离原则(ISP)
建立单一接口,不要建立臃肿庞大的接口。即接口尽量细化,同时接口中的方法尽量的少,保证纯洁性。
客户端不应该依赖它不需用的接口(Clients should not be forced to depend upon interfaces that they don’t use)。
类间的依赖关系应该建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)。
根据接口隔离原则拆分接口时,必须首先满足单一职责原则。
优点
使用接口隔离原则,意在设计一个短而小的接口和类,符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性。
注意事项
- 1.接口要尽量小
- 2.接口要高内聚
高内聚就是提高接口,类,模块的处理能力,减少对外的交互。接口是对外的承诺,承诺越少对系统开发越有利,变更的风险就越少。
- 3.接口设计是有限度的
接口的设计粒度越小,系统越灵活。但是灵活的同时也带来了结构复杂,开发难度大,可维护性降低。所以接口设计是注意度。
开发建议
- 1.一个接口只服务于一个子模块或业务逻辑
- 2.通过业务逻辑压缩接口中的public方法,接口要不断的精简,以达到接口不断完善
- 3.已经被污染的接口,尽量去修改,若变更的风险较大,则采用适配器进行转化处理
接口隔离原则与单一职责原则有什么区别呢?
接口隔离原则与单一职责原则的审视角度不相同。单一职责原则要求是类和接口的职责单一,注重的是职责,这是业务逻辑上的划分。接口隔离原则要求接口的方法尽量少。
比如一个赛马的程序,需要两个功能一是记每匹马跑的圈数,另一个是计算谁是对每匹马计算最终得分。 这两个功能有一点的联系,但是外部调用有可能只需要统计每匹马跑的圈数。所以要把这两个功能写到一个类里面。否则违反了单一职责原则。
依赖倒置原则(DIP)
转换了依赖,高层模块不依赖低层模块的实现,而低层模块依赖于高层模块定义的接口。高层模块定义接口,低层模块负责实现。
高层模块不应依赖于低层模块,两者应该依赖于抽象
抽象接口不应该依赖于具体实现,具体实现依赖于抽象
因此也引申出 IOC
, DI
, IOC容器
等概念
场景
只要有一张银行卡(当然这卡得有钱,还得知道密码),随便到哪一家银行的ATM都能取钱。ATM相当于高层模块,银行卡相当于低层模块。ATM定义了一个插口(接口),所有的银行卡都可以插入使用。即ATM机不关心具体哪个银行的卡。ATM机定义好银行卡的规格参数(接口),所有实现了这种规格参数的银行卡都能在ATM上取钱。
优点
- 系统更柔韧: 可以修改一部分代码而不影响其他模块
- 系统更健壮: 可以修改一部分代码而不会让系统崩溃
- 系统更高效: 组件松耦合,且可复用,提高开发效率
控制反转(IOC, DIP的具体实现方式)
软件设计模式,它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制。
- IOC有两种常见的实现方式:
- 依赖注入
- 服务定位
依赖注入(DI, IOC的具体实现方式)
使用钩子再原来执行流程中注入其他对象,即将依赖对象的创建和绑定转移到被依赖对象类的外部来实现。
- 实现方式
- 1.构造函数注入: 通过
__init__
在初始化时注入 - 2.属性注入: 在实例化后直接
属性赋值
注入 - 3.接口注入:
调用实例方法
注入
- 1.构造函数注入: 通过
IOC容器(DI框架)
软件系统在没有引入IOC容器之前,对象A
依赖于对象B
,那么对象A在初始化或者运行到某一点的时候,自己必须主动
去创建对象B
或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
软件系统在引入IOC容器之后,这种情形就完全改变了,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后的对比,我们不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。
对象A依赖于对象B,当对象 A需要用到对象B的时候,IOC容器就会立即创建一个对象B送给对象A。IOC容器就是一个对象制造工厂,你需要什么,它会给你送去,你直接使用就行了,而再也不用去关心你所用的东西是如何制成的,也不用关心最后是怎么被销毁的,这一切全部由IOC容器包办。
- 反转: 获得依赖对象的过程被反转了
- 注入: 由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。
技术剖析
IOC中最基本的技术就是 反射(Reflection)
编程。根据给出的类名(字符串方式)来动态生成对象。
我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言的的反射编程,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。
缺陷
- 1.软件系统中由于引入了第三方IOC容器,生成对象的步骤变得更复杂。
- 2.由于IOC容器生成对象是通过发射方式,在运行效率上有一定的损耗。
- 3.IOC框架产品(比如: Spring)可能需要大量配置工作。
扩展
一个网页会有很多频道,于是,我们的前端工程师进入到各个页面为各种频道开发他们的页面,随着频道越来越多,前端开发工程师的人数也越来越多,每增加一个频道,就要增加一个为这个频道服务的前端团队,于是,人数越来越多,干成了劳动密集型。为什么不反转控制,倒置依赖呢?前端的同学完全可以开发出各种页面的标准组件,布局,模板,以前与后端交互框架,然后,让后端的同学反过来依赖于前端的标准,使用前端的框架,前端的布局,模板,和组件,以向前端接入后端的模块。
约定
- 1.
接口类
以大写I
开头进行标识 - 2.
抽象类
以大写A
开头进行标识
单元测试
结合实际业务代码,按照五大原则进行优化。针对优化后的代码进行单元测试。
测试方法
- 1.选用测试框架pytest + mock 进行测试
- 2.针对项目中大量和数据库交互场景,使用测试库替换掉正式库进行测试,同时把数据源换成较轻量的sqlite。
思考
- 1.看完这篇博客并不能立马让你代码质量有显著提升,能让你意识到该注意代码质量的重要性已经足够
- 2.代码质量的提升不是一朝一夕的事,需要长期的积累和训练
总结
对于设计模式的五大设计原则,单一职责原则主要说明类的职责要单一;里氏替换原则强调不要破坏继承体系;依赖倒置原则描述要面向接口编程;接口隔离原则讲解设计接口的时候要精简;开闭原则讲述的是对扩展开放,对修改关闭。
三个关键点
- 1.解耦,解耦,解耦: 尽量地让你的模块不要在实现上耦合,而是耦合某个规范,某个标准。
- 2.KISS,KISS,KISS(Keep It Simple, Stupid): 要做到高度解耦,你的模块就一定要很简单,当然不是说简单到只有几行代码,而是简单到只干一件事,并把这件事干到极致。然后通过某个标准拼装起来。
- 3.拼装,拼装,拼装: 当我想用一个模块的时候,我直接调用就好了,没有必要像C或Java一样,还要编译。是的,拼装需要一个框架,需要一种标准协议,然后让所有的系统都耦合在这种规范上,各自独立运行,就像一个机器上的各个部件一样,当我觉得这个部件不爽,换了就是了。