设计模式之美笔记6

记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步

重构

1. 重构的目的:为什么要重构why

1. 概念

软件设计大师Martion Fowler定义:重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。

可以把重构理解为,在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量。

2. 为什么重构

  1. 重构是时刻保持代码质量的一个极其有效的手段,不至于代码腐化到无可救药的地步。项目在演进,代码在堆砌,没人维护质量的话,代码会越来越混乱,到一定程度,量变引起质变。
  2. 优秀的代码或架构不是一开始就完全设计好的,重构不可避免。
  3. 重构是避免过度设计的有效手段。真正遇到问题时,再对代码重构,有效避免前期投入太多时间做过度设计。
  4. 此外,重构对工程师本身技术的成长也很重要。重构实际上就是将学过的设计思想、设计原则、设计模式、编程规范等理论知识,应用到实践的很好的场景,锻炼熟练使用这些理论知识的能力。此外,将烂代码重构为好代码也很有成就感。“初级工程师在维护代码,高级工程师在设计代码,资深工程师在重构代码。”

3. 重构的对象:到底重构什么what

大重构包括:系统、模块、代码结构、类与类之间的关系等重构。重构的手段有:分层、模块化、解耦、抽象可复用组件等。影响较大,代码改动较多,难度大,耗时长,引入bug的风险相对较大。

小重构指的是对类、方法、变量的代码级别的重构,如规范命名、规范注释、消除超大类或方法、提取重复代码等。

4. 重构的时机:什么时候重构when

可持续、可演进的方式,持续重构。修改、添加某个功能代码的时候,顺手把不符合规范、不好的设计重构下。让重构作为开发的一部分,成为一种开发习惯。

持续重构意识更重要,时刻具有持续重构意识,才能避免开发初期就过度设计。

5. 重构的方法:如何重构how

大重构:提前做好完善的重构计划,分阶段进行。每阶段完成一部分的重构,然后提交、测试、运行,发现没问题,再继续下一阶段的重构,保证代码仓库的代码一直可运行、逻辑正确。每个阶段,控制好重构影响到的代码范围,考虑好如何兼容老的代码逻辑,必要时写一些过渡代码。每个阶段最好一天内完成。

小重构:除了人工发现低层次的质量问题,还可借助静态代码分析工具,如CheckStyle、FindBugs、PMD,自动发现代码中的问题,针对性的重构优化。

重构这种事情,资深工程师、项目leader要负责,没事就重构代码,时刻保证代码质量处于良好状态。否则,一旦出现破窗效应,一个人往里面堆了烂代码,之后会有更多烂代码,此外,最好打造好的技术氛围,驱动大家主动关注代码质量,持续重构代码。

2. 保证重构不出错的落地手段

单元测试unit testing

1. 概念

单元测试由研发工程师自己编写,用来测试自己写的代码的正确性。相对于集成测试integration testing来说,测试粒度更小。集成测试的测试对象是整个系统或某个功能模块,如测试用户注册、登录功能是否正常,端到端测试。而单元测试测试对象是类或方法,测试一个类或方法是否按照预期的逻辑执行,代码层级的测试。

单元测试主要看是否能设计出覆盖各种正常及异常情况的测试用例,来保证代码再任何预期或非预期的情况下都能正确运行。

2. 为什么写单元测试

  • 单元测试能有效帮你发现代码中的bug 坚持编写完善的单元测试,写出的代码几乎是bug free的,节省了fix低级bug的时间,也会赢得很多人的认可。拉开和其他人的差距。
  • 写单元测试能帮你发现代码设计上的问题 对于一段代码,如果很难写单元测试,说明代码设计的不合理,如没有使用依赖注入、大量使用静态方法、全局变量、代码高度耦合等
  • 单元测试是对集成测试的有力补充 程序的bug一般都出现在一些边界条件、异常情况下。如除数未判空、网络超时等。单元测试利用mock的方式,控制mock的对象返回我们需要模拟的异常,测试代码在异常情况下的表现 此外,底层bug少了,集成测试时的bug也会少很多
  • 写单元测试的过程本身就是代码重构的过程 相当于就是对代码的一次自我code review,发现设计上的问题以及代码编写的问题(如边界处理不当),然后针对性的重构
  • 阅读单元测试能帮我们快速熟悉代码 单元测试实际就是用户用例,反映了代码的功能和如何使用。知道代码实现了什么功能,有哪些特殊情况需要考虑,有哪些边界条件需要处理
  • 单元测试是TDD可落地执行的改进方案 测试驱动开发Test-Driven Development,TDD经常被提及但很少被执行的开发模式。先写代码,紧接着写单元测试,最后根据单元测试反馈的问题,回头重构代码,更容易落地

