利用设计模式消除业务代码中的 if-else

准备工作:
假设这样的一个业务场景:有一个自动开票的功能需要实现,在程序里面需要根据账单的类型执行对应的处理逻辑。

以下使用了 Lombok 简化代码!!!

账单类型枚举:

/**
 * @author ly-az
 * @date 12/23/2020 11:34
 * 账单类型
 */
public enum BillType {

    /**
     * 账单的类型
     */
    BT0001, BT0002

}

账单类:

/**
 * @author ly-az
 * @date 12/23/2020 11:31
 * 账单实体
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Bill {

    /**
     * 账单信息
     */
    String message;

    /**
     * 账单类型类型(BT0001、BT0002)
     */
    BillType type;

}

账单的模拟生成:用于生成一下模拟数据

/**
 * @author ly-az
 * @date 12/23/2020 11:42
 * 生成用于测试的账单
 */
public class BillBuilder {
    /**
     * 生成账单
     *
     * @return 账单集合
     */
    public static List<Bill> generateBillList() {
        val billList = new ArrayList<Bill>();
        billList.add(new Bill("我是第一种类型的账单", BillType.BT0001));
        billList.add(new Bill("我是第二种类型的账单", BillType.BT0002));
        return billList;
    }
}

使用if-else来实现的话:

/**
 * @author ly-az
 * @date 12/24/2020 10:31
 * if-else
 */
public class Test01 {

    public static void main(String[] args) {
        List<Bill> billList = BillBuilder.generateBillList();
        billList.forEach(bill -> {
            if (bill.getType().equals(BillType.BT0001)) {
                // 模拟实现业务处理
                System.out.println("开始处理BT0001类型的账单 > > > " + bill.getMessage() + ". . . ");
                System.out.println("处理成功!!!");
            } else if (bill.getType().equals(BillType.BT0002)) {
                System.out.println("开始处理BT0002类型的账单 > > > " + bill.getMessage() + ". . . ");
                System.out.println("处理成功!!!");
            }
            // 。。。 未来还有数不清的else if分支 。。。
            else {
                System.out.println("账单类型不匹配!!!");
            }
        });
    }

}

