【JAVA】If...Else if...else...
- 如果……又如果……否则……
- 那你也不能这样用呀!
- 熟悉的场景
- 熟悉的味道
- 不一样的体验
- 小结
- 如果……如果……如果……
- 18层地狱
- “花心”的条件
- 小结
- 如果……如果……没有如果!
- 没有如果
- 高富帅的认证
- 异常没有如果
- 庞大的异常
- 熔断机制
- 正确地使用业务异常熔断
- 回归高富帅的验证
- 小结
- 不是结局的结局
- 番外篇
如果……又如果……否则……
这条是一切编程语言中出场率最高的一个语句,这是所有计算逻辑的基础,这是“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的时候……噩梦模式不要随便选择呀喂!
不一样的体验
吐槽了这么多,那么怎么才能更好的优化这种代码呢?
我们首先得总结一下“前任”代码的缺点:
- 代码过长,不利于定位具体业务逻辑;
- 代码复杂度高,万一某个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);
并且,所有的业务对应关系一目了然,也能通过对应的业务类快速定位。
小结
现在,我们来归纳一下目前的体验:
- 代码长度只在于mapping的长度了,也可以从比较聚拢的枚举常量和注释上更好的判断业务内存;
- 代码很清新,所有逻辑判断交给了枚举类的循环,甚至你可以直接交给valueOf,只是没有那么形象而已(并且当匹配不到抛的异常不一样);
- 业务代码也抽离到对应的类中,降低了耦合度,定位更加清晰。
如果……如果……如果……
如果这个世界有如果……啊……跑题了……
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;
}
}
小结
现在,我们来总结一下这样对付“花心汉”的好处:
- “花心汉”的先后顺序清晰易见;
- 代码层级关系大幅降低;
- 可扩展性更加强。
如果……如果……没有如果!
不说什么了,直接上歌:没有如果
没有如果
“真该让那家伙听听梁静茹!”小明愤愤地道。
小安关切的问:“哪一首?”
“没有如果!”小明又啪啪啪的把所有多余if判断的逻辑抹去。
高富帅的认证
曾经的“前任”,为了区分验证逻辑的结果,将每个分支中设置一个返回码,然后再在外层判断返回码,最后生成验证错误信息。这让小明非常不爽,里一层外一层,那是不是有一点弱!
我们再次来到场景还原:
//
public String validateTallRichHandsome{
// 不能用异常,异常性能低下,直接返回错误代码
if (!u.isTall()){
return "short";
}
if (!u.isHandsome()) {
return "ugly";
}
if (!u.isRich()) {
return "poor"
}
return "";
}
//
public ResponseEntity<String> validateTallRichHandsome(Person u){
if ("".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";
}
}
各位看官,看到这里,你觉得这个代码弱在哪呢?是不是:
- 出现二次判断;
- 实际检验不通过的原因不明确;
- 为什么注释说不能用异常?
- ""是谁?
异常没有如果
要解决上面的问题,我们所有得了解为什么大家都说异常性能不好。
庞大的异常
我们先看一个常见的异常日志回忆一下:
: 400 Bad Request
at (:91)
at (:616)
at (:572)
at (:532)
at (:264)
... 此处省略无关重要的堆栈若干条
at (:745)
我们可以看到,每个异常中,都记录着前面的堆栈信息,这是怎么来的呢?
我们知道,Java的异常体系分为Error和Exception,他们都继承自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;
}
回归高富帅的验证
因为业务异常的性能问题我们已经解决了,所以我们可以使用熔断机制优化我们的代码,并由低层业务异常捕捉器捕获,返回对应的错误信息。
//
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;
}
//
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哈哈~)
番外篇
等等?你还想问3是谁?嗯,你们可以认为是他。
by William Liang @ VIP_FCS_DEV
-
/zh-hans/熔断机制 ↩︎
-
/jrose/longjumps-considered-inexpensive ↩︎
-
/p/34389352?group_id=955505915823222784 ↩︎