3. 如何编写单元测试

java比较出名的单元测试框架如Junit、TestNG、Spring Test等,提供了通用的执行流程(如执行测试用例的TestCaseRunner)和工具类库(如各种Assert判断函数)。只需关注测试用例本身的编写即可。

此外, 一些经验总结:

  • 写单元测试真的是件很耗时的事情吗 单元测试的代码量可能是被测代码本身的1-2倍,写的过程很繁琐,但并不耗时,毕竟不需要考虑代码设计的问题,实现较简单。不同测试用例之间简单copy即可
  • 对单元测试的代码质量有什么要求 不用线上运行,质量放低些,命名可稍微不规范,代码稍微重复,没问题
  • 单元测试只要覆盖率高就够了嘛 覆盖率较容易量化,但并不是唯一标准。没必要过度关注。
  • 单元测试需要了解代码的实现逻辑吗 只关心被测方法实现什么功能,不依赖具体实现
  • 如何选择单元测试框架 不必过度关注框架,如果自己写的代码用已选定的框架无法测试,说明代码的可测试性不好,需要重构代码

4. 单元测试为何难落地

知易行难,开发任务紧,放低对单元测试的要求,慢慢就都不写了。关键是没有建立对单元测试正确认知。

3. 代码的可测试性

1. 编写可测试代码的案例

其中,Transaction是抽象简化之后的一个电商系统的交易类,用来记录每笔订单交易的情况。Transaction类的execute()方法负责执行转账操作,将钱从买家的钱包转到卖家的钱包。真正的转账操作是通过调用WalletRpcService RPC服务来完成的。此外,代码还涉及一个分布式锁DistributedLock单例类,用来避免Transaction并发执行,导致用户的钱被重复转出。

public class Transaction{
	private String id;
	private Long buyerId;
	private Long sellerId;
	private Long productId;
	private String orderId;
	private Long createTimestamp;
	private Double amount;
	private STATUS status;
	private String walletTransactionId;

	//...get() methods...

	public Transaction(String preAssignedId,Long buyerId,Long sellerId,Long productId,String orderId){
		if(preAssignedId!=null && !preAssignedId.isEmpty()){
			this.id = preAssignedId;
		}else{
			this.id = IdGenerator.generateTransactionId();
		}
		if(!this.id.startWith("t_")){
			this.id = "t_"+preAssignedId;
		}
		this.buyerId = buyerId;
		this.sellerId = sellerId;
		this.productId = productId;
		this.orderId = orderId;
		this.createTimestamp = System.currentTimestamp();
		this.status = STATUS.TO_BE_EXECUTED;
	}

	public boolean execute() throws InvalidTransactionException{
		if(buyerId==null || (sellerId==null || amount<0.0)){
			throw new InvalidTransactionException(...);
		}
		if(status==STATUS.EXECUTED) return true;
		boolean isLocked = false;
		try{
			isLocked = RedisDistributedLock.getSingletonInstance().lockTransaction(id);
			if(!isLocked){
				return false;//锁定未成功,返回false job兜底执行
			}
			if(status==STATUS.EXECUTED) return true;//double check
			long executionInvokedTimestamp = System.currentTimestamp();
			if(executionInvokedTimestamp - createdTimestamp > 14days){
				this.status = STATUS.EXPIRED;
				return false;
			}
			WalletRpcService walletRpcService = new WalletRpcService();
			String walletTransactionId = walletRpcService.moveMoney(id,buyerId,seller,amount);
			if(walletTransactionId!= null){
				this.walletTransactionId = walletTransactionId;
				this.status = STATUS.EXECUTED;
				return true;
			}else {
				this.status = STATUS.FAILED;
				return false;
			}
		}finally{
			if(isLocked){
				RedisDistributedLock.getSingletonInstance().unlockTransaction(id);
			}
		}
	}
}

