【代码质量】-如何避免写过多的if-else语句,从青铜到钻石级码农是如何重构的?

前言:开篇先放一张大神写的代码,反正我看到这张图第一反应就是佩服(画质有点渣,不过就算是4K蓝光我也看不懂里面的逻辑)

如果在开发中写出这样一段代码,那么恭喜你,拥有铁饭碗了,连技术总监都拿你没办法,你走了这代码谁来维护?

玩笑归玩笑,那么到底该如何避免在写代码中出现大量的if-else以及控制If-else的层级数?

阿里巴巴的java技术开发手册其实已经给出了答案,我这里结合实际场景再深入梳理一二,同时再给出一些除此之外的替代解决方案.


先来看下业务场景:现有某收费系统,针对不同身份的用户有不同的计费逻辑,比如用户是普通用户,则收取全额费用,用户是vip用户,则收取费用按照vip等级进行打折,等级越高,打折力度也越大.(具体的计费逻辑已被我简化成这样,方便理解,实际中会更复杂)

上面的场景如果用传统的If-else来实现:

/**
 * 账单
 */
@Data
@AllArgsConstructor
public class Bill {
    /**
     * 账单类型 0:普通用户账单,1:vip1级账单,2:vip2级账单...
     */
    private Integer type;
    /**
     * 账单总金额
     */
    private BigDecimal total;
}
public class BadExample {
    public void calcTotal(Bill bill){
        if (Objects.equals(bill.getTotal(),0)){
            //TODO 普通账单计费逻辑
        }else if (Objects.equals(bill.getTotal(),1)){
            //TODO vip1级用户账单计费逻辑
        }else if (Objects.equals(bill.getTotal(),2)){
            //TODO vip2级用户账单计费逻辑
        }else if (Objects.equals(bill.getTotal(),3)){
            //TODO vip3级用户账单计费逻辑
        }
        //...
    }
}

代码中会出现大量的if-else,如果里面的计费逻辑很复杂,每一段计费逻辑里又包含了很多层if-else,这片代码看着就会很乱,难于维护,这时候我们就可以先尝试去优化这段代码了.

下面我们一起看看青铜,白银,黄金,钻石...段位的码农是如何来优化的.

青铜:

先整个枚举类(有些甚至枚举类都不整,直接魔法值0,1,2,3...上阵),然后用switch case代替If,为每一个计费逻辑封装一个方法,然后在case中调用,这样看的确代码优雅了一些,但效果有限,而且不符合开闭原则,每当有新的计费逻辑时,就要加进switch中,再枚举类中也新加类型.这块的代码比较简单,我就不写了,自行脑补.

白银:

先写一个计费的接口:

public interface CalcService {
    /**
     * 处理账单计算
     */
    void handleCalc(BigDecimal total);
}

然后不同的计费逻辑用不同的实现类去实现此接口:

public class OridinaryCalcServiceImpl implements CalcService {
    @Override
    public void handleCalc(BigDecimal total) {
        System.out.println("处理普通用户的账单计费:" + total);
    }
}
public class Vip1CalcServiceImpl implements CalcService {
    @Override
    public void handleCalc(BigDecimal total) {
        System.out.println("处理vip1级用户的账单计费:"+total);
    }
}

...

然后把这些实现类统一用一个Map<Integer,CalcService>封装起来,在调用时根据key(账单类型Integer)来get对应的service,就能优雅的调用具体的实现类了,不错,这样代码确实优雅了很多,但一旦有新的计费逻辑出现,开发人员不得不去维护这个Map,同样违背了代码设计的开闭原则.

黄金:

黄金的实现思路与白银的类似,只不过黄金平时比较喜欢装X,喜欢在代码里搞点高逼格的东西,利用Java8提供的Function函数式编程,来让自己的代码逼格看起来高一点?话不多说,先上代码后解释:

public class CalcFunction {
    private String calcOrdinary(Bill bill) {
        return "处理普通用户的账单计费:" + bill.getTotal();
    }

    private String calcVip1(Bill bill) {
        return "处理vip1级用户的账单计费:" + bill.getTotal();
    }

    private String calcVip2(Bill bill) {
        return "处理vip2级用户的账单计费:" + bill.getTotal();
    }

    public Function<Bill, String> getFunction(Integer type) {
        Function<Bill, String> ordinary = bill -> calcOrdinary(bill);
        Function<Bill, String> vip1 = bill -> calcVip1(bill);
        Function<Bill, String> vip2 = bill -> calcVip2(bill);
        Supplier<Map<Integer, Function<Bill, String>>> supplier = () -> {
            Map<Integer, Function<Bill, String>> map = new HashMap<>(3);
            map.put(BillType.ORDINARY.getType(), ordinary);
            map.put(BillType.VIP1.getType(), vip1);
            map.put(BillType.VIP2.getType(), vip2);
            return map;
        };
        return supplier.get().get(type);
    }
}
    @Test
    public void testFunctional() {
        CalcFunction calcFunction = new CalcFunction();

        Bill bill1 = new Bill(BillTypeEnum.ORDINARY.getType(), new BigDecimal(500));
        calcFunction.getFunction(bill1.getType()).apply(bill1);

        Bill bill2 = new Bill(BillTypeEnum.VIP1.getType(), new BigDecimal(400));
        calcFunction.getFunction(bill2.getType()).apply(bill2);

        Bill bill3 = new Bill(BillTypeEnum.VIP2.getType(), new BigDecimal(300));
        calcFunction.getFunction(bill3.getType()).apply(bill3);
    }

