[Code quality] - how to avoid too much to write if-else statements, how from Bronze to Diamond code is agricultural reconstruction?

Introduction: opening the first release of code written by a great God, anyway, I see this picture the first reaction is to admire (picture a little residue, but even I can not read 4K Blu-ray inside the logic)

If you write such a piece of code in development, then congratulations, you have a secure job, and even technical director will not get you, you're gone this code is going to protect?

All joking aside, then in the end how to avoid a large number of if-else write the code to control the number and level of If-else?

Alibaba java technology development manual is already given the answer, here I go any further sort out the actual scene or two, in addition to the same time and then give some alternative solutions.


First look at the business scenarios: existing in a toll system, has a different billing logic for different user identities, such as the user is a normal user, then charge the full amount, the user is vip user is charged in accordance with vip discount level the higher the rating, the greater the discount efforts. (specific billing logic has been simplified to me this way, easy to understand, practice is more complicated)

If the above scenario using conventional If-else achieved:

/**
 * 账单
 */
@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级用户账单计费逻辑
        }
        //...
    }
}

The code will be a lot of if-else, if there's logic is very complicated billing, billing logic in each section also contains a lot of layers if-else, look at this piece of code will be messy, difficult to maintain, this time we We can try to optimize the code.

Here we take a look at the bronze, silver, gold, diamonds ... Dan code farmers how to optimize.

bronze:

First the entire enumeration classes (some even integer enumeration classes are not directly battle mana 0,1,2,3 ...), and then replaced with a switch case If, ​​for each logic package a billing method, then in case call, so the code does look elegant some, but the effect is limited, and does not comply with the principle of opening and closing, whenever a new billing logic, we should add into the switch, and then enumerate the classes also add new types. this code block is relatively simple, I do not write, make their own brain.

silver:

The first to write a billing interface:

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

Then different billing logic with different implementation class to implement this interface:

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万+

Guess you like

Origin blog.csdn.net/lovexiaotaozi/article/details/103910776