在遇到if-else的分支业务逻辑比较复杂时,为了代码的可读性我们会将其整理成一个方法或者封装成一个工具类去调用,避免整个if-else的结构就显得过于臃肿(这里就模拟了两种类型,偷懒…ψ(`∇´)ψ)。
但是当账单类型越来越多时,if-else分支就会越来越多,每增加一种类型,就需要去修改或添加if-else分支,违反了开闭原则(对扩展开放,对修改关闭)

方案一:使用策略模式+Map字典

策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。
——《JAVA与模式》

也就是说让我们自己的代码不再依赖于调用业务代码的时的客户端,能够根据不同的客户端执行不同的代码,而客户端只需要调用我们的策略接口即可。

在上述场景中,可以把 if-else 分支的代码抽取为各种不同的策略实现,而且我们可以把选择不同策略的实现的代码逻辑抽取到一个工厂类中去,这就是策略模式+简单工厂模式,为了进一步简化,我们还可以把策略的实现放到一个Map中封装成字典

看代码:

  • 抽象策略:通常由一个接口或抽象类实现。是所有的具体策略类所需的接口。
/**
 * @author ly-az
 * @date 12/23/2020 11:46
 * @see BillHandleStrategyFactory 策略工厂
 * 账单处理的策略接口<br>
 * 有新的类型的账单时只需要实现这个接口重写接口里的实现方法并添加到策略工厂里面的字典里即可
 */
public interface BillHandleStrategy {
    /**
     * 处理方法
     *
     * @param bill 账单
     */
    void handleBill(Bill bill);
}
  • 环境:持有一个策略接口的引用。
/**
 * @author ly-az
 * @date 12/23/2020 11:46
 */
public class BillStrategyContext {

    private BillHandleStrategy billHandleStrategy;

    /**
     * 设置策略接口
     *
     * @param billHandleStrategy 策略接口
     */
    public void setBillHandleStrategy(BillHandleStrategy billHandleStrategy) {
        this.billHandleStrategy = billHandleStrategy;
    }

    public void handleBill(Bill bill) {
        if (billHandleStrategy != null) {
            billHandleStrategy.handleBill(bill);
        }
    }
}
  • 具体的策略实现:就是处理账单的策略接口的具体实现,账单对应的处理者。
/**
 * @author ly-az
 * @date 12/23/2020 13:01
 * 处理BT0001账单的策略接口的实现类
 */
public class Bt0001BillHandleStrategy implements BillHandleStrategy {

    @Override
    public void handleBill(Bill bill) {
        // 模拟业务处理
        System.out.println("开始处理BT0001类型的账单 > > > " + bill.getMessage() + ". . .");
        System.out.println("处理成功!!!");
    }

}
/**
 * @author ly-az
 * @date 12/23/2020 13:02
 * 处理BT0002类型的策略实现类
 */
public class Bt0002BillHandleStrategy implements BillHandleStrategy {

    @Override
    public void handleBill(Bill bill) {
        // 模拟实现业务处理
        System.out.println("开始处理BT0002类型的账单 > > > " + bill.getMessage() + ". . . ");
        System.out.println("处理成功!!!");
    }
}

  • 策略工厂:用于获取具体策略的实现类对象
/**
 * @author ly-az
 * @date 12/23/2020 13:05
 * 策略工厂
 */
public class BillHandleStrategyFactory {

    private static final Map<BillType, BillHandleStrategy> BILL_HANDLE_STRATEGY_MAP;

    static {
        // 初始化,
        BILL_HANDLE_STRATEGY_MAP = new HashMap<>();
        BILL_HANDLE_STRATEGY_MAP.put(BillType.BT0001, new Bt0001BillHandleStrategy());
        BILL_HANDLE_STRATEGY_MAP.put(BillType.BT0002, new Bt0002BillHandleStrategy());
    }


    /**
     * 根据账单类型直接从Map字典里获取对应的处理策略实现类
     *
     * @param billType 账单类型
     * @return 处理策略的实现类
     */
    public static BillHandleStrategy getBillHandleStrategy(BillType billType) {
        return BILL_HANDLE_STRATEGY_MAP.get(billType);
    }
}


测试代码:

/**
 * @author ly-az
 * @date 12/23/2020 13:12
 */
public class Test02 {

    public static void main(String[] args) {
        //模拟账单
        List<Bill> billList = BillBuilder.generateBillList();
        //策略上下文
        BillStrategyContext billStrategyContext = new BillStrategyContext();
        billList.forEach(bill -> {
            //获取并设置策略
            BillHandleStrategy billHandleStrategy = BillHandleStrategyFactory.getBillHandleStrategy(bill.getType());
            billStrategyContext.setBillHandleStrategy(billHandleStrategy);
            //执行策略
            billStrategyContext.handleBill(bill);
        });
    }
}


测试结果:

**
每当有一种新的账单类型,只需要添加新的账单处理策略,并添加到BillHandleStrategyFactory中的Map集合。
如果要使得程序符合开闭原则,则需要调整BillHandleStrategyFactory中处理策略的获取方式。
(好吧,我摊牌了,其实是我太菜了没有想到怎么写ヾ(•ω•`)o)

改进思路:策略模式+注解,通过自定义注解,使用注解标记具体的策略实现类,我们就可以通过反射获取到具体的策略实现类的实例,然后放置到容器的Map字典中!

方案二:使用责任链模式

顾名思义,责任链模式(Chain of Responsibility Pattern)为调用请求创建了一个接收者对象的链。让多个对象都有可能接收请求,并将这些对象连接成一条链,并且沿着这条链传递调用请求,直到有对象处理它为止。这种类型的设计模式也属于行为型模式。
发出这个调用请求的客户端并不知道链上的哪一个对象会最终处理这个调用请求,这使得我们的程序可以在不影响客户端的情况下动态地重新组织和分配责任。

  • 账单处理的抽像接口
/**
 * @author ly-az
 * @date 12/23/2020 14:51
 * 账单的抽象处理接口
 */
public interface IBillHandler {

    /**
     * 利用责任链处理账单的抽象方法
     * @param bill 账单
     * @param handleChain 处理链
     */
    void handleBill(Bill bill, IBillHandleChain handleChain);

}
  • 账单处理的的具体实现
/**
 * @author ly-az
 * @date 12/23/2020 15:14
 * 账单的具体处理者的实现
 */
public class Bt0001BillHandler implements IBillHandler {

    @Override
    public void handleBill(Bill bill, IBillHandleChain handleChain) {
        if (BillType.BT0001.equals(bill.getType())) {
            // 模拟实现业务处理
            System.out.println("开始处理BT0001类型的账单 > > > " + bill.getMessage() + ". . . ");
            System.out.println("处理成功!!!");
        }
        //处理不了该回执就往下传递
        else {
            handleChain.handleBill(bill);
        }
    }
}
/**
 * @author ly-az
 * @date 12/23/2020 15:42
 * todo
 */