具体实现思路就是,先按照不同计费规则封装具体的计费方法,然后把方法通过Supplier<?>的get提供给调用方,内部也维护了一个Map用来存放不同的function,黄金的实现看上去也挺优雅的,但还是不满足开闭原则,所以阿里的解决方案里并没有提到这几种方式.

相比白银,黄金的实现方式比较装X,但也不全是装X,通过function接口提供的compose,andThen方法可以灵活地加入其它处理逻辑,所以勉强给评个黄金段位.

钻石:

钻石的思路其实就是策略模式+自定义注解+Springboot监听.

先定义一个统一的计算账单接口,然后不同的计费规则分别去实现此接口,不同的是,钻石采用了自定义注解+Spring监听的模式,在Spring上下文环境发生变化时(一般是Spring项目启动完成),将这些加了注解的实现类统一自动注入到自定义的上下文中,交由Spring来管理,不需要再手动维护.需要时可以直接从上下文中取出对应的处理逻辑,看不懂没关系,不妨先看代码:

①定义统一的账单处理接口

public interface CalcService {
    /**
     * 处理账单计算
     */
    void handleCalc(BigDecimal total);
}

② 不同的计费逻辑分别去实现该接口中的handleCalc方法:

@BillTypeHandler(BillType.ORDINARY)
public class OridinaryCalcServiceImpl implements CalcService {
    @Override
    public void handleCalc(BigDecimal total) {
        System.out.println("处理普通用户的账单计费:" + total);
    }
}
@BillTypeHandler(BillType.VIP1)
public class Vip1CalcServiceImpl implements CalcService {
    @Override
    public void handleCalc(BigDecimal total) {
        System.out.println("处理vip1级用户的账单计费:"+total);
    }
}

省略vip3,vip4...代码,与上面雷同

③定义自定义注解:

@Service
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BillTypeHandler {
    BillType value();

    enum BillType {
        ORDINARY(0, "普通用户"),
        VIP1(1, "一级会员"),
        VIP2(2, "二级会员");
        private Integer type;
        private String desc;

        BillType(Integer type, String desc) {
            this.type = type;
            this.desc = desc;
        }

        public Integer getType() {
            return type;
        }
    }
}

④创建自定义的上下文环境:

此上下文环境中维护了一个Map,用来存放CalcService的实现类,然后通过@Component交由Spring容器管理

@Component
public class BillServiceContext {
    @Getter
    private final static Map<Integer, CalcService> calcServiceMap;

    static {
        calcServiceMap = new HashMap<>();
    }

    public CalcService get(Integer type) {
        return calcServiceMap.get(type);
    }

    public void put(Integer type, CalcService calcService) {
        calcServiceMap.put(type, calcService);
    }
}

⑤定义Springboot的监听器:

在项目启动后用来处理自定义注解的逻辑,先拿到每个添加了自定义注解的类,然后通过反射拿到该类的自定义注解,再通过自定义注解中的值(计费类型type枚举值),然后把该值put进④中自定义的那个上下文环境的Map中

@Component
public class BillTypeListener implements ApplicationListener<ContextRefreshedEvent> {
    @Resource
    private BillServiceContext billServiceContext;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        Map<String, CalcService> beans = contextRefreshedEvent.getApplicationContext().getBeansOfType(
            CalcService.class);
        beans.forEach((k, calcService) -> {
            Class clazz = calcService.getClass();
            BillTypeHandler billTypeHandler = (BillTypeHandler)clazz.getAnnotation(BillTypeHandler.class);
            billServiceContext.put(billTypeHandler.value().getType(), calcService);
        });
    }
}

⑥在需要调用的地方通过@Resource或者@Autowired注解将自定义的上下文环境注入,然后直接调用即可:

@SpringBootTest
@RunWith(SpringRunner.class)
public class BillTest {
    @Autowired
    BillServiceContext billServiceContext;

    @Test
    public void test() {
        Bill bill1 = new Bill(BillType.ORDINARY.getType(), new BigDecimal(500));
        billServiceContext.get(bill1.getType()).handleCalc(bill1.getTotal());

        Bill bill2 = new Bill(BillType.VIP1.getType(), new BigDecimal(400));
        billServiceContext.get(bill2.getType()).handleCalc(bill2.getTotal());

        Bill bill3 = new Bill(BillType.VIP2.getType(), new BigDecimal(300));
        billServiceContext.get(bill3.getType()).handleCalc(bill3.getTotal());
    }
}

测试结果符合预期:

这样的代码看着要比if-else清爽多了,而且在之后如果有新的计费逻辑进来,只需要新增实现类即可,无需改动原有代码,符合开闭原则,调用也是十分简单,后期维护会更方便.


白银:这样的代码不香吗?

塑料:真香!但我还是选择用If-else,不整这些花里胡哨5556的...

若干年过去了,白银当上了CTO,塑料成为了其下属,尽管白银对塑料各种嫌弃,一心想让塑料拍屁股走人,但一想到塑料的这段拳皇代码,还是算了...

发布了89 篇原创文章 · 获赞 69 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/lovexiaotaozi/article/details/103910776