这段代码较为复杂,要如何写单元测试呢?

在Transaction类中,主要逻辑集中在execute()方法中,所以它是测试的重点对象。为了尽可能全面覆盖各种正常和异常情况,针对该方法,设计6个测试用例:

  1. 正常情况下,交易执行成功,回填用于对账(交易与钱包的交易流水)用的walletTransactionId,交易状态设置为EXECUTED,方法返回true
  2. buyer、sellerId为null,amount小于0,返回InvalidTransactionException
  3. 交易过期(createTimestamp超过14天),交易状态设置为EXPIRED,返回false
  4. 交易已执行status为EXECUTED,不再重复执行转钱逻辑,返回true
  5. 钱包WalletRpcService转钱失败,交易状态设置为FAILED,方法返回false
  6. 交易正在执行,不会被重复执行,方法直接返回false

对于上述测试用例,第2个实现很简单,重点看其中的1和3

先看测试用例1的代码实现,具体:

public void testExecute(){
	Long buyerId = 123L;
	Long sellerId = 234L;
	Long orderId = 456L;
	Long productId = 345L;
	Transaction transaction  = new Transaction(null,buyerId,sellerId,productId,orderId);
	boolean executedResult = transaction.execute();
	assertTrue(executedResult);
}

execute()方法的执行以来两个外部的服务,一个是RedisDistributedLock,一个是WalletRpcService,导致上面的单元测试代码存在下面问题:

  • 如果要让单元测试能够运行,需要搭建Redis服务和Wallet RPC服务,搭建和维护成本较高
  • 还要保证将伪造的transaction数据发送给Wallet RPC服务之后,能正确返回期望的结果,而Wallet RPC服务可能是第三方的,不可控。
  • Transaction的执行跟redis、rpc服务通信,走网络,耗时会较长,对单元测试的邢娜娜会有影响
  • 网络终端、超时、redis、rpc服务的不可用,都会影响单元测试的执行

回到单元测试的定义看,主要是测试程序员自己编写的代码逻辑的正确性,并非端到端的集成测试,不需要依赖外部系统(分布式锁、Wallet RPC服务)的逻辑正确性。如果代码依赖外部环境或不可控组件,如需要依赖数据库、网络通信、文件系统等,需要将被测代码和外部系统解依赖,这种方法叫mock,也就是用一个假的服务替换真正的服务,mock的服务完全在我们的控制下,模拟输出我们想要的数据。

如何mock呢?有两种,手动mock和利用框架mock。利用框架mock仅仅是为了简化代码编写。展示手动mock,通过继承WalletRpcService类,并且重写其中的moveMoney()方法的方式实现mock。

public class MockWalletRpcServiceOne extends WalletRpcService{
	public String moveMoney(Long id,Long fromUserId,Long toUserId,Double amount){
		return "123bac";
	}
}

public class MockWalletRpcServiceTwo extends WalletRpcService{
	public String moveMoney(Long id,Long fromUserId,Long toUserId,Double amount){
		return null;
	}
}

如何用他们替代真正的WalletRpcService呢?

因为WalletRpcService是在execute()方法中通过new方式创建的,无法动态的替换。也就是说该方法可测试性很差,需要重构。如何重构?

依赖注入,将WalletRpcService对象的创建反转给上层逻辑,外部创建好之后,注入到Transaction类中。

public class Transaction{
	//...
	//添加一个成员变量及其set方法
	private WalletRpcService walletRpcService;

