设计原则与思想:规则与重构
理论一:重构的why、what、when、how
重构的目的:为什么重构(why)?
- 对于项目而言,重构可以保持代码质量持续处于一个可控状态,不至于太糟糕(无可救药)
- 对于个人而言,重构非常锻炼一个人的代码能力(很有成就感)
重构的对象:重构什么(what)?
大规模高层次的重构
- 代码分层
- 模块化
- 解耦
- 梳理类之间的交互关系
- 抽象复用组件
- …
小规模低层次的重构
- 规范命名
- 注释
- 修正函数参数过多
- 消除超大类
- 提取重复代码
- …
重构的时机:什么时候重构(when)?
- 建立持续
重构
意识 重构
要融入日常开发中- 而非等到代码出现大问题才
重构
重构的方法:如何重构(how)?
- 大规模高层次的重构
- 有组织、有计划地进行
- 分阶段地小步快跑
- 时刻让代码处于一个可运行的状态
- 小规模低层次的重构
- 随时随地
理论二:单元测试(保证重构不出错的技术手段)
- What:
- 代码层面的测试,用来测试编写代码逻辑的正确性
- 单元一般是类或函数,而不是模块或者系统
- Why:
- 写
单元测试
的过程本身就是代码Code Review和重构的过程,能有效发现代码中的BUG和代码设计上的问题 单元测试
是集成测试
的有力补充- 能帮助我们快速熟悉代码
- 写
- How:
- 针对代码设计各种测试用例,以覆盖各种输入、异常、边界情况,并翻译成代码
单元测试
不要依赖被测代码的具体实现逻辑- 难以编写
单元测试
,往往是代码的可测试性不好(说明要重构了)
- Example:
@RunWith(SpringRunner.class) @SppringBootTest public class UserTest { @Autowired private UserService userService; @Test public void getUser(){ //测试getUserById接口是否正常 User user = userService.getUserById(111L); //user不为空断言(如果为空会报错) Assert.assertNotNull(user); } }
理论三:代码的可测试性
- What:代码的可测试性,就是针对代码编写单元测试的难易程度。
- How:
依赖注入
是编写可测试性代码的最有效手段。(减少不必要的rpc耗时+自定义返回内容)/** * 一个电商系统的交易类 */ public class Transaction { //依赖的外部rpc接口(可能耗时长,不便于单测) private WalletRpcService walletRpcService; //通过外部set方法实现依赖注入(传递参数) public void setWalletRpcService(WalletRpcService walletRpcService) { this.walletRpcService = walletRpcService; } //交易方法 public boolean execute(){ walletRpcService.handle(); //... } } /** * mock的rpc */ public class MockWalletRpcService extends WalletRpcService { //... } /** * 单元测试 */ public class TransactionTest { @Test public void testExecute() { Transaction transaction = new Transaction(); //这里执行我们mock的类(可以自定义返回内容,而非调用实际的WalletRpcService返回不可控的内容以及不可控的耗时) transaction.setWalletRpcService(new MockWalletRpcService()); boolean result = transaction.execute(); //断言,如果result不为true则报错 assertTrue(result); } }
常见的Anti-Patterns(反面模式)
1.代码中包含未决行为逻辑(代码return是随机/不确定的)
public class Demo { /** * 计算入参早于当前时间多少天 * @param dueTime * @return 每天返回的值不同 */ public long calculateDelayDays(Date dueTime) { long currentTime = System.currentTimeMillis(); if(dueTime.getTime() >= currentTime) { return 0; } long delayTime = currentTime - dueTime.getTime(); long delayDays = delayTime / 86400; return delayDays; } }
2.滥用可变全局变量(容易影响其他测试用例)
如果单元测试方法testAdd()和testReduce()是并发执行的,那么可能在assertEquals执行之前value就执行了一次add()和一次reduce(),那么结果就跟我们预期的不一样了/** * 简易计算器 */ public class Counter { public static int value = 0; //加法 public boolean add(int addValue) { } //减法 public boolean reduce(int reduceValue) { } public static int getValue() { return value; } } /** * 单元测试 */ public class CounterTest { public void testAdd() { Counter counter = new Counter(); counter.add(2); assertEquals(2, counter.getValue()); } public void testReduce() { Counter counter = new Counter(); counter.reduce(2); assertEquals(-2, counter.getValue()); } }
3.滥用静态方法(跟
2.滥用可变全局变量
类似)4.使用复杂的继承关系(如果父类需要mock某个依赖对象,那么子类、子类的子类…都需要mock这个依赖对象,会很复杂)
5.高耦合代码(如果一个类依赖几十个外部对象,那我们编写单元测试的时候就可能需要mock几十个外部对象,复杂且不合理)
理论四:代码解耦
- 为什么要解耦?
- 过于复杂的代码往往可读性、可维护性不友好(解耦可以保证代码质量)
- 保证代码松耦合、高内聚(能有效控制代码复杂度)
- 怎么看是否需要解耦?
- 直接的衡量标准是把模块与模块、类与类自己的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重构
- 怎么重构?
- 封装与抽象
- 中间层(如mq)
- 模块化
- 一些设计思想与原则
- 单一职责原则
- 基于接口而非实现编程
- 依赖注入
- 多用组合少用继承
- 迪米特法则(不是很懂。。。。)
- 设计模式(如观察者模式)
理论五:快速改善代码质量的20条编程规范
实战一:如何发现代码质量问题
实战二:如何处理程序出错的返回
- 1.返回错误码
- 是C语言最常用的出错处理方式
- Java语言极少会用到错误码(异常)
- 2.返回NULL值
- 用来表示”不存在”这种语义
- 对于查找函数(如getxxx、queryxxx)来说,数据不存在是一种正常行为(并非异常情况),所以返回NULL值更加合理
public class UserService {
public User getUserById(int id) {
//如果用户不存在,则返回null
return null;
}
}
- 3.返回空对象
- 针对
2.返回NULL值
的弊端:调用方容易不做判空导致出现NPE异常 - 对于字符串类型或者集合类型时,我们可以用空字符串或空集合替代NULL值(表示不存在)
- 针对
- 4.抛出异常对象
- 直接吞掉(原地catch并打印错误日志)
- 直接往上抛出(当前方法不处理)
- 包裹成新的异常抛出(包装成通用的异常返回,不暴露实现细节)