• 别再用if-else了,分享一下我使用“策略模式”的项目经验...


            “代码优化”是每个程序员都会经历的一个课题,提到优化,我最先想做的事就是干掉项目里的“if-else”。代码中,如果“if-else”比较多,不仅阅读起来比较困难,而且可维护性也会变差,如果后期在此基础上增加新的业务逻辑,会很容易产生出bug。

            网上有很多名为:“别再if-else走天下了”,“教你干掉if-else”之类的文章,基本上都会提到策略模式的应用。我最开始也是跟着文章学习,用多了自然有了自己的理解。本讲,我就把使用“策略模式”优化“if-else”的经验历程分享一下。


    正文

            “策略模式”核心就在于“策略”,实现的方式也大同小异:服务端定义统一行为(接口或抽象类),并实现不同策略下的处理逻辑(对应实现类)。更进一步的话,可以利用工厂类来统一处理客户端逻辑。

            “策略模式”的UML类图如下:

            接下来,我用实践中常遇到的业务场景,深入浅出的讲解一下:

            假如,对于不同来源(pc端、mobile端)的支付订单需要不同的逻辑处理。通常,项目中会有一个实体类 Order 承载变量,还有一个专用的 OrderService 来处理该类订单业务,方法里面有一坨 if-else 的逻辑,目的是根据订单的来源的做不同的处理,如下:

    1. /**
    2. * 支付订单的实体类
    3. **/
    4. @Data
    5. public class Order {
    6. // 订单来源
    7. private String source;
    8. // 支付方式
    9. private String payWay;
    10. // 订单编号
    11. private String orderCode;
    12. // 订单金额
    13. private BigDecimal amount;
    14. // ... 其他字段
    15. }
    16. /**
    17. * 处理支付订单的服务
    18. * 为了简化代码,处理订单的服务没有使用接口设计
    19. **/
    20. @Service
    21. public class OrderService {
    22. public void orderMethod(Order order) {
    23. if(order.getSource().equals("pc")){
    24. // 处理pc端订单的逻辑
    25. }else if(order.getSource().equals("mobile")){
    26. // 处理移动端订单的逻辑
    27. }else {
    28. // ...其他逻辑
    29. }
    30. }
    31. }

             “策略模式”就是要干掉上面的一坨if-else,使得代码看起来优雅且高大上,分析下以下几种方案:


    一、策略+工厂

    这种方案是 “策略模式”最好的诠释,完全按照UML图来,先往下看,一会再说缺点。

    1. 定义OrderHandler接口

    首先定义一个OrderHandler接口,此接口规定了处理订单的方法。

    1. public interface OrderHandler {
    2. void handle(Order order);
    3. }

    2. 定义PC和Mobile实现类

    接下来,就是实现pc端和移动端订单处理各自的handler,重写各自的处理逻辑,互补干扰。

    1. @Service
    2. public class MobileOrderHandler implements OrderHandler {
    3. @Override
    4. public void handle(Order order) {
    5. System.out.println("处理移动端订单");
    6. }
    7. }
    8. @Service
    9. public class PCOrderHandler implements OrderHandler {
    10. @Override
    11. public void handle(Order order) {
    12. System.out.println("处理PC端订单");
    13. }
    14. }

    3. 定义工厂类

    Order工厂类的取代了“if-else”的操作:根据不同的参数,匹配不同的实体类,以便执行不同的业务逻辑。

    1. public class OrderServiceFactory {
    2. private static final Map map = new HashMap<>();
    3. static {
    4. map.put("pc", new PCOrderHandler());
    5. map.put("mobile", new MobileOrderHandler());
    6. }
    7. // 对外提供的服务
    8. public static IOrderHandler getOrderService(String source) {
    9. return map.get(source);
    10. }
    11. }

    4. Junit测试

    用 Junit 测试类,简单模拟一下Controllor层的业务逻辑。

    1. @Test
    2. void payWayTest() {
    3. Order order = new Order();
    4. order.setSource("pc");
    5. OrderHandler orderHandler = OrderServiceFactory.getOrderService(order.getSource());
    6. orderHandler.handle(order);
    7. }
    8. // 输出:处理PC端订单

    二、策略+Enum

            有没有发现一个问题:开发人员不仅要关注策略实现类的业务,还要在 OrderServiceFactory  工厂类中把用到的对象先配置出来,就好像是在维护一个配置文件。

    1. static {
    2. map.put("pc", new PCOrderHandler());
    3. map.put("mobile", new MobileOrderHandler());
    4. }

            这种操作实际上是有悖于提升代码可读性、可维护性的。所以,我优先想到的就是用Enum取代工厂类,毕竟Enum的可读性更好。

    1. 定义Enum

    1. @Getter
    2. public enum OrderServiceEnum {
    3. PC("pc", new PCOrderHandler()),
    4. MOBILE("mobile", new MobileOrderHandler());
    5. private String name;
    6. private OrderHandler handler;
    7. OrderServiceEnum(String name, OrderHandler handler) {
    8. this.name = name;
    9. this.handler = handler;
    10. }
    11. // 定义public的获取方式
    12. public static OrderHandler getEnum(String name) {
    13. for (OrderServiceEnum e : OrderServiceEnum.values()) {
    14. if (e.getName().equals(name)) {
    15. return e.getHandler();
    16. }
    17. }
    18. return null;
    19. }
    20. }

    2. Junit测试

    用 Junit 测试类,简单模拟一下Controllor层的业务逻辑。

    1. @Test
    2. void enumTest() {
    3. Order order = new Order();
    4. order.setSource("pc");
    5. OrderHandler orderHandler = OrderServiceEnum.getEnum(order.getSource());
    6. orderHandler.handle(order);
    7. }
    8. // 输出:处理PC端订单

    三、注解实现

            虽然使用Enum对可读性有所改进,但是还没有达到预期,我在考虑有没有一种可能:工厂类自动加载所有的策略实现,不需要外界干预。日后增加产品族时,只需要关注实现类本身,不需要修改工厂类代码。

            我想到了“反射”,想到了“注解”,想到了利用“反射”自动加载所有实现类的“注解”,说干就干:

    1. 定义注解

    1. @Target(ElementType.TYPE)
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Documented
    4. @Service
    5. public @interface OrderHandlerType {
    6. String source();
    7. }

    2. 实现类上加注解

    1. @Service
    2. @OrderHandlerType(source = "mobile")
    3. public class MobileOrderHandler implements OrderHandler {
    4. @Override
    5. public void handle(Order order) {
    6. System.out.println("处理移动端订单");
    7. }
    8. }
    9. @Service
    10. @OrderHandlerType(source = "pc")
    11. public class PCOrderHandler implements OrderHandler {
    12. @Override
    13. public void handle(Order order) {
    14. System.out.println("处理PC端订单");
    15. }
    16. }

    3. 定义工厂类

            利用反射,自动获取 OrderHandler 接口下所有实现类,并且识别实现类注解上的 value 值,完成 map 自动装配,从此避免认为干涉。

    1. @Component
    2. public class OrderServiceFactory {
    3. private static Map map = new HashMap<>();
    4. static {
    5. // 1.利用反射,获取IMedalService接口下所有实现类的class
    6. List clazzs = getAllInterfaceAchieveClass(OrderHandler.class);
    7. for(Class clazz : clazzs){
    8. // 2.获取class的MedalType注解的value值
    9. String type = AnnotationUtils.findAnnotation(clazz, OrderHandlerType.class).source();
    10. try {
    11. // 3.保存产品族为map对象
    12. map.put(type, (OrderHandler) clazz.newInstance());
    13. } catch (InstantiationException e) {
    14. e.printStackTrace();
    15. } catch (IllegalAccessException e) {
    16. e.printStackTrace();
    17. }
    18. }
    19. }
    20. /**
    21. * 对外提供的服务
    22. * @return
    23. */
    24. public static OrderHandler getOrderService(String medalType) {
    25. return map.get(medalType);
    26. }
    27. /**
    28. * 获取所有接口的实现类
    29. */
    30. public static List getAllInterfaceAchieveClass(Class clazz){
    31. ArrayList list = new ArrayList<>();
    32. // 判断是否是接口
    33. if (clazz.isInterface()) {
    34. try {
    35. ArrayList allClass = getAllClassByPath(clazz.getPackage().getName());
    36. /**
    37. * 循环判断路径下的所有类是否实现了指定的接口
    38. * 并且排除接口类自己
    39. */
    40. for (int i = 0; i < allClass.size(); i++) {
    41. // 排除抽象类
    42. if(Modifier.isAbstract(allClass.get(i).getModifiers())){
    43. continue;
    44. }
    45. // 判断是不是同一个接口
    46. if (clazz.isAssignableFrom(allClass.get(i))) {
    47. if (!clazz.equals(allClass.get(i))) {
    48. list.add(allClass.get(i));
    49. }
    50. }
    51. }
    52. } catch (Exception e) {
    53. System.out.println("出现异常");
    54. }
    55. }
    56. return list;
    57. }
    58. /**
    59. * 从指定路径下获取所有类
    60. */
    61. public static ArrayList getAllClassByPath(String packagename){
    62. ArrayList list = new ArrayList<>();
    63. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    64. String path = packagename.replace('.', '/');
    65. try {
    66. ArrayList fileList = new ArrayList<>();
    67. Enumeration enumeration = classLoader.getResources(path);
    68. while (enumeration.hasMoreElements()) {
    69. URL url = enumeration.nextElement();
    70. fileList.add(new File(url.getFile()));
    71. }
    72. for (int i = 0; i < fileList.size(); i++) {
    73. list.addAll(findClass(fileList.get(i),packagename));
    74. }
    75. } catch (IOException e) {
    76. e.printStackTrace();
    77. }
    78. return list;
    79. }
    80. /**
    81. * 如果file是文件夹,则递归调用findClass方法,或者文件夹下的类
    82. * 如果file本身是类文件,则加入list中进行保存,并返回
    83. */
    84. private static ArrayList findClass(File file,String packagename) {
    85. ArrayList list = new ArrayList<>();
    86. if (!file.exists()) {
    87. return list;
    88. }
    89. File[] files = file.listFiles();
    90. for (File file2 : files) {
    91. if (file2.isDirectory()) {
    92. // 添加断言用于判断
    93. assert !file2.getName().contains(".");
    94. ArrayList arrayList = findClass(file2, packagename+"."+file2.getName());
    95. list.addAll(arrayList);
    96. }else if(file2.getName().endsWith(".class")){
    97. try {
    98. // 保存的类文件不需要后缀.class
    99. list.add(Class.forName(packagename + '.' + file2.getName().substring(0,
    100. file2.getName().length()-6)));
    101. } catch (ClassNotFoundException e) {
    102. e.printStackTrace();
    103. }
    104. }
    105. }
    106. return list;
    107. }
    108. }

    4. 测试

    1. @Test
    2. void orderTest() {
    3. Order order = new Order();
    4. order.setSource("pc");
    5. OrderHandler orderHandler = OrderServiceFactory.getOrderService(order.getSource());
    6. orderHandler.handle(order);
    7. }
    8. // 输出:处理PC端订单

    总结

            使用“注解”的策略模式,可以让代码维护变的更简洁,工厂类可以封装起来,开发过程中,只需要关注策略实现就可以了。

            说了这么多,难道使用“策略模式”就没有缺点吗?其实,缺点也很明显:

    1. 首先,“策略模式”虽然节省了“if-else”部分的代码,但是会产生很多的策略类,每个具体策略类都会产生一个新类,这无疑是变相的增加了代码的维护成本。
    2. 其次,如果“策略模式”封装的是多种算法逻辑,那就意味着客户端必须知道所有的策略类,理解这些算法的区别,然后自行决定使用哪一个策略类。

  • 相关阅读:
    (实战)[自动驾驶赛车-中国联赛]-合集
    SpringBoot快速入门--高级版
    计算机毕业设计SSM电脑外设销售系统小程序【附源码数据库】
    HarmonyOS真机调试报错:INSTALL_PARSE_FAILED_USESDK_ERROR处理
    如何开始使用 Kubernetes RBAC
    【计网】第三章 数据链路层(4)局域网、以太网、无线局域网、VLAN
    npm使用
    高校为什么需要大数据挖掘平台?
    java毕业设计社区团购系统Mybatis+系统+数据库+调试部署
    【ijkplayer】整体结构总结(一)
  • 原文地址:https://blog.csdn.net/weixin_44259720/article/details/126051923