	public void setWalletRpcService(WalletRpcService walletRpcService){
		this.walletRpcService = walletRpcService;
	}

	public boolean execute() throws InvalidTransactionException{
		if(buyerId==null || (sellerId==null || amount<0.0)){
			throw new InvalidTransactionException(...);
		}
		if(status==STATUS.EXECUTED) return true;
		boolean isLocked = false;
		try{
			isLocked = RedisDistributedLock.getSingletonInstance().lockTransaction(id);
			if(!isLocked){
				return false;//锁定未成功,返回false job兜底执行
			}
			if(status==STATUS.EXECUTED) return true;//double check
			long executionInvokedTimestamp = System.currentTimestamp();
			if(executionInvokedTimestamp - createdTimestamp > 14days){
				this.status = STATUS.EXPIRED;
				return false;
			}
			
			String walletTransactionId = walletRpcService.moveMoney(id,buyerId,seller);
			if(walletTransactionId!= null){
				this.walletTransactionId = walletTransactionId;
				this.status = STATUS.EXECUTED;
				return true;
			}else {
				this.status = STATUS.FAILED;
				return false;
			}
		}finally{
			if(isLocked){
				RedisDistributedLock.getSingletonInstance().unlockTransaction(id);
			}
		}
	}
}

现在在单元测试中,就可以替换WalletRpcService了。


public void testExecute() {
  Long buyerId = 123L;
  Long sellerId = 234L;
  Long productId = 345L;
  Long orderId = 456L;
  Transction transaction = new Transaction(null, buyerId, sellerId, productId, orderId);
  // 使用mock对象来替代真正的RPC服务
  transaction.setWalletRpcService(new MockWalletRpcServiceOne()):
  boolean executedResult = transaction.execute();
  assertTrue(executedResult);
  assertEquals(STATUS.EXECUTED, transaction.getStatus());
}

再看RedisDistributedLock,它的mock和替换要复杂一些,因为它是单例类,单例相当于一个全局变量,无法mock(无法继承和重写),也无法通过依赖注入的方式替换

如果RedisDistributedLock是自己维护的,可自由修改、重构,就可以将其改为非单例的模式,或者定义一个接口,如IDistributedLock,让RedisDistributedLock实现该接口。但如果RedisDistributedLock不是自己维护的,无权限修改,怎么办?

可以对transaction上锁的部分的逻辑重新封装,如下

public class TransactionLock{
	public boolean lock(String id){
		return RedisDistributedLock.getSingletonInstance().lockTransaction(id);
	}

	public void unlock(){
		RedisDistributedLock.getSingletonInstance().unlockTransaction(id);
	}

	public class Transaction{
		//...
		private TransactionLock lock;

		public void setTransactionLock(TransactionLock lock){
			this.lock = lock;
		}
	}

	public boolean execute(){
		//...
		try{
			isLocked = lock.lock();
			//...
		}finally{
			if(isLocked){
				lock.unlock();
			}
		}
		//...
	}
}

针对重构过的代码,单元测试代码修改后,可隔离分布式锁:

public void testExecute(){
    Long buyerId = 123L;
    Long sellerId = 234L;
    Long productId = 345L;
    Long orderId = 456L;
    
    TransactionLock mockLock = new TransactionLock(){
        public boolean lock(String id){
            return true;
        }
        
        public void unlock(){
            
        }
    };
    
    Transaction transaction = new Transaction(null,buyerId,sellerId,productId,orderId);
    transaction.setWalletRpcService(new MockWalletRpcServiceOne());
    transaction.setTransactionLock(mockLock);
    boolean executedResult = transaction.execute();
    assertTrue(executedResult);
    assertEquals(STATUS.EXECUTED,transaction.getStatus());
}

测试用例1写好了。再看测试用例3:交易已过期,交易状态设置为EXPIRED,返回false。针对这个单元测试,先写出代码

