代码重构原则与技巧

代码可读性是衡量代码质量的重要标准,可读性也是可维护性、可扩展性的保证,因为代码是连接程序员和机器的中间桥梁,要对双边友好。

随着项目在不断演进过程中,代码不停地在堆砌,如果没有人为代码的质量负责,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。

坏代码的问题

  • 难以复用
    系统关联性过多,导致很难分离可重用部分

  • 难于变化
    一处变化导致其他很多部分的修改,不利于系统稳定

  • 难于理解
    命名杂乱,结构混乱,难于阅读和理解

  • 难以测试
    分支、依赖较多,难以覆盖全面

坏代码的缘由

  • 编码之前缺乏有效的设计
  • 成本上的考虑,在原功能堆砌式编程
  • 缺乏有效代码质量监督机制

工作中常见的坏代码(“坏味道”):

在开发工作中,我们往往能够发现一些不好的设计,例如技术团队做内部的代码review时,或者是SonarQube等代码工具的扫描提醒。坏代码尤其常见于在新同事开发,或者是工期紧迫不得不降低工程质量的时候。下面做下详细举例

代码重复

实现逻辑相同、执行流程相同,例如重复的工具类或业务逻辑方法,常见于代码介绍不周全且多人开发时导致的代码重叠

参考建议:
约定好代码分层设计,如DDD结构,务必避免逻辑在各层次(如controller、service、domain、repo层)分散,做好逻辑收敛(到service、domain层),这样可以避免大部分代码重叠
完善代码文档。当然,若指导文档较少、严重依赖口口相传时,应该考虑结对编程,重视代码review
把通用的逻辑方法、工具类下放到底层,或者是封装对应的工具包,并做好宣讲、介绍
不恰当的命名

类的命名不足以描述所做的事情
命名无法准确描述做的事情、不符合约定俗称的惯例

参考建议:
不适用无意义、无含义的命名,避免有排序性质的后缀(没有人知道这个后缀为什么这样弄),当然也要避免过长(单词数<5)
尽量避免使用一些编程语法、通用中间件相关的关键字(如channel),这样有利于架构代码和业务代码区分开,能够更好地识别出业务逻辑
类方法名可通过重载方式使统一命名,这样更利于代码的阅读,例如用query代替queryByABCD
推荐在需求讨论阶段做DDD的讨论,使能够统一开发语言(业务的术语、命名)
过大的类

类做了太多的事情
包含过多的实例变量和方法

参考建议:
按照“单一职责原则”,类的逻辑应该主核心逻辑,类成员方法应有明显聚合性,其余不相关、弱相关的逻辑可拆分出去
参考重载、重写等面向对象思想,丰富类的方法、方法参数,但注意限制数量,避免带来糟糕的可读性
方法过长

方法中的语句不在同一个抽象层级
逻辑难以理解,需要大量的注释
面向过程编程而非面向对象(如逻辑扁平化、面条化、过多if-else条件判断)

参考建议:
按照“单一职责原则”,类成员方法也应该有核心逻辑,不妨拆解出多个方法,按照主、子方法(如Java的public、private之分)的思想,简化主逻辑代码、突出主逻辑流程
按照“开放封闭原则”,如果方法在上线后还有持续修改(无论是入参调整还是if分支增减),说明这个方法设计得非常糟糕,应该在前期设计时就落地适当的设计模式(推荐策略工厂模式)
对于过多的if-else分支,参考策略工厂模式拆分成多个类,使if逻辑分散到各个策略类中,后期若有if逻辑的增删则开发对应的策略类即可,这样也具备了平台化设计的雏形
临时变量过多

参考【方法过长】场景

逻辑分散

发散式变化:某个类经常因为不同的原因在不同的方向上发生变化
散弹式修改:发生某种变化时,需要在多个类中做修改

参考建议:
可能是代码分层设计没做好,导致逻辑在各层次(如controller、service、domain、repo层)分散,技术团队需做好相关代码规范、约定
可能是业务逻辑没做好收敛,做不到“高内聚、低耦合”,这很可能需要做重构设计
严重的情结依恋

某个类的方法过多的使用其他类的成员

参考建议:
按照“接口分离原则”,可能是接口层功能过于复杂,导致后续代码也臃肿,不妨先考虑下拆分、简化API
可能是代码分层设计没做好,导致了循环依赖、代码重复,最终可能会越来越混乱,技术团队需做好相关代码规范、约定
可能是逻辑过于复杂,不妨使用“行为型”设计模式做下优化,如策略模式(条件逻辑水平分化)、责任链模式(过程节点化拆解)、模板模式(归总公共逻辑,凸显主逻辑)
不合理的继承体系

