【JAVA】If...Else if...else...

如果……又如果……否则……

这条是一切编程语言中出场率最高的一个语句,这是所有计算逻辑的基础,这是“if … else if … else …”。

那你也不能这样用呀!

某一天,这句熟悉的语句又出现在小明前面的荧光幕上。
“wo kao”,小明不经意地吼出了声音,吸引了旁边的小安。
“怎么啦?”小安好奇的问到。“还能怎么样?!遇到这样的‘前任’算我倒霉!”这里小明说的“前任”可不是他的前女友哦!那么问题来了,他/她是谁呢?
“哦!又是那哥们的流水账代码是吧,不是都司空见惯了吗?”小安安慰到。
“以前以为他只是业务逻辑没想清楚就乱写一通,现在发现那已经是好的了,不信你来看看。”小明拉着小安到自己的屏幕前面。
“哈哈哈哈哈哈哈~~~~~~~~~~~~”小安笑得按着肚子,“这哥们真的太逗了,不就是个业务逻辑判断嘛。”
“那你也不能这样用呀!”小明迫不及待地接上。“一个业务判断用了300条if…else…,这是记流水账吗!”

熟悉的场景

这场景熟悉吗?或者某些正在读这篇文章的你也曾经遇到过。
型如这样的“前任”的代码:

// 业务类型判断
if(Constants.A.equals(type)){
	doA(); // 很多时候,连这个归纳的方法也没有
} else if (Constants.B.equals(type)){
	doB();
} else if (Constants.C.equals(type)){
	doB();
	doC();
// 省略若干行(具体数字请对号入座)
} else if (Constants.ZZZ.equals(type)){
	doZZZ();
} else{
	doDefault();
}

当其出现在你面前的时候,大概也只有问候“前任”祖宗十八代才能发泄你心中的郁闷了。这种流水账搬的代码往往因为篇幅太长,阅读不便,所以导致难以维护。对于接任人来说,这就简直是搬砖地狱了。

熟悉的味道

这个时候,有些朋友可能会说,这么多判断,直接使用switch不就好了。那么我们在看看switch的效果:

// 业务类型判断
switch(type) {
  case Constants.A:
	doA(); // 很多时候,连这个归纳的方法也没有
	break;
  case Constants.B:
	doB();
	break;
  case Constants.C:
	doB();
	doC();
	break;
// 省略若干行(具体数字请对号入座)
  case Constants.ZZZ.equals(type)){
	doZZZ();
	break;
  default:
	doDefault();
}

感觉怎么样,有什么变化吗?嗯,单行长度的确减下来了。然而,你还是需要翻几页才能找到你要的逻辑,更坏的情况是,万一你漏了某个case的break的时候……噩梦模式不要随便选择呀喂!

不一样的体验

吐槽了这么多,那么怎么才能更好的优化这种代码呢?
我们首先得总结一下“前任”代码的缺点:

  1. 代码过长,不利于定位具体业务逻辑;
  2. 代码复杂度高,万一某个else if判断漏了就直接到包底了;

总的来说,就是代码可读性不高,所以改进的方向就是把不好阅读的变为更好阅读。
我们知道,在Java中1.5开始就引入了enum类型,我们可以通过enum定义一系列的枚举常量,在switch中使用enum的话,很多IDE也会有适当的提示,哪些枚举常量没有被匹配到的。但是为了不掉进break陷阱,我建议大家可以对enum进行一个简单的改造。我们的目的是,让枚举常量变为一个对象匹配列表。

比如说,我这个长分支是为了判断走哪个业务分支,做一个启动器的角色,我们可以定义一个Launcher的枚举类。枚举的可以是带业务意义的常量,外加一个匹配值和对应启动程序。

enum Launcher {
	// 业务类型A,我们使用BusinessClassA的execute方法
	TYPE_A("A", BusinessClassA.execute),
	// 业务类型B,我们使用BusinessClassB的execute方法
	TYPE_B("B", BusinessClassB.execute),
	// 业务类型C,我们使用BusinessClassC的execute方法
	TYPE_C("C", BusinessClassC.execute),
	// 省略若干行(具体数字请对号入座)
	// 业务类型ZZZ,我们使用BusinessClassZZZ的execute方法
	TYPE_ZZZ("ZZZ", BusinessClassZZZ.execute);
	
	private String key;
	private Consumer<String[]> consumer;
	
	private Launcher(String key, Consumer<String[]> consumer) {
		this.key = key;
		this.consumer = consumer;
	}
	
	private void execute(String[] args){
		this.consumer.accept(args);
	}
	
	private static Launcher getLauncher(String key){
		for(Launcher l : Launcher.values()){
			if(l.key.equals(key)){
				return l;
			}
		}
		throw new BusinessException("未找到【" + key + "】对应的Launcher!");
	}
}
	public class BusinessClass{A...ZZZ} {
		public static void execute(String[] args){
			System.out.println(args.length);
		}
	}