public void testExecute_with_TransactionIsExpired(){
    Long buyerId = 123L;
    Long sellerId = 234L;
    Long productId = 345L;
    Long orderId = 456L;
    Transaction transaction = new Transaction(null,buyerId,sellerId,productId,orderId);
    transaction.setCreatedTimestamp(System.currentTimeMillis()-14days);
    boolean actualResult = transaction.execute();
    assertFalse(actualResult);
    assertEquals(STATUS.EXPIRED,transaction.getStatus());
}

上面代码看似没有问题,我们将transaction的创建时间createdTimestamp设置为14天前,也就是说,单元测试代码运行时,transaction一定处于过期状态,但是,如果在transaction类中,并没有暴露修改createdTimestamp成员变量的set方法(也就是没有定义setCreatedTimestamp()方法)呢?

在Transaction类的设计中,createdTimestamp是在交易生成时(也即是构造方法中)自动获取的系统时间,本就不该人为的轻易修改,针对这种代码中包含跟“时间”有关的“未决行为”逻辑,一般的处理方法是将这种未决行为逻辑重新封装,针对该类,只需将交易是否过期,封装到isExpired()方法中即可。

public class Transaction {
    
    protected boolean isExpired(){
        long executionInvokedTimestamp = System.currentTimeMillis();
        return executionInvokedTimestamp - createTimestamp >14days;
    }
    
    public boolean execute() throws InvalidTransactionException{
        //...
        if(isExpired()){
            this.status = STATUS.EXPIRED;
            return false;
        }
        //...
    }
}

针对重构后的代码,测试用例3的代码实现如下:

public void testExecute_with_TransactionIsExpired(){
    Long buyerId = 123L;
    Long sellerId = 234L;
    Long productId = 345L;
    Long orderId = 456L;
    Transaction transaction = new Transaction(null,buyerId,sellerId,productId,orderId){
        protected boolean isExpired(){
            return true;
        }
    };
    boolean actualResult = transaction.execute();
    assertFalse(actualResult);
    assertEquals(STATUS.EXPIRED,transaction.getStatus());
}

通过重构,Transaction代码的可测试性提高了。不过,Transaction类的构造方法的设计还有些不妥。里面交易id的赋值逻辑稍微复杂,最好测试下,保证这部分逻辑的正确性。为方便测试,可把id赋值这部分逻辑单独抽象到一个方法中,具体代码如下:

public Transaction(String preAssignedId, Long buyerId, Long sellerId, Long productId, String orderId) {
    //...
    fillTransactionId(preAssignedId);
    //...
}

protected void fillTransactionId(String preAssignedId){
    if (preAssignedId != null && !preAssignedId.isEmpty()) {
        this.id = preAssignedId;
    } else {
        this.id = IdGenerator.generateTransactionId();
    }
    if (!this.id.startWith("t_")) {
        this.id = "t_" + preAssignedId;
    }
}

到此为止,我们一步步的将Transaction从不可测试代码重构为测试性良好的代码。其实,代码的可测试性可以从侧面反映代码设计是否合理。

2. 其他常见的anti-patterns

总结下,有哪些典型的、常见的测试性不好的代码,也就是常说的anti-patterns

  1. 未决行为 也就是,代码的输出是随机的或说不确定的,比如跟时间、随机数有关的代码。
  2. 全局变量 滥用全局变量让编写单元测试变得困难。
  3. 静态方法 静态方法也很难mock。也需要分情况,如Math.abs()这样的简单静态方法,并不影响代码的可测试性,但是静态方法如果执行耗时太长,依赖外部资源、逻辑复杂、行为未决等情况,需要在单元测试中mock这个静态方法。
  4. 复杂继承 相比组合关系,继承关系的代码结构更加耦合、不灵活,更不易扩展、不易维护。更难测试。
  5. 高耦合代码 如果一个类职责很重,依赖十几个外部对象才能完成工作,代码高度耦合,单元测试时,可能㤇mock十几个依赖的对象。不合理。

4. 解耦

大型重构是对系统、模块、代码结构、类和类之间关系等顶层代码设计进行的重构。最有效的手段之一即是“解耦”。解耦的目的是实现高内聚、低耦合。

