设计原则Go设计模式(3)-设计原则是从相对高的维度来进行代码设计,设计的再好,代码编写不优雅,代码质量也难以得到保障。
即使当时写的很好,如何保证随着业务变化、其他同学的修改,代码仍然是优雅的呢?另外,怎样的代码称之为优雅,又让谁来评判呢?
所以代码编写至少涉及三个问题:
高质量代码的标准是什么?
如何编写高质量代码?
如何保证代码一直高质量?
当然,在代码编写方面,我仍然是个学生,大家要是觉得有价值可以参考一下,如果有错误的地方,也希望大家多多批评指正。
1高质量代码的标准
代码质量评价有很高的主观性。一般最常用到几个评判代码质量的标准有:可维护性、可读性、可扩展性、灵活性、简洁性、可复用性、可测试性。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。
要写出高质量代码,需要善用面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。
Go设计模式(3)-设计原则和Go设计模式(2)-面向对象分析与设计更多讲的是可维护性、可扩展性、灵活性、可复用性。而可读性、简洁性、可测试性则与基础的编码能力相关。
2如何编写高质量的代码
2.1编程规范
仅从编写看起来优雅的代码层面讲,起效最快的是符合编程规范。编程规范其实分两部分:
- 通用规范,各种语言都能用
- 指定语言规范,这是针对各种语言自己特性编写的,如Go、Java、PHP等,网上有很多可以拿来参考
这里我们仅讲一下通用编程规范。通用编程规范一般包含三个方面,命名与注释、代码风格、编程技巧。
命名与注释
命名与注释主要是为了增加可读性。但有部分程序员命名能力实在堪忧(说的就是我),好在找到了一个神器https://unbug.github.io/codelf/,可以查一下其他人是怎么命名的。
- 命名的关键是能准确达意
- 借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名
- 命名要可读、可搜索,不要使用生僻的、不好读的英文单词
- 接口的2中命名方式:在接口中带前缀”I”;在接口的实现类中带后缀”Impl”。抽象类的2中命名方式:带上前缀“Abstract”;不带前缀
- 注释的内容:做什么、为什么、怎么做。复杂的类和接口,还要写明”如何用“
- 类和函数一定要写注释,而且要写的尽可能全面详细
代码风格
- 函数的代码行数不要超过一屏幕的大小,比如50行
- 一行代码最好不要超过IDE的显示宽度
- 善用空行分割单元块
- 推荐两格缩进,节省空间。一定不要用tab键缩进
- 将大括号跟上一条语句同一行,可以节省代码行数,另起新的一行,结构清晰
- 在GoogleJava编程规范中,依赖类按照字母序从小到大排列。类中先写成员变量后写函数,成员变量之间或函数之间,先写静态成员变量或函数,后写普通变量或函数,并且按照作用域大小依次排列
编程技巧
- 将复杂的逻辑提炼拆分成函数和类
- 通过拆分成多个函数或将参数封装为对象的方式,来处理参数过多
- 函数中不要使用参数来做代码执行逻辑的控制
- 函数设计要职责单一
- 移除过深的嵌套层次
- 用字面常量取代魔法数
- 用解释性变量来解释复杂表达式
2.2代码可测试性
代码的可测试性是指针对代码编写单元测试的难易程度。容易编写单元测试就说明代码可测试性好,反之意味代码设计的并不是很合理。
提高代码可测试性有两个手段
- 依赖注入可以通过mock的方法将不可控的依赖变得可控
- 利用二次封装来解决某些代码行为不可控的情况
代码的可测试性也是检查代码是否高质量的一个手段。写完代码之后,一定记得编写单元测试。首先需要思考有哪些测试用例,然后开始编写单元测试,此时可能发现有部分单元测试无法编写。原因一般有如下几条:
- 未决行为:如代码和时间、随机数相关。一般使用二次封装来解决。
- 全局变量:全局变量会导致上一次运行的结果可能影响下一个运行。一般使用reset全局变量解决。可以看看能否不用全局变量。
- 静态方法:不好处理,非要测试只能mock。可以看看能否不使用静态方法。
- 复杂继承:父类需要mock某个依赖对象才能进行单元测试,那所有的子类、子类的子类都需要mock出这个对象。能做单元测试,就是比较麻烦。可以看看是否能改为组合。
- 高耦合代码:依赖十几个外部对象完成工作,需要mock十几个外部对象来进行单元测试。看看是否可以通过重构解耦。
3如何保证代码一直高质量
要想代码随着业务的变更、新人的更改一直保持高质量,除了不断的重构别无它法。这也分情况,如果是新项目,大家技术水平高、都有不断重构的意识、代码Review也会考虑代码质量问题,保持代码高质量相对简单一些。如果是老旧项目、当时活多时间紧,代码质量已经很差了,把这种代码提升到较高质量会难很多。
基于这两种情况,重构方案也有所不同。
3.1重构
对于第一种情况,使用编程规范中的方法进行优化。当然新的业务还是需要用到面向对象设计思想、设计原则、设计模式等。
对于第二种情况,就需要做大型重构了。代码是否需要做大型重构,一线开发人员最有发言权。如果觉得这个代码进行细微改动后不知道会引发什么、或者感觉改不动了、或者这个代码的开发严重影响了项目进度,那肯定是要重构了,不重构难道留着过年?当然,重构是个技术活,但有个前提,需要上下一心,大家统一认识,要有重构的决心。
大型重构主要靠解耦。代码松耦合、高内聚,是控制代码复杂度的有效手段。为了解耦,一般使用如下方案:
- 封装与抽象:有效地隐藏实现的复杂性,隔离实现的 易变性,给依赖的模块提供稳定且易用的抽象接口
- 引入中间层:简化模块或类之间的依赖关系
- 模块化:分而治之
- 使用设计思想和原则:单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特发展
3.2单元测试
重构有风险,重构需谨慎。很多时候大家不敢做重构,主要怕重构引起大问题,影响业务。那如何能保证重构不引起问题呢?理论上没法百分之百保证不会产生问题,但是我们可以通过一些手段来规避风险,其中最有效的就是单元测试。
写单元测试难度不大,所以部分程序员不想写。另外有时候项目会很紧急,没有写单元测试的时间。所以关键问题是团队需要建立对单元测试正确的认识,如果没有单元测试,代码不准上线,通过种种手段,保证单元测试的编写。
其实单元测试有很多好处。
写单元测试的过程本身就是代码Review和重构的过程,能有效地发现代码中的bug和代码设计上的问题。
另外重构的过程中,如果单元测试仍然能够跑通,说明重构质量是可以的。
4实例
上面聊了这么多理论,总得看点代码才行。正好前两天家里人要去医院,所以简单写了一个抢号的功能。当时写的比较急,属于能用就行,趁这次写文章,就拿这个代码来做优化吧。
这个代码是PHP的,毕竟脚本写起来更快一些(PHP是最好的语言)。代码编写和语言关系不大,就优化PHP版的吧,如果大家有兴趣,可以写Go版的。
这个功能只支持一家医院,我希望优化完后,可以快速支持多家医院的抢号。另外默认都是通过微信进行预约。我们只是用这个用例来锻炼,希望大家不要用来做不好的事情。
我们先看一下代码以前的样子:
1 |
|
通过代码可以看出该功能主要包含以下函数:获得当前时间、发送请求、获取该微信号对应的用户id信息、获取病人id信息、获取医生可用时间段、注册功能。而且初步判断整个使用微信注册流程就包含如上步骤。
代码有如下问题:使用面向过程、命名待优化、缺乏注释、复杂逻辑没有拆分为函数和类、有重复逻辑、不支持多个医院、请求链接与参数配置散乱。
我们可以做如下设计:获取当前时间放到工具类里;发送请求放到网络类里;和医院进行交互的操作可以放到一个类里,但是解析结果功能需要能够替换,毕竟每家医院返回结果可能不一致;创建工厂类用于选择不同医院;
修改后代码为:
1 |
|
改写后的代码还有一定的优化空间,如错误返回、函数返回限制、父类和子类的关系限制、代码可测试性检查等。大家有时间可以尝试再优化一下。
总结
写出高质量的代码需要付出的精力比随便写要多得多,但随着不断的练习,速度和质量都会很快的提升。需要先有这个意识、然后掌握一定方法、然后不断实践,最终慢慢成功。