public class Bt0002BillHandler implements IBillHandler {

    @Override
    public void handleBill(Bill bill, IBillHandleChain handleChain) {
        if (BillType.BT0002.equals(bill.getType())) {
            // 模拟实现业务处理
            System.out.println("开始处理BT0002类型的账单 > > > " + bill.getMessage() + ". . . ");
            System.out.println("处理成功!!!");
        }
        //处理不了该回执就往下传递
        else {
            handleChain.handleBill(bill);
        }
    }
}

  • 责任链接口
/**
 * @author ly-az
 * @date 12/23/2020 14:52
 * 责任链接口
 */
public interface IBillHandleChain {
    /**
     * 处理账单的抽象方法
     *
     * @param bill 账单
     */
    void handleBill(Bill bill);
}

  • 责任链接口的实现
/**
 * @author ly-az
 * @date 12/23/2020 15:08
 * 处理账单的责任链
 */
public class BillHandleChain implements IBillHandleChain {
    /**
     * 记录当前处理者位置
     */
    private int index = 0;
    /**
     * 处理者集合
     */
    private static final List<IBillHandler> BILL_HANDLER_LIST;

    static {
        //从容器中获取处理器对象
        BILL_HANDLER_LIST = BillHandlerContext.getBillHandlerList();
    }

    @Override
    public void handleBill(Bill bill) {
        if (BILL_HANDLER_LIST != null && BILL_HANDLER_LIST.size() > 0) {
            if (index != BILL_HANDLER_LIST.size()) {
                IBillHandler billHandler = BILL_HANDLER_LIST.get(index++);
                billHandler.handleBill(bill, this);
            }
        }
    }
}

  • 责任链处理者容器(如果项目中有Spring的IOC容器 ,则可以直接通过依赖注入的方式获取到IBillHandler 的具体实现)
/**
 * @author ly-az
 * @date 12/23/2020 15:43
 * 处理容器
 */
public class BillHandlerContext {

    private BillHandlerContext() {
    }
    
    public static List<IBillHandler> getBillHandlerList() {
        val billHandlerList = new ArrayList<IBillHandler>();
        billHandlerList.add(new Bt0001BillHandler());
        billHandlerList.add(new Bt0002BillHandler());
        return billHandlerList;
    }

}
  • 测试用例
/**
 * @author ly-az
 * @date 12/23/2020 15:48
 * 责任链模式下的测试
 */
public class TestClient2 {

    public static void main(String[] args) {
        List<Bill> billList = BillBuilder.generateBillList();
        billList.forEach(bill -> {
            //回执处理链的实例对象
            BillHandleChain billHandleChain = new BillHandleChain();
            billHandleChain.handleBill(bill);
        });
    }

}

结果:
image.png

同样,如果要使得程序符合开闭原则,则需要调整BillHandlerContext中处理者的获取方式,通过反射的方式,获取指定包下的所有IBillHandler的实现类。
**
o( ̄▽ ̄)ブ,我想到了这种解决方案下该如何改造使其符合开闭原则!!!

我们需要引入一个反射用的工具类:

/**
 * @author ly-az
 * @date 12/23/2020 16:00
 * 反射工具类
 */
public class ReflectionUtil {

    /**
     * 定义类集合(用于存放所有加载的类的镜像)
     */
    private static final Set<Class<?>> CLASS_SET;

    static {
        //指定加载包路径
        CLASS_SET = getClassSet("com.az");
    }