1. 解耦为何如此重要

软件设计与开发的最重要的工作之一就是应对复杂性。人处理复杂性的能力是有限的。过于复杂的代码往往可读性、可维护性都不友好。如何控制代码的复杂性呢?最关键的就是解耦。如果说重构是保证代码质量不至于腐化到无可救药的地步的有效手段,那么利用解耦的方法对代码重构,就是保证代码不至于复杂到无法控制的有效手段。

2. 代码是否需要解耦

如何判断代码的耦合程度呢?

有个直接的衡量标准,就是把模块和模块之间、类和类之间的依赖关系画出来,根据依赖关系图的复杂性判断是否需要解耦重构。

如果依赖关系复杂、混乱,从代码结构上讲,可读性和可维护性肯定不够友好,需要考虑解耦,让依赖关系变得清晰、简单。

3. 如何解耦

  1. 封装和抽象 可用在很多设计场景中,如系统、模块、lib、组件、接口、类等的设计。有效隐藏实现的复杂性,隔离实现的易变性,给依赖的模块提供稳定且易用的抽象接口。比如Unix系统的open()文件操作函数用起来很简单,但底层实现非常复杂,涉及到权限控制、并发控制、物理存储等。封装为open()函数后,有效控制代码复杂性的蔓延,此外,由于open()方法基于抽象而非具体实现定义,改动底层实现,并不需要改动依赖它的上层代码。
  2. 中间层 引入中间层简化模块或类之间的依赖关系,如jvm、消息中间件等,简化了依赖关系,代码结构更清晰;此外,重构时,引入中间层起到过渡的作用,让开发和重构同步进行,不互相干扰。如某个接口设计的有问题,需要修改它的定义。同时,所有调用这个接口的代码都要做相应的改动。如果新开发的代码也用到这个接口,开发和重构冲突。为让重构小步快跑,分四个阶段:引入一个中间层,包裹老的接口,提供新的接口定义;新开发的代码依赖中间层提供的新接口;将依赖老接口的代码改为调用新接口;确保所有的代码都调用新接口后,删除老的接口
  3. 模块化 对于大型复杂系统,没人能掌控所有的细节,之所以能搭建如此复杂的系统,并维护好,原因就是将系统划分为各个独立的模块,让不同人负责不同的模块,管理者协调各个模块,系统有效运转。其实,像SOA、微服务、lib库、系统内模块划分、甚至类、方法的设计,都体现了模块化思想。模块化思想更本质的东西就是分而治之。
  4. 其他设计思想和原则 高内聚、低耦合是个非常重要的设计思想,很多设计原则都是以实现它为目的。

模块化,让我突然想到隋唐设置的三省六部制就是这种思想的产物,皇帝老儿没法管理所有事务,分模块,高内聚、低耦合,将各个事务分解为六部:吏部(组织人才的选拔)、户部(户籍财政)、礼部(外宣、科举)、兵部(国防军事、装备研发)、刑部(公检法)、工部(治水、技术研发);按流程分解为中书省(草拟诏令,国务院)、门下省(审核,人大)、尚书省(执行,各部委)。

5. 编程规范

命名

实在找不到,就在github上用相关的关键词联想搜索下,看类似的代码如何命名的。其实很多大牛都有自己的心得,如effective java一书中就有推荐。

注释

包含做什么、为什么、怎么做。如下:

/**
 * (what) Bean factory to create beans
 * 
 * (why) The class likes Spring IOC framework, but is more lightweight
 * 
 * (how) Create objects from different sources sequentially
 * use specified object > SPI > configuration > default object
 **/
public class BeansFactory {
    //...
}

当然注释也不是越多越好,毕竟代码会持续演进的,有时候代码改了,注释忘了同步修改,会造成代码阅读者的困扰。类和方法一定要写注释,而且写得全面、详细,方法内部的注释相对少些,靠好的命名、提炼方法、解释性变量、总结性注释来提高代码的可读性。

