if-else 超过三层之后,代码的可读性就会大大降低。可以使用卫语句、策略模式、状态模式来改善代码结构。
具体方案如下:
1.使用卫语句取代嵌套表达式
函数中的条件逻辑使人难以看清正常的执行途径。使用卫语句表现所有特殊情况。
动机:条件表达式通常有2种表现形式。第一:所有分支都属于正常行为。第二:条件表达式提供的答案中只有一种是正常行为,其他都是不常见的情况。
这2类条件表达式有不同的用途。如果2条分支都是正常行为,就应该使用形如if…..else…..的条件表达式;如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为“卫语句”。
Replace Nested Conditional with Guard Clauses (以卫语句取代嵌套条件表达式)的精髓是:给某个分支以特别的重视。它告诉阅读者:这种情况很罕见,如果它真的发生了,请做一些必要的整理工作,然后退出。
“每个函数只能有一个入口和一个出口”的观念,根深蒂固于某些程序员的脑海里。现今的编程语言都会强制保证每个函数只有一个入口,至于“单一出口”规则,其实不是那么有用。保持代码清晰才是最关键的:如果单一出口能使这个函数更清晰易读,那么就使用单一出口;否则就不必这么做。
做法:1、对于每个检查,放进一个卫语句。卫语句要不就从函数返回,要不就抛出一个异常。
2、每次将条件检查替换成卫语句后,编译并测试。如果所有卫语句都导致相同的结果,请使用 Consolidate Conditional Expression (合并条件表达式)。
2.卫语句就是把复杂的条件表达式拆分成多个条件表达式,比如一个很复杂的表达式,嵌套了好几层的if - then-else语句,转换为多个if语句,实现它的逻辑,这多条的if语句就是卫语句.
3有时候条件式可能出现在嵌套n次才能真正执行,其他分支只是简单报错返回的情况,对于这种情况,应该单独检查报错返回的分支,当条件为真时立即返回,这样的单独检查就是卫语句(guard clauses).卫语句可以把我们的视线从异常处理中解放出来,集中精力到正常处理的代码中。
例如下列代码:
void func(void)
{
if(IsWorkDay())
{
printf("Error,is work day");
}
else
{
if(IsWorkTime())
{
printf("Error ,is work time");
}
else
{
rest();
}
}
}
使用卫语句替换以后
void func()
{
if(IsWorkDay())
{
printf("Error,is work day");
return;
}
if(IsWorkTime())
{
printf("Error,is work time");
return ;
}
rest();
}
什么是策略模式?其思想是针对一组算法,将每一种算法都封装到具有共同接口的独立的类中,从而是它们可以相互替换。策略模式的最大特点是使得算法可以在不影响客户端的情况下发生变化,从而改变不同的功能。
假如我们有一个根据不同用户类型返回不同折扣的方法,我们的实现可能是这样:
import org.springframework.stereotype.Service; @Service public class CashService { public double cash(String type, double money) { if ("svip".equals(type)) { return money * 0.75; } else if ("vip".equals(type)) { return money * 0.9; } else { return money; } } }
现在我们各个类型的用户折扣耦合在一起,修改一个用户的折扣力度有可能会对其他类型用户造成影响。根据策略模式的思想,我们需要把折扣力度封装成具体的方法并面向接口编程。我们首先定义公共的接口DiscountService,编写其实现类,则我们改造后的代码可能如下所示:
import com.study.designer.strategy.NormalDiscountStrategy; import com.study.designer.strategy.SvipDiscountStrategy; import com.study.designer.strategy.VipDiscountStrategy; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class CashService { @Autowired private SvipDiscountStrategy svipDiscountStrategy; @Autowired private VipDiscountStrategy vipDiscountStrategy; @Autowired private NormalDiscountStrategy normalDiscountStrategy; public double cash(String type, double money) { if ("svip".equals(type)) { return svipDiscountStrategy.getMoney(money); } else if ("vip".equals(type)) { return vipDiscountStrategy.getMoney(money); } else { return normalDiscountStrategy.getMoney(money); } } }
可以看到,改造后的CashService中还存在许多if判断,我们需要消除这些if判断。我们可以在CashService初始化时就获取到所有的折扣策略,然后根据具体类型计算具体折扣。获取所有策略可以交由Spring来完成,改造后的代码如下所示:
import com.study.designer.strategy.inf.DiscountStrategy;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class CashService {
private Map
public CashService(List
for (DiscountStrategy strategy : strategies) {
strategyMap.put(strategy.getType(), strategy);
}
}
public double cash(String type, double money) {
return strategyMap.get(type).getMoney(money);
}
}
本次LZ给各位介绍状态模式,之前在写设计模式的时候,引入了一些小故事,二十章职责连模式是故事版的最后一篇,之后还剩余四个设计模式,LZ会依照原生的方式去解释这几个设计模式,特别是原型模式和解释器模式,会包含一些其它的内容。
好了,接下来,我们先来看看状态模式的定义吧。
定义:(源于Design Pattern):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
上述是百度百科中对状态模式的定义,定义很简单,只有一句话,请各位形象的去理解这句话,它说当状态改变时,这个对象的行为也会变,而看起来就像是这个类改变了一样。
这正是应验了我们那句话,有些人一旦发生过什么事以后,就像变了个人似的,这句话其实与状态模式有异曲同工之妙。
我们仔细体会一下定义当中的要点。
1、有一个对象,它是有状态的。
2、这个对象在状态不同的时候,行为不一样。
3、这些状态是可以切换的,而非毫无关系。
前两点比较好理解,第3点有时候容易给人比较迷惑的感觉,什么叫这些状态是可以切换的,而非毫无关系?
举个例子,比如一个人的状态,可以有很多,像生病和健康,这是两个状态,这是有关系并且可以转换的两个状态。再比如,睡觉、上班、休息,这也算是一组状态,这三个状态也是有关系的并且可以互相转换。
那如果把生病和休息这两个状态放在一起,就显得毫无意义了。所以这些状态应该是一组相关并且可互相切换的状态。
下面我们来看看状态模式的类图。
类图中包含三个角色。
Context:它就是那个含有状态的对象,它可以处理一些请求,这些请求最终产生的响应会与状态相关。
State:状态接口,它定义了每一个状态的行为集合,这些行为会在Context中得以使用。
ConcreteState:具体状态,实现相关行为的具体状态类。
如果针对刚才对于人的状态的例子来分析,那么人(Person)就是Context,状态接口依然是状态接口,而具体的状态类,则可以是睡觉,上班,休息,这一系列状态。
不过LZ也看过不少状态模式的文章和帖子,包括《大话设计模式》当中,都举的是有关人的状态的例子,所以这里给大家换个口味,我们换一个例子。
我们来试着写一个DOTA的例子,最近貌似跟DOTA干上了,不为其他,就因为DOTA伴随了LZ四年的大学时光。
玩过的朋友都知道,DOTA里的英雄有很多状态,比如正常,眩晕,加速,减速等等。相信就算没有玩过DOTA的朋友们,在其它游戏里也能见到类似的情况。那么假设我们的DOTA没有使用状态模式,则我们的英雄类会非常复杂和难以维护,我们来看下,原始版的英雄类是怎样的。
package com.state; //英雄类 public class Hero { public static final int COMMON = 1;//正常状态 public static final int SPEED_UP = 2;//加速状态 public static final int SPEED_DOWN = 3;//减速状态 public static final int SWIM = 4;//眩晕状态 private int state = COMMON;//默认是正常状态 private Thread runThread;//跑动线程 //设置状态 public void setState(int state) { this.state = state; } //停止跑动 public void stopRun() { if (isRunning()) runThread.interrupt(); System.out.println("--------------停止跑动---------------"); } //开始跑动 public void startRun() { if (isRunning()) { return; } final Hero hero = this; runThread = new Thread(new Runnable() { public void run() { while (!runThread.isInterrupted()) { try { hero.run(); } catch (InterruptedException e) { break; } } } }); System.out.println("--------------开始跑动---------------"); runThread.start(); } private boolean isRunning(){ return runThread != null && !runThread.isInterrupted(); } //英雄类开始奔跑 private void run() throws InterruptedException{ if (state == SPEED_UP) { System.out.println("--------------加速跑动---------------"); Thread.sleep(4000);//假设加速持续4秒 state = COMMON; System.out.println("------加速状态结束,变为正常状态------"); }else if (state == SPEED_DOWN) { System.out.println("--------------减速跑动---------------"); Thread.sleep(4000);//假设减速持续4秒 state = COMMON; System.out.println("------减速状态结束,变为正常状态------"); }else if (state == SWIM) { System.out.println("--------------不能跑动---------------"); Thread.sleep(2000);//假设眩晕持续2秒 state = COMMON; System.out.println("------眩晕状态结束,变为正常状态------"); }else { //正常跑动则不打印内容,否则会刷屏 } } }
下面我们写一个客户端类,去试图让英雄在各种状态下奔跑一下。
package com.state; public class Main { public static void main(String[] args) throws InterruptedException { Hero hero = new Hero(); hero.startRun(); hero.setState(Hero.SPEED_UP); Thread.sleep(5000); hero.setState(Hero.SPEED_DOWN); Thread.sleep(5000); hero.setState(Hero.SWIM); Thread.sleep(5000); hero.stopRun(); } }
可以看到,我们的英雄在跑动过程中随着状态的改变,会以不同的状态进行跑动。
在上面原始的例子当中,我们的英雄类当中有明显的if else结构,我们再来看看百度百科中状态模式所解决的问题的描述。
状态模式解决的问题:状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
不用说,状态模式是可以解决我们上面的if else结构的,我们采用状态模式,利用多态的特性可以消除掉if else结构。这样所带来的好处就是可以大大的增加程序的可维护性与扩展性。
下面我们就使用状态模式对上面的例子进行改善,首先第一步,就是我们需要定义一个状态接口,这个接口就只有一个方法,就是run。
package com.state; public interface RunState { void run(Hero hero); }
与状态模式类图不同的是,我们加入了一个参数Hero(Context),这样做的目的是为了具体的状态类当达到某一个条件的时候可以切换上下文的状态。下面列出四个具体的状态类,其实就是把if else拆掉放到这几个类的run方法中。
package com.state; public class CommonState implements RunState{ public void run(Hero hero) { //正常跑动则不打印内容,否则会刷屏 } }
package com.state; public class SpeedUpState implements RunState{ public void run(Hero hero) { System.out.println("--------------加速跑动---------------"); try { Thread.sleep(4000);//假设加速持续4秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------加速状态结束,变为正常状态------"); } }
package com.state; public class SpeedDownState implements RunState{ public void run(Hero hero) { System.out.println("--------------减速跑动---------------"); try { Thread.sleep(4000);//假设减速持续4秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------减速状态结束,变为正常状态------"); } }
package com.state; public class SwimState implements RunState{ public void run(Hero hero) { System.out.println("--------------不能跑动---------------"); try { Thread.sleep(2000);//假设眩晕持续2秒 } catch (InterruptedException e) {} hero.setState(Hero.COMMON); System.out.println("------眩晕状态结束,变为正常状态------"); } }
这下我们的英雄类也要相应的改动一下,最主要的改动就是那些if else可以删掉了,如下。
package com.state; //英雄类 public class Hero { public static final RunState COMMON = new CommonState();//正常状态 public static final RunState SPEED_UP = new SpeedUpState();//加速状态 public static final RunState SPEED_DOWN = new SpeedDownState();//减速状态 public static final RunState SWIM = new SwimState();//眩晕状态 private RunState state = COMMON;//默认是正常状态 private Thread runThread;//跑动线程 //设置状态 public void setState(RunState state) { this.state = state; } //停止跑动 public void stopRun() { if (isRunning()) runThread.interrupt(); System.out.println("--------------停止跑动---------------"); } //开始跑动 public void startRun() { if (isRunning()) { return; } final Hero hero = this; runThread = new Thread(new Runnable() { public void run() { while (!runThread.isInterrupted()) { state.run(hero); } } }); System.out.println("--------------开始跑动---------------"); runThread.start(); } private boolean isRunning(){ return runThread != null && !runThread.isInterrupted(); } }
可以看到,现在我们的英雄类优雅了许多,我们使用刚才同样的客户端运行即可得到同样的结果。
对比我们的原始例子,现在我们使用状态模式之后,有几个明显的优点:
一、我们去掉了if else结构,使得代码的可维护性更强,不易出错,这个优点挺明显,如果试图让你更改跑动的方法,是刚才的一堆if else好改,还是分成了若干个具体的状态类好改呢?答案是显而易见的。
二、使用多态代替了条件判断,这样我们代码的扩展性更强,比如要增加一些状态,假设有加速20%,加速10%,减速10%等等等(这并不是虚构,DOTA当中是真实存在这些状态的),会非常的容易。
三、状态是可以被共享的,这个在上面的例子当中有体现,看下Hero类当中的四个static final变量就知道了,因为状态类一般是没有自己的内部状态的,所有它只是一个具有行为的对象,因此是可以被共享的。
四、状态的转换更加简单安全,简单体现在状态的分割,因为我们把一堆if else分割成了若干个代码段分别放在几个具体的状态类当中,所以转换起来当然更简单,而且每次转换的时候我们只需要关注一个固定的状态到其他状态的转换。安全体现在类型安全,我们设置上下文的状态时,必须是状态接口的实现类,而不是原本的一个整数,这可以杜绝魔数以及不正确的状态码。
状态模式适用于某一个对象的行为取决于该对象的状态,并且该对象的状态会在运行时转换,又或者有很多的if else判断,而这些判断只是因为状态不同而不断的切换行为。
上面的适用场景是很多状态模式的介绍中都提到的,下面我们就来看下刚才DOTA中,英雄例子的类图。
可以看到,这个类图与状态模式的标准类图是几乎一模一样的,只是多了一条状态接口到上下文的依赖线,而这个是根据实际需要添加的,而且一般情况下都是需要的。
状态模式也有它的缺点,不过它的缺点和大多数模式相似,有两点。
1、会增加的类的数量。
2、使系统的复杂性增加。
尽管状态模式有着这样的缺点,但是往往我们牺牲复杂性去换取的高可维护性和扩展性是相当值得的,除非增加了复杂性以后,对于后者的提升会乎其微。
状态模式在项目当中也算是较经常会碰到的一个设计模式,但是通常情况下,我们还是在看到if else的情况下,对项目进行重构时使用,又或者你十分确定要做的项目会朝着状态模式发展,一般情况下,还是不建议在项目的初期使用。