    /**
     * 获取类加载器
     *
     * @return 类加载器
     */
    public static ClassLoader getClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * 加载类
     *
     * @param className     类全限定名称
     * @param isInitialized 是否在加载完成后执行静态代码块
     * @return 类的镜像
     */
    public static Class<?> loadClass(String className, boolean isInitialized) {
        Class<?> cls;
        try {
            cls = Class.forName(className, isInitialized, getClassLoader());
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        return cls;
    }

    public static Class<?> loadClass(String className) {
        return loadClass(className, true);
    }

    /**
     * 获取指定包下所有类
     *
     * @param packageName 全限包名
     * @return 镜像的Set集合
     */
    public static Set<Class<?>> getClassSet(String packageName) {
        Set<Class<?>> classSet = new HashSet<>();
        try {
            Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".", "/"));
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                if (url != null) {
                    String protocol = url.getProtocol();
                    if ("file".equals(protocol)) {
                        String packagePath = url.getPath().replace("%20", "");
                        addClass(classSet, packagePath, packageName);
                    } else if ("jar".equals(protocol)) {
                        JarURLConnection jarUrlConnection = (JarURLConnection) url.openConnection();
                        if (jarUrlConnection != null) {
                            JarFile jarFile = jarUrlConnection.getJarFile();
                            if (jarFile != null) {
                                Enumeration<JarEntry> jarEntries = jarFile.entries();
                                while (jarEntries.hasMoreElements()) {
                                    JarEntry jarEntry = jarEntries.nextElement();
                                    String jarEntryName = jarEntry.getName();
                                    if (jarEntryName.endsWith(".class")) {
                                        String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
                                        doAddClass(classSet, className);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return classSet;
    }

    private static void doAddClass(Set<Class<?>> classSet, String className) {
        Class<?> cls = loadClass(className, false);
        classSet.add(cls);
    }

    private static void addClass(Set<Class<?>> classSet, String packagePath, String packageName) {
        final File[] fileList = new File(packagePath).listFiles(file -> (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory());
        assert fileList != null;
        Arrays.asList(fileList).forEach(file -> {
            String fileName = file.getName();
            if (file.isFile()) {
                String className = fileName.substring(0, fileName.lastIndexOf("."));
                if (StringUtils.isNotEmpty(packageName)) {
                    className = packageName + "." + className;
                }
                doAddClass(classSet, className);
            } else {
                String subPackagePath = fileName;
                if (StringUtils.isNotEmpty(packagePath)) {
                    subPackagePath = packagePath + "/" + subPackagePath;
                }
                String subPackageName = fileName;
                if (StringUtils.isNotEmpty(packageName)) {
                    subPackageName = packageName + "." + subPackageName;
                }
                addClass(classSet, subPackagePath, subPackageName);
            }
        });
    }


    public static Set<Class<?>> getClassSet() {
        return CLASS_SET;
    }

    /**
     * 获取应用包名下某父类(或接口)的所有子类(或实现类)
     *
     * @param superClass 父类(或接口)镜像
     * @return 所有子类(或实现类)的镜像集合
     */
    public static Set<Class<?>> getClassSetBySuper(Class<?> superClass) {
        Set<Class<?>> classSet = new HashSet<>();
        for (Class<?> cls : CLASS_SET) {
            if (superClass.isAssignableFrom(cls) && !superClass.equals(cls)) {
                classSet.add(cls);
            }
        }
        return classSet;
    }

    /**
     * 获取应用包名下带有某注解的类
     *
     * @param annotationClass 注解的镜像
     * @return 镜像集合
     */
    public static Set<Class<?>> getClassSetByAnnotation(Class<? extends Annotation> annotationClass) {
        Set<Class<?>> classSet = new HashSet<>();
        for (Class<?> cls : CLASS_SET) {
            if (cls.isAnnotationPresent(annotationClass)) {
                classSet.add(cls);
            }
        }
        return classSet;
    }

}

对责任链模式的处理容器修改一下:

/**
 * @author ly-az
 * @date 12/23/2020 15:43
 * 处理容器
 */
public class BillHandlerContext {

    private BillHandlerContext() {
    }

    public static List<IBillHandler> getBillHandlerList() {
        val billHandlerList = new ArrayList<IBillHandler>();
        //获取IBillHandler接口的实现类
        Set<Class<?>> classList = ReflectionUtil.getClassSetBySuper(IBillHandler.class);
        if (classList.size() > 0) {
            classList.forEach(clazz -> {
                try {
                    billHandlerList.add((IBillHandler) clazz.getDeclaredConstructor().newInstance());
                } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                    e.printStackTrace();
                }
            });
        }
        return billHandlerList;
    }

}

至此,责任链方案就符合开闭原则,如果新增一个账单类型,只需要添加一个新的账单处理处理器的实现类即可,无需做其它改动。

小结

if-else以及switch-case 这种分支判断的方式对于分支逻辑不多的简单业务,还是更加直观高效的。但是对于业务复杂,分支逻辑多的情况下,采用一下适当的设计模式,会让代码更加清晰,容易维护,但同时类或方法数量也是倍增的。我们需要对业务做好充分分析,避免一上来就设计模式,避免过度设计!