代码风格

  • 方法最好不要超过一个显示屏的垂直高度,否则为了串联代码逻辑,还要上下滚动屏幕,阅读效果不好。
  • 善用空行分割单元块 添加空行,让代码的整体结构更加清晰、有条理
  • 类中成员顺序:先写成员变量再写方法,成员变量之间或方法之间,先写静态成员变量或方法,再写普通变量或方法,且按照作用域大小排列

其他

  • 把代码分割为更小的单元块

代码逻辑较为复杂时,建议提炼类或者方法。举例如下,inverse()方法,最开始的处理时间的代码,是不是很难懂?

public void invest(long userId,long financialProductId){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.set(Calendar.DATE,(calendar.get(Calendar.DATE)+1));
    if (calendar.get(Calendar.DAY_OF_MONTH) == 1){
        return;
    }
    //...
}

对其重构,将这部分逻辑抽象为一个方法,命名为isLastDayOfMonth,从名字上就能清晰的了解功能,判定今天是否为当月的最后一天,提高了代码的可读性

public void invest(long userId,long financialProductId){
    if (isLastDayOfMonth(new Date())){
        return;
    }
    //...
}

public boolean isLastDayOfMonth(Date date){
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.set(Calendar.DATE,(calendar.get(Calendar.DATE)+1));
    return calendar.get(Calendar.DAY_OF_MONTH) == 1;
}
  • 避免方法参数过多

针对过多参数,有两种解决方案:拆分为多个方法来减少参数;将方法的参数封装为对象

  • 不要用方法参数来控制逻辑

不要在方法中用boolean类型的标识参数控制内部逻辑,true的时候走这块,false走另一块,违反了单一职责原则和接口隔离原则。建议拆分为两个方法,可读性也更好。

public void buyCourse(long userId,long courseId,boolean isVip);
    
//拆分为两个方法
public void buyCourse(long userId,long courseId);
public void buyCourse4Vip(long userId,long courseId,boolean isVip);

当然,如果方法为private,或者拆分后两个方法经常同时被调用,可以酌情考虑保留标识参数

除boolean类型,还有一种根据参数是否为null来控制逻辑的情况。应该将其拆分为多个方法。拆分后方法职责更明确,代码如下

public List<Transaction> selectTransactions(Long userId,Date startDate,Date endDate){
    if (startDate != null && endDate != null){
        //查询两个时间区间的transactions
    }
    if (startDate != null && endDate == null){
        //查询startDate之后所有transactions
    }
    if (startDate == null && endDate != null){
        //查询endDate 之前所有transactions
    }
    if (startDate == null && endDate == null){
        //查询所有transactions
    }
}

//拆分为多个public方法,更清晰易用
public List<Transaction> selectTransactionsBetween(Long userId,Date startDate,Date endDate){
    return selectTransactions(userId, startDate, endDate);
}

public List<Transaction> selectTransactionsStartWith(Long userId,Date startDate){
    return selectTransactions(userId, startDate, null);
}

public List<Transaction> selectTransactionsEndWith(Long userId,Date endDate){
    return selectTransactions(userId, null, endDate);
}

public List<Transaction> selectAllTransaction(Long userId){
    return selectTransactions(userId, null, null);
}

private List<Transaction> selectTransactions(Long userId,Date startDate,Date endDate){
    //...
}
  • 移除过深的嵌套层次

过深是因为if-else,switch-case、for循环过度嵌套导致。嵌套最好不超过两层。解决方法有3种思路:

a. 去掉多余的if或else语句

public double caculateTotalAmount(List<Order> orders){
    if (orders == null || orders.isEmpty()){
        return 0.0;
    }else{//此处的else可以去掉
        //...主逻辑
    }
}

b. 调整执行顺序减少嵌套 先判空,再执行主逻辑

c. 将部分嵌套逻辑封装为方法,减少嵌套

  • 学会使用解释性变量 如PI代替3.1415 SUMMER_START替代具体时间

猜你喜欢

转载自blog.csdn.net/wjl31802/article/details/107749093