那么,我们业务逻辑只需要在业务类实现一个消费对应参数String[]的方法execute就能很轻松的使用如下代码启动了。

	Launcher.getLauncher(type).execute(args);

并且,所有的业务对应关系一目了然,也能通过对应的业务类快速定位。

小结

现在,我们来归纳一下目前的体验:

  1. 代码长度只在于mapping的长度了,也可以从比较聚拢的枚举常量和注释上更好的判断业务内存;
  2. 代码很清新,所有逻辑判断交给了枚举类的循环,甚至你可以直接交给valueOf,只是没有那么形象而已(并且当匹配不到抛的异常不一样);
  3. 业务代码也抽离到对应的类中,降低了耦合度,定位更加清晰。

如果……如果……如果……

如果这个世界有如果……啊……跑题了……

18层地狱

“牛逼!”,小明突然发出的声音,吸引了旁边的小安。
“这次又有什么好代码了?”小安问到。
“我从未见过如果厚颜无耻的if深度呀!”很明显,这是小明又一次吐槽。
“让我看看。”小安凑过头去一瞧。“1层,2层,3层……18层,这是下地狱的节奏呀!”
“施主,我不入地狱谁入地狱。”于是小明又抄起键盘,将这18层地狱抹去了……

“花心”的条件

我们先还原一下小明看到的场景。

if (!Amy.loveMe()) {
	if (!Belly.loveMe()){
		if (!Cecilia.loveMe()){
		   // 省略14层
		   if (!Rachael.loveMe()){
				return I.showShaneMercy();
		   } else {
				return I.loveU2(Rachael);
		   }
		} else {
			return I.loveU2(Cecilia);
		}
	} else {
		return I.loveU2(Belly);
	}
} else {
	return I.loveU2(Amy);
}

噢,准确的讲,是小明的“前任”引入的场景的一个模拟。这个小明的“前任非常花心”,已经追求了18个字母的女孩了,但是他还不确定他跟哪一个好,所以他决定看看谁喜欢他,他就喜欢谁。当然了,花心的他还是有偏好的,而这个偏好也刚好和字母序一样。而且,他连翻船的情景也想好了……Shane是谁?我哪知道!

面对这么“花心”的小明的“前任”,对于他最好的方法,我觉得就是使用责任链的模式,用责任束缚他。

public class ResponseChain {
	private List<Lover> perferGirls = Array.asList(new Amy(), new Belly(), new Cecilia(), 
	// 省略14位
	new Rachael());
	
	public MyResponse findMyDestiny(){
		MyResponse destiny = null;
		do {
			destiny = new LoveResponse(girl).doResponse();
		} while (destiny != null);
		return destiny;
	}

	public static final void main(String[] args){
		MyResponse destiny = ResponseChain.findMyDestiny();
		if (destiny != null) {
			// 好像暴露了点什么
			destiny = Gay.mercyTo(new Shane());
		}
		return destiny;
	}
}

interface Response{
	public boolean doResponse();
}

@AllArgsConstructor
class LoverResponse implements Response{
	// 做人要专一
	private Lover onlyOne;

	@Override
	public MyResponse doResponse(){
		if (onlyOne.loveMe()) {
			return I.loveU2(onlyOne);
		}
		// 既然不爱,那就放空
		return null;
	}
}

小结

现在,我们来总结一下这样对付“花心汉”的好处:

  1. “花心汉”的先后顺序清晰易见;
  2. 代码层级关系大幅降低;
  3. 可扩展性更加强。

如果……如果……没有如果!

不说什么了,直接上歌:没有如果

没有如果

“真该让那家伙听听梁静茹!”小明愤愤地道。
小安关切的问:“哪一首?”
“没有如果!”小明又啪啪啪的把所有多余if判断的逻辑抹去。

高富帅的认证

曾经的“前任”,为了区分验证逻辑的结果,将每个分支中设置一个返回码,然后再在外层判断返回码,最后生成验证错误信息。这让小明非常不爽,里一层外一层,那是不是有一点弱!

我们再次来到场景还原:

// xxxServiceImpl.java
public String validateTallRichHandsome{
	// 不能用异常,异常性能低下,直接返回错误代码
	if (!u.isTall()){
		return "short";
	}
	if (!u.isHandsome()) {
		return "ugly";
	}
	if (!u.isRich()) {
		return "poor"
	}
	return "cloud.wong";
}
// xxxController.java
public ResponseEntity<String> validateTallRichHandsome(Person u){
	if ("cloud.wong".equals(xxxServiceImpl.validateTallRichHandsome(u))) {
		return "Really Tall & Rich & Handsome";
	} else if ("short".equals(xxxServiceImpl.validateTallRichHandsome(u))) {
		return "Really Short & Rich & Handsome";
	} else if ("poor".equals(xxxServiceImpl.validateTallRichHandsome(u))) {
		return "Really Short & Ugly & Handsome";
	} else {
		return "Really Short & Ugly & Poor";
	}
}