继承打破了封装性,子类依赖其父类中特定功能的实现细节
子类必须跟着其父类的更新而演变,除非父类是专门为了扩展而设计,并且有很好的文档说明

参考建议:
接口应该是功能的描述,且应该遵循“接口分离原则”、“依赖倒置原则”
父类(抽象父类)应该作为抽象层而存在,其属性、方法应该是较为通用的、面向所有子类的、不仅仅是为某个别场景服务的。
具体的业务逻辑、面向业务场景的具体实现,应该下放到子类,当多个子类中出现共同的逻辑代码段,则该代码段可以上升到父类
过长的参数列
参考建议:
使用业务类来代替基本类型,比如表示数值与币种的Money类、起始值与结束值的Range类
令人迷惑的临时变量
参考建议:
对于单例类的成员变量(全局变量),要慎重使用,因为可能在多线程场景下被其他线程修改了而本线程又无法感知
务必定义一个有意义的命名
思考变量是否能作为常量,若能则优先以静态变量的形式,这样也可以规避“魔法数”坏代码
若临时变量较多、相关逻辑较多时,不妨封装到新类里面,加入到BO层(Business Object,区别于VO、DTO、PO)
数据类包含业务逻辑(属性方法)

数据类应该纯粹地仅包含字段和访问(读写)这些字段的方法,应保持最小可变性

过多的注释或者过时的注释
参考建议:
关于过多的注释:实际中应该少见(爱写注释的程序员不多见),若实在过多,酌情精简下描述即可
关于过时的注释:往往是修改代码时没有顺带修改相关的注释,这也是不好的习惯,给后续维护带来麻烦。对此,注释应该和代码一起同删同改
综上,有注释总比没注释要好,考虑到“两三个月不接触就会发生遗忘”的原则,代码应该保留足够的注释,以便自己、他人回顾阅读。建议20行内必有注释,能做到10行则更佳

什么是好代码

代码质量的评价有很强的主观性,描述代码质量的词汇也有很多,比如可读性、可维护性、灵活、优雅、简洁。这些词汇是从不同的维度去评价代码质量的。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。
要写出高质量代码,我们就需要掌握一些更加细化、更加能落地的编程方法论,这就包含面向对象设计的原则、设计模式、编码规范、重构技巧等。

面向对象设计的原则(SOLID)

单一职责原则(Single Responsibility Principle, SRP):简单地说:接口职责应该单一,不要承担过多的职责,因此一个类应该有且只有一个引起它变化的原因。工作中我们需要迭代调整代码以应对需求变更,如果职责越多,职责间存在耦合越大,意味着可能潜在的变化点越多,变化的概率和风险越大,后续实现改变的需求难度越大。因此,如果在设计过程中发现一个类承担的职责太多,最直接有效的解决方式就是按职责 "拆分"。

开放封闭原则(Open Closed Principle,OCP):一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。允许通过扩展的方式增加系统行为,禁止通过直接修改源码的方式增加系统行为。简单地说:就是当别人要修改软件功能的时候,他不能修改我们原有代码,只能新增代码实现软件功能修改的目的。例如代码中存在if-else语句,且随着功能拓展不得不追加,这是拓展性糟糕的表象,应该做好功能抽象、逻辑解耦、分层设计,这里推荐使用简单工厂+策略模式。

里氏替换原则(Liskov Substitution Principle,LSP):所有引用基类的地方必须能透明地使用其子类的对象。简单地说:所有父类能出现的地方,子类就可以出现,并且替换了也不会出现任何错误。这就要求子类的所有相同方法,都必须遵循父类的约定,否则当父类替换为子类时就会出错。这里父类的约定,不仅仅指的是语法层面上的约定,还包括实现上的约定。

接口分离原则(Interface Segregation Principle,ISP):类间的依赖关系应该建立在最小的接口上,即:接口的内容一定要尽可能地精简,能有多小就多小。这样做也是为了更好地隔离变化,降低内部改动导致的风险。在软件设计中,建议不要将一个大而全的接口扔给使用者,而是将每个使用者关注的接口进行隔离,分别提供不同的接口服务。

依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖底层具体逻辑,应该依赖其抽象(接口)。即接口或抽象类不依赖于实现类,在工作中我们应该面向接口编程,通过抽象成接口,使各个子类的实现彼此独立,实现类之间的松耦合,而此时接口时应该具备通用的业务特性,能够满足对外开放使用。

