项目在不断演进过程中,代码不停地在堆砌。如果没有人为代码的质量负责,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。
造成这样的原因往往有以下几点:
-
编码之前缺乏有效的设计
-
成本上的考虑,在原功能堆砌式编程
-
缺乏有效代码质量监督机制
对于此类问题,业界已有有很好的解决思路:通过持续不断的重构将代码中的“坏味道”清除掉。
什么是重构
重构一书的作者Martin Fowler对重构的定义:
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
根据重构的规模可以大致分为大型重构和小型重构:
大型重构:对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入bug的风险也会相对比较大。
小型重构:对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名和注释、消除超大类或函数、提取重复代码等等。小型重构更多的是使用统一的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入bug的风险相对来说也会比较小。什么时候重构 新功能开发、修bug或者代码review中出现“代码坏味道”,我们就应该及时进行重构。持续在日常开发中进行小重构,能够降低重构和测试的成本。
代码的坏味道
代码重复
-
实现逻辑相同、执行流程相同
方法过长
-
方法中的语句不在同一个抽象层级
-
逻辑难以理解,需要大量的注释
-
面向过程编程而非面向对象
过大的类
-
类做了太多的事情
-
包含过多的实例变量和方法
-
类的命名不足以描述所做的事情
逻辑分散
-
发散式变化:某个类经常因为不同的原因在不同的方向上发生变化
-
散弹式修改:发生某种变化时,需要在多个类中做修改
严重的情结依恋
-
某个类的方法过多的使用其他类的成员
数据泥团/基本类型偏执
-
两个类、方法签名中包含相同的字段或参数
-
应该使用类但使用基本类型,比如表示数值与币种的Money类、起始值与结束值的Range类
不合理的继承体系
-
继承打破了封装性,子类依赖其父类中特定功能的实现细节
-
子类必须跟着其父类的更新而演变,除非父类是专门为了扩展而设计,并且有很好的文档说明
过多的条件判断
过长的参数列
临时变量过多
令人迷惑的暂时字段
-
某个实例变量仅为某种特定情况而设置
-
将实例变量与相应的方法提取到新的类中
纯数据类
-
仅包含字段和访问(读写)这些字段的方法
-
此类被称为数据容器,应保持最小可变性
不恰当的命名
-
命名无法准确描述做的事情
-
命名不符合约定俗称的惯例
过多的注释
坏代码的问题
-
难以复用
-
系统关联性过多,导致很难分离可重用部分
-
难于变化
-
一处变化导致其他很多部分的修改,不利于系统稳定
-
难于理解
-
命名杂乱,结构混乱,难于阅读和理解
-
难以测试
-
分支、依赖较多,难以覆盖全面
什么是好代码
代码质量的评价有很强的主观性,描述代码质量的词汇也有很多,比如可读性、可维护性、灵活、优雅、简洁。这些词汇是从不同的维度去评价代码质量的。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。
要写出高质量代码,我们就需要掌握一些更加细化、更加能落地的编程方法论,这就包含面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。
如何重构
SOLID原则
单一职责原则
一个类只负责完成一个职责或者功能,不要存在多于一种导致类变更的原因。
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、松耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
开放-关闭原则
添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。
里氏替换原则
子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
子类可以扩展父类的功能,但不能改变父类原有的功能
父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。
接口隔离原则
调用方不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
依赖反转原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。
迪米特法则
一个对象应该对其他对象保持最少的了解
合成复用原则
尽量使用合成/聚合的方式,而不是使用继承。
单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。
设计模式
设计模式:软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。
-
创建型:主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码
-
结构型:主要通过类或对象的不同组合,解耦不同功能的耦合
-
行为型:主要解决的是类或对象之间的交互行为的耦合
代码分层
模块结构说明
-
server_main:配置层,负责整个项目的module管理,maven配置管理、资源管理等;
-
server_application:应用接入层,承接外部流量入口,例如:RPC接口实现、消息处理、定时任务等;不要在此包含业务逻辑;
-
server_biz:核心业务层,用例服务、领域实体、领域事件等
-
server_irepository:资源接口层,负责资源接口的暴露
-
server_repository:资源层,负责资源的proxy访问,统一外部资源访问,隔离变化。注意:这里强调的是弱业务性,强数据性;
-
server_common:公共层,vo、工具等
代码开发要遵守各层的规范,并注意层级之间的依赖关系。
命名规范
一个好的命名应该要满足以下两个约束:
准确描述所做得事情
格式符合通用的惯例
如果你觉得一个类或方法难以命名的时候,可能是其承载的功能太多了,需要进一步拆分。
约定俗称的惯例
类命名
类名使用大驼峰命名形式,类命通常使用名词或名词短语。接口名除了用名词和名词短语以外,还可以使用形容词或形容词短语,如 Cloneable,Callable 等,表示实现该接口的类有某种功能或能力。
方法命名
方法命名采用小驼峰的形式,首字小写,往后的每个单词首字母都要大写。和类名不同的是,方法命名一般为动词或动词短语,与参数或参数名共同组成动宾短语,即动词 + 名词。一个好的函数名一般能通过名字直接获知该函数实现什么样的功能。
重构技巧
提炼方法
多个方法代码重复、方法中代码过长或者方法中的语句不在一个抽象层级。
方法是代码复用的最小粒度,方法过长不利于复用,可读性低,提炼方法往往是重构工作的第一步。
意图导向编程:把处理某件事的流程和具体做事的实现方式分开。
-
把一个问题分解为一系列功能性步骤,并假定这些功能步骤已经实现
-
我们只需把把各个函数组织在一起即可解决这一问题
-
在组织好整个功能后,我们在分别实现各个方法函数
/**
* 1、交易信息开始于一串标准ASCII字符串。
* 2、这个信息字符串必须转换成一个字符串的数组,数组存放的此次交易的领域语言中所包含的词汇元素(token)。
* 3、每一个词汇必须标准化。
* 4、包含超过150个词汇元素的交易,应该采用不同于小型交易的方式(不同的算法)来提交,以提高效率。
* 5、如果提交成功,API返回”true”;失败,则返回”false”。
*/
publicclass Transaction {
public Boolean commit(String command) {
Boolean result = true;
String[] tokens = tokenize(command);
normalizeTokens(tokens);
if (isALargeTransaction(tokens)) {
result = processLargeTransaction(tokens);
} else {
result = processSmallTransaction(tokens);
}
return result;
}
}
以函数对象取代函数
将函数放进一个单独对象中,如此一来局部变量就变成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数。
引入参数对象
方法参数比较多时,将参数封装为参数对象
移除对参数的赋值
public int discount(int inputVal, int quantity, int yearToDate) {
if (inputVal > 50) inputVal -= 2;
if (quantity > 100) inputVal -= 1;
if (yearToDate > 10000) inputVal -= 4;
return inputVal;
}
public int discount(int inputVal, int quantity, int yearToDate) {
int result = inputVal;
if (inputVal > 50) result -= 2;
if (quantity > 100) result -= 1;
if (yearToDate > 10000) result -= 4;
return result;
}
将查询与修改分离
任何有返回值的方法,都不应该有副作用
-
不要在convert中调用写操作,避免副作用
-
常见的例外:将查询结果缓存到本地
移除不必要临时变量
临时变量仅使用一次或者取值逻辑成本很低的情况下
引入解释性变量
将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途
if ((platform.toUpperCase().indexOf("MAC") > -1)
&& (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize > 0) {
// do something
}
finalboolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
finalboolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
finalboolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
// do something
}
使用卫语句替代嵌套条件判断
把复杂的条件表达式拆分成多个条件表达式,减少嵌套。嵌套了好几层的if - then-else语句,转换为多个if语句
//未使用卫语句
public void getHello(int type) {
if (type == 1) {
return;
} else {
if (type == 2) {
return;
} else {
if (type == 3) {
return;
} else {
setHello();
}
}
}
}
//使用卫语句
public void getHello(int type) {
if (type == 1) {
return;
}
if (type == 2) {
return;
}
if (type == 3) {
return;
}
setHello();
}
使用多态替代条件判断断
当存在这样一类条件表达式,它根据对象类型的不同选择不同的行为。可以将这种表达式的每个分支放进一个子类内的复写函数中,然后将原始函数声明为抽象函数。
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站不拥有所有权,不承担相关法律责任。如发现有侵权/违规的内容, 联系QQ15101117,本站将立刻清除。