各位看官,看到这里,你觉得这个代码弱在哪呢?是不是:

  1. 出现二次判断;
  2. 实际检验不通过的原因不明确;
  3. 为什么注释说不能用异常?
  4. "cloud.wong"是谁?

异常没有如果

要解决上面的问题,我们所有得了解为什么大家都说异常性能不好。

庞大的异常

我们先看一个常见的异常日志回忆一下:

org.springframework.web.client.HttpClientErrorException: 400 Bad Request
	at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
	at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:616)
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:572)
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:532)
	at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:264)
	... 此处省略无关重要的堆栈若干条
	at java.lang.Thread.run(Thread.java:745)

我们可以看到,每个异常中,都记录着前面的堆栈信息,这是怎么来的呢?

我们知道,Java的异常体系分为Error和Exception,他们都继承自Throwable,其中,Exception还分CheckedException和UncheckedException。

Error
Throwable
Exception
CheckedException
UncheckedException

因为所有Exception在初始化的时候都调用父类Throwable的构造函数,所以所有异常都会调用一个名为fillInStackTrace的方法。而当查看fillInStackTrace的实现时,你会发现他是一个同步方法,而且会在此调用低层方法。

    public synchronized Throwable fillInStackTrace() {
        if (stackTrace != null ||
            backtrace != null /* Out of protocol state */ ) {
            fillInStackTrace(0);
            stackTrace = UNASSIGNED_STACK;
        }
        return this;
    }
    
    private native Throwable fillInStackTrace(int dummy);

这个记录堆栈信息的位置,正是异常开销大,性能差的根源。

熔断机制

熔断机制1(英语:Circuit breaker / Trading curb)指的是在股票市场的交易时间中,当价格波动的幅度达到某一个限定的目标(熔断点)时,对其暂停交易一段时间的机制。此机制如同保险丝在电流过大时候熔断比较相似,故而得名。熔断机制推出的目的是为了防范系统性风险,给市场更多的冷静时间,避免恐慌情绪蔓延导致市场波动,从而防止大规模股价下跌现象的发生。然而熔断机制也因切断了资金的流通性,同样会造成市场情绪加大,并令市场风险在熔断期结束后继续扩大。

啊,我们今天聊的不是股市。在程序开发中,我们同样有熔断机制。在程序运行时,如果某个验证条件触发业务异常,则直接返回,停止后续无效运行。

正确地使用业务异常熔断

为了保持性能,并使用异常的熔断功能,我们需要改造我们的业务异常类定义。因为通常在业务异常中,我们并不关心他的堆栈,所以我们可以在业务异常类中复写他的fillInStackTrace方法。2

@Override
public Throwable fillInStackTrace() {
	return this;
}

回归高富帅的验证

因为业务异常的性能问题我们已经解决了,所以我们可以使用熔断机制优化我们的代码,并由低层业务异常捕捉器捕获,返回对应的错误信息。

// xxxServiceImpl.java
public boolean validateTallRichHandsome{
	// 用异常熔断
	if (!u.isTall()){
		throw new BusinessException("Not tall enough!");
	}
	if (!u.isHandsome()) {
		throw new BusinessException("Not handsome enough!");
	}
	if (!u.isRich()) {
		throw new BusinessException("Not rich enough!");
	}
	return true;
}
// xxxController.java
public ResponseEntity<String> validateTallRichHandsome(Person u){
	return xxxServiceImpl.validateTallRichHandsome(u) ? "Really Tall & Rich & Handsome" : "";
}
// @ControllerAdvice
public class GlobalExceptionHandler {
	@ResponseBody
	@ExceptionHandler({ BusinessException.class })
	public ResponseEntity<String> handleException(BusinessException ex) {
		return ex.getMessage();
	}
}

小结

这个场景下,我们使用了熔断机制,解决了在大量业务验证后的二次验证的问题,并且异常中能将错误信息交由低层结构中的异常捕捉完成,内聚验证异常信息功能。

不是结局的结局

就这样,小明将所有“前任”留下的如果问题都解决了,他和小安过上了幸福快乐的日子了……
(本故事纯属虚构,如有雷同,请不要对号入座 O(∩_∩)O哈哈~)

番外篇

等等?你还想问cloud.wong3是谁?嗯,你们可以认为是他。

by William Liang @ VIP_FCS_DEV


  1. https://zh.wikipedia.org/zh-hans/熔断机制 ↩︎

  2. https://blogs.oracle.com/jrose/longjumps-considered-inexpensive ↩︎

  3. https://zhuanlan.zhihu.com/p/34389352?group_id=955505915823222784 ↩︎

猜你喜欢

转载自blog.csdn.net/vipshop_fin_dev/article/details/83753166