常见代码重构技巧(非常实用)
共 33342字,需浏览 67分钟
·
2021-05-20 02:18
点击上方“码农突围”,马上关注 这里是码农充电第一站,回复“666”,获取一份专属大礼包 真爱,请设置“星标”或点个“在看
关于重构
为什么要重构
编码之前缺乏有效的设计
成本上的考虑,在原功能堆砌式编程
缺乏有效代码质量监督机制
什么是重构
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
代码的坏味道
实现逻辑相同、执行流程相同
方法中的语句不在同一个抽象层级
逻辑难以理解,需要大量的注释
面向过程编程而非面向对象
类做了太多的事情
包含过多的实例变量和方法
类的命名不足以描述所做的事情
发散式变化:某个类经常因为不同的原因在不同的方向上发生变化
散弹式修改:发生某种变化时,需要在多个类中做修改
某个类的方法过多的使用其他类的成员
两个类、方法签名中包含相同的字段或参数
应该使用类但使用基本类型,比如表示数值与币种的Money类、起始值与结束值的Range类
继承打破了封装性,子类依赖其父类中特定功能的实现细节
子类必须跟着其父类的更新而演变,除非父类是专门为了扩展而设计,并且有很好的文档说明
某个实例变量仅为某种特定情况而设置
将实例变量与相应的方法提取到新的类中
仅包含字段和访问(读写)这些字段的方法
此类被称为数据容器,应保持最小可变性
命名无法准确描述做的事情
命名不符合约定俗称的惯例
坏代码的问题
难以复用
系统关联性过多,导致很难分离可重用部分
难于变化
一处变化导致其他很多部分的修改,不利于系统稳定
难于理解
命名杂乱,结构混乱,难于阅读和理解
难以测试
分支、依赖较多,难以覆盖全面
什么是好代码
如何重构
SOLID原则
单一职责原则
开放-关闭原则
里氏替换原则
父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。
接口隔离原则
依赖反转原则
迪米特法则
合成复用原则
设计模式
设计模式:软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案。
创建型:主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码
结构型:主要通过类或对象的不同组合,解耦不同功能的耦合
行为型:主要解决的是类或对象之间的交互行为的耦合
代码分层
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”。
*/
public class 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
}
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
// do something
}
使用卫语句替代嵌套条件判断
//未使用卫语句
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();
}
使用多态替代条件判断断
public int calculate(int a, int b, String operator) {
int result = Integer.MIN_VALUE;
if ("add".equals(operator)) {
result = a + b;
} else if ("multiply".equals(operator)) {
result = a * b;
} else if ("divide".equals(operator)) {
result = a / b;
} else if ("subtract".equals(operator)) {
result = a - b;
}
return result;
}
基于这种场景,我们可以考虑使用“多态”来代替冗长的条件判断,将if else(或switch)中的“变化点”封装到子类中。这样,就不需要使用if else(或switch)语句了,取而代之的是子类多态的实例,从而使得提高代码的可读性和可扩展性。很多设计模式使用都是这种套路,比如策略模式、状态模式。
public interface Operation {
int apply(int a, int b);
}
public class Addition implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}
public class OperatorFactory {
private final static Map<String, Operation> operationMap = new HashMap<>();
static {
operationMap.put("add", new Addition());
operationMap.put("divide", new Division());
// more operators
}
public static Operation getOperation(String operator) {
return operationMap.get(operator);
}
}
public int calculate(int a, int b, String operator) {
if (OperatorFactory .getOperation == null) {
throw new IllegalArgumentException("Invalid Operator");
}
return OperatorFactory .getOperation(operator).apply(a, b);
}
使用异常替代返回错误码
不要使用异常处理用于正常的业务流程控制 异常处理的性能成本非常高 尽量使用标准异常 避免在finally语句块中抛出异常 如果同时抛出两个异常,则第一个异常的调用栈会丢失 finally块中应只做关闭资源这类的事情
//使用错误码
public boolean withdraw(int amount) {
if (balance < amount) {
return false;
} else {
balance -= amount;
return true;
}
}
//使用异常
public void withdraw(int amount) {
if (amount > balance) {
throw new IllegalArgumentException("amount too large");
}
balance -= amount;
}
引入断言
不要滥用断言,不要使用它来检查“应该为真”的条件,只使用它来检查“一定必须为真”的条件
如果断言所指示的约束条件不能满足,代码是否仍能正常运行?如果可以就去掉断言
引入Null对象或特殊对象
//空对象的例子
public class OperatorFactory {
static Map<String, Operation> operationMap = new HashMap<>();
static {
operationMap.put("add", new Addition());
operationMap.put("divide", new Division());
// more operators
}
public static Optional<Operation> getOperation(String operator) {
return Optional.ofNullable(operationMap.get(operator));
}
}
public int calculate(int a, int b, String operator) {
Operation targetOperation = OperatorFactory.getOperation(operator)
.orElseThrow(() -> new IllegalArgumentException("Invalid Operator"));
return targetOperation.apply(a, b);
}
//特殊对象的例子
public class InvalidOp implements Operation {
@Override
public int apply(int a, int b) {
throw new IllegalArgumentException("Invalid Operator");
}
}
提炼类
//原始类
public class Person {
private String name;
private String officeAreaCode;
private String officeNumber;
public String getName() {
return name;
}
public String getTelephoneNumber() {
return ("(" + officeAreaCode + ")" + officeNumber);
}
public String getOfficeAreaCode() {
return officeAreaCode;
}
public void setOfficeAreaCode(String arg) {
officeAreaCode = arg;
}
public String getOfficeNumber() {
return officeNumber;
}
public void setOfficeNumber(String arg) {
officeNumber = arg;
}
}
//新提炼的类(以对象替换数据值)
public class TelephoneNumber {
private String areaCode;
private String number;
public String getTelephnoeNumber() {
return ("(" + getAreaCode() + ")" + number);
}
String getAreaCode() {
return areaCode;
}
void setAreaCode(String arg) {
areaCode = arg;
}
String getNumber() {
return number;
}
void setNumber(String arg) {
number = arg;
}
}
组合优先于继承
// Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedHashSet() { }
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
@Override
public int size() { return s.size(); }
@Override
public boolean isEmpty() { return s.isEmpty(); }
@Override
public boolean contains(Object o) { return s.contains(o); }
@Override
public Iterator<E> iterator() { return s.iterator(); }
@Override
public Object[] toArray() { return s.toArray(); }
@Override
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override
public boolean add(E e) { return s.add(e); }
@Override
public boolean remove(Object o) { return s.remove(o); }
@Override
public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
@Override
public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
@Override
public boolean retainAll(Collection<?> c) { return s.retainAll(c); }
@Override
public boolean removeAll(Collection<?> c) { return s.removeAll(c); }
@Override
public void clear() { s.clear(); }
}
// Wrappter class - uses composition in place of inheritance
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedHashSet1(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
只有当子类真正是父类的子类型时,才适合继承。对于两个类A和B,只有两者之间确实存在“is-a”关系的时候,类B才应该继承A;
在包的内部使用继承是非常安全的,子类和父类的实现都处在同一个程序员的控制之下;
对于专门为了继承而设计并且具有很好的文档说明的类来说,使用继承也是非常安全的;
其他情况就应该优先考虑组合的方式来实现
接口优于抽象类
现有的类可以很容易被更新,以实现新的接口。
接口是定义混合类型(比如Comparable)的理想选择。
接口允许构造非层次结构的类型框架。
接口的变量修饰符只能是public static final的
接口的方法修饰符只能是public的
接口不存在构造函数,也不存在this
可以给现有接口增加缺省方法,但不能确保这些方法在之前存在的实现中都能良好运行。
因为这些默认方法是被注入到现有实现中的,它们的实现者并不知道,也没有许可
Java 8 之前我们知道,一个接口的所有方法其子类必须实现(当然,这个子类不是一个抽象类),但是 java 8 之后接口的默认方法可以选择不实现,如上的操作是可以通过编译期编译的。这样就避免了由 Java 7 升级到 Java 8 时项目编译报错了。Java8在核心集合接口中增加了许多新的缺省方法,主要是为了便于使用lambda。
例如在 List 等集合接口中都有一些默认方法,List 接口中默认提供 replaceAll(UnaryOperator)、sort(Comparator)、、spliterator()等默认方法,这些方法在接口内部创建,避免了为了这些方法而专门去创建相应的工具类。
在 Java 8 之前我们可能需要创建一个基类来实现代码复用,而默认方法的出现,可以不必要去创建基类。
优先考虑泛型
// 比较三个值并返回最大值
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x;
// 假设x是初始最大值
if ( y.compareTo( max ) > 0 ) {
max = y; //y 更大
} if ( z.compareTo( max ) > 0 ) {
max = z; // 现在 z 更大
} return max; // 返回最大对象
}
public static void main( String args[] ) {
System.out.printf( "%d, %d 和 %d 中最大的数为 %d\n\n", 3, 4, 5, maximum( 3, 4, 5 ));
System.out.printf( "%.1f, %.1f 和 %.1f 中最大的数为 %.1f\n\n", 6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ));
System.out.printf( "%s, %s 和 %s 中最大的数为 %s\n","pear", "apple", "orange", maximum( "pear", "apple", "orange" ) );
}
不要使用原生态类型
要尽可能地消除每一个非受检警告
利用有限制通配符来提升API的灵活性
//List<? extends E>
// Number 可以认为 是Number 的 "子类"
List<? extends Number> numberArray = new ArrayList<Number>();
// Integer 是 Number 的子类
List<? extends Number> numberArray = new ArrayList<Integer>();
// Double 是 Number 的子类
List<? extends Number> numberArray = new ArrayList<Double>();
//List<? super E>
// Integer 可以认为是 Integer 的 "父类"
List<? super Integer> array = new ArrayList<Integer>();、
// Number 是 Integer 的 父类
List<? super Integer> array = new ArrayList<Number>();
// Object 是 Integer 的 父类
List<? super Integer> array = new ArrayList<Object>();
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
静态成员类优于非静态成员类
匿名类(anonymous class)
局部类(local class)
静态成员类(static member class)
非静态成员类(nonstatic member class)
优先使用模板/工具类
分离对象的创建与使用
public class BusinessObject {
public void actionMethond {
//Other things
Service myServiceObj = new Service();
myServiceObj.doService();
//Other things
}
}
public class BusinessObject {
public void actionMethond {
//Other things
Service myServiceObj = new ServiceImpl();
myServiceObj.doService();
//Other things
}
}
public class BusinessObject {
private Service myServiceObj;
public BusinessObject(Service aService) {
myServiceObj = aService;
}
public void actionMethond {
//Other things
myServiceObj.doService();
//Other things
}
}
public class BusinessObject {
private Service myServiceObj;
public BusinessObject() {
myServiceObj = ServiceFactory;
}
public void actionMethond {
//Other things
myServiceObj.doService();
//Other things
}
}
对象的创建者耦合的是对象的具体类型,而对象的使用者耦合的是对象的接口。也就是说,创建者关心的是这个对象是什么,而使用者关心的是它能干什么。这两者应该视为独立的考量,它们往往会因为不同的原因而改变。
可访问性最小化
私有的(private修饰)--只有在声明该成员的顶层类内部才可以访问这个成员;
包级私有的(默认)--声明该成员的包内部的任何类都可以访问这个成员;
受保护的(protected修饰)--声明该成员的类的子类可以访问这个成员,并且声明该成员的包内部的任何类也可以访问这个成员;
公有的(public修饰)--在任何地方都可以访问该成员;
如果类或接口能够做成包级私有的,它就应该被做成包级私有的;
如果一个包级私有的顶层类或接口只是在某一个类的内部被用到,就应该考虑使它成为那个类的私有嵌套类;
公有类不应直接暴露实例域,应该提供相应的方法以保留将来改变该类的内部表示法的灵活性;
当确定了类的公有API之后,应该把其他的成员都变成私有的;
如果同一个包下的类之间存在比较多的访问时,就要考虑重新设计以减少这种耦合;
可变性最小化
声明所有的域都是私有的
声明所有的域都是final的
如果一个指向新创建实例的引用在缺乏同步机制的情况下,从一个线程被传递到另一个线程,就必须确保正确的行为
不提供任何会修改对象状态的方法
保证类不会被扩展(防止子类化,类声明为final)
防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏该类的不可变行为
确保对任何可变组件的互斥访问
如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用。在构造器、访问方法和readObject 方法中使用保护性拷贝技术
除非有很好的理由要让类成为可变的类,否则它就应该是不可变的;
如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性;
除非有令人信服的理由要使域变成非final的,否则要使每个域都是private final的;
构造器应该创建完全初始化的对象,并建立起所有的约束关系;
质量如何保证
测试驱动开发
测试驱动开发(TDD)要求以测试作为开发过程的中心,要求在编写任何代码之前,首先编写用于产码行为的测试,而编写的代码又要以使测试通过为目标。TDD要求测试可以完全自动化地运行,并在对代码重构前后必须运行测试。
TDD的开发周期
两个基本的原则
仅在测试失败时才编写代码并且只编写刚好使测试通过的代码
编写下一个测试之前消除现有的重复设计,优化设计结构
分层测试点
参考资料
重构-改善既有代码的设计 设计模式 Effective Java 敏捷软件开发与设计的最佳实践 实现模式 测试驱动开发
- END - 最近热文
• 如何写出让同事无法维护的代码? • 86版西游记“红孩儿”成中科院博士!做CTO身价过亿! • 刘强东的代码水平到底有多牛? 网友:95年一个晚上赚5万! • 一些恶心的代码片段