迪米特法则:一个对象应该对其他对象保持最少的了解,以降低代码间的耦合。
合成复用原则:尽量使用合成/聚合的方式,而不是使用继承。

总而言之
依赖倒置原则告诉我们要面向接口编程;
当面向接口编程之后,接口隔离原则和单一职责原则又告诉我们设计接口的时候要精简单一,要注意职责的划分;
当我们职责划分清楚后,里氏替换原则告诉我们在使用继承时,要注意遵守父类的约定;
依赖倒置、接口隔离、单一职责、里氏替换的最终目标都是为了实现开闭原则;
开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。

设计模式

设计模式:软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。设计模式可以分为三大类:创建型、结构型、行为型。

  • 创建型

创建型模式,就是创建对象的模式,抽象了实例化的过程。它帮助一个系统独立于如何创建、组合和表示它的那些对象。关注的是对象的创建,创建型模式将创建对象的过程进行了抽象,也可以理解为将创建对象的过程进行了封装,作为客户程序仅仅需要去使用对象,而不再关心创建对象过程中的逻辑。包括:
(常用)
工厂模式(Factory Pattern)
抽象工厂模式(Abstract Factory Pattern)
单例模式(Singleton Pattern)
建造者模式(Builder Pattern)
(不常用)
原型模式(Prototype Pattern)

  • 结构型

结构型模式是为解决怎样组装现有的类,设计他们的交互方式,从而达到实现一定的功能的目的。结构型模式包容了对很多问题的解决。例如:扩展性(外观、组成、代理、装饰)、封装性(适配器,桥接)。包括:
(常用)
代理模式(Proxy Pattern)
适配器模式(Adapter Pattern)
桥接模式(Bridge Pattern)
装饰器模式(Decorator Pattern)
过滤器模式(Filter、Criteria Pattern)
(不常用)
组合模式(Composite Pattern)
外观模式(Facade Pattern)
享元模式(Flyweight Pattern)

  • 行为型

主要解决的是类或对象之间的交互行为的耦合,是为了让代码逻辑更灵活,有利于实现高聚合、低耦合。包括:
(常用)
责任链模式(Chain of Responsibility Pattern)
策略模式(Strategy Pattern)
模板模式(Template Pattern)
迭代器模式(Iterator Pattern)
状态模式(State Pattern)
观察者模式(Observer Pattern)
(不常用)
命令模式(Command Pattern)
解释器模式(Interpreter Pattern)
中介者模式(Mediator Pattern)
备忘录模式(Memento Pattern)
空对象模式(Null Object Pattern)
访问者模式(Visitor Pattern)

各个模式之间的关系图(参见《设计模式》第8页):

设计模式差异汇总:
1、单例模式和工厂模式:

在实际开发中,一般会把工厂类写成单例模式;

2、策略模式和工厂模式:

1)策略模式属于行为模式,工厂模式属于创建型模式;
2)工厂模式在于封装对象的创建,策略模式在于接收工厂创建的对象,从而实现不同的行为。

3、策略模式和委派模式:

1)策略模式是委派模式的一种内部实现形式,策略模式关注的结果是能否相互贴换。
2)委派模式不是GOF23种设计模式,更多关注分发,调度的过程。

4、策略模式和模板模式:

1)策略模式和模板模式都是行为模式;
2)策略模式与模板模式都有封装算法,策略模式重点是不同的算法之间可以相互贴换,模板模式重点是定义一套流程。
3)策略模式可以改变算法流程,可以替代代码中的if...else...分支;模板模式不能改变算法的流程。

5、装饰器模式和静态代理:

1)装饰器模式强调给对象动态添加方法,而代理更注重控制对 对象的访问。
2)代理模式和装饰器模式都持有对方的引用,但逻辑处理重心不一样。

6、装饰器模式和适配器模式:

1)装饰者模式和适配器模式都是属于包装器模式;
2)装饰者模式可以实现被装饰者与相同的接口或者继承被装饰者作为它的子类,而适配器和被适配者可以实现不同的接口。

代码质量工具举例

checkstyle:编译过程产生代码质量提示(可定义warn、error级别)
SonarQube:能够检查几乎所有内容,如代码质量,格式,变量声明,异常处理等,支持超过25种编程语言,有免费的社区版本和其他付费版本。

posted @ 2023-09-20 13:09  鱼007  阅读(130)  评论(0编辑  收藏  举报