多分支相似逻辑重构设计
工作中经常碰到很多分支的处理逻辑,但是每个分支的处理逻辑相似,只是具体某些字段或者说比较逻辑等有不同。如果不思考直接写代码很容易出现一大堆的if else出现,这种代码难于维护、冗余特别严重。这个时候其实我们需要对于结构进行重新设计,提高扩展性和可维护性。
拿个实际的例子,最近在弄的某促销下系统,对于一个展示情况,在活动类型=1的时候要xxx,活动类型=2要xxx,这个如果用ifelse很容易实现,但是里面很容易出现特别多的冗余代码,拆出来不好拆,不拆又特别糟心。比如下面的代码:
//1.普通 ifelse方式 public static Object handleByIfElse(int id) { if (id == 1) { return "aaa"; } if (id == 2) { return 2; } return null; }
我们实际情况中肯定比这个要复杂的多,return一般也会有大篇幅的代码段存在,代码的可读性非常差。
于是我们可以设计一下处理逻辑,在确定每个分支下面的处理逻辑大体相同后,我们可以创建一个抽象类,抽象类中有一个抽象方法供子类覆盖,同时还有一个protected的方法是所有子类公用的,这些protected的方法就是ifelse分支中重复的代码。如此,结构变成:
public abstract class Handler { abstract Object handle(); protected HandleConfigurations getConfiguration() { return new HandleConfigurations(); } // @PostConstruct protected void regist() { HandlerEntrance.regist(this); } }
代码非常简单,拿个postconstruct的内容后面解释,其余的部分其实就是一个protected方法供所有子类共享,一个abstract方法供所有子类覆盖。
public class Handler1 extends Handler { @Override Object handle() { HandleConfigurations handleConfigurations = getConfiguration(); return handleConfigurations.toString(); } }
public class Handler2 extends Handler { @Override Object handle() { return null; } }
如此,我们把所有的分支全部重构称为一个一个的子类,需要变动哪一个直接找到对应的子类进行修改。有新的分支,就可以直接新增一个子类,这样子代码可读性提高了很多。
但是我们还有一件需要做的事情就是需要让请求具体分发到具体的子类上面,我们可以简单采用switch来进行:
//2.通过switch方式 public static Object handleBySwitch(int id) { switch (id) { case 1: return new Handler1().handle(); case 2: return new Handler2().handle(); default: return null; } }
我们会发现,如果我们这样子做,switch会限制只有基本类型和枚举才可以,如果其他数据结构我们还需要用ifelse,而且代码也相当不优雅。所以我们采用map来进行:
//3.通过初始化好的map进行映射 private static Map<Integer, Handler> handlerMap = Maps.newHashMap(); static { handlerMap.put(1, new Handler1()); handlerMap.put(2, new Handler2()); } public static Object handleByMap(int id) { return handlerMap.get(id).handle(); }
我们初始化好,传入参数与具体处理的子类的对应关系,在入口拿到参数后获得子类的实例后直接进行处理。但是这样子,我们不得不维护一个map来做对应关系,或者在xml里面进行配置,这样子如果map中的值特别多,也会变成一大坨,相当难看。
所以我们需要想一个办法来避免这种情况,反射可以做到我们想要的:
//4.通过反射 public static Object handleByReflection(int id) { try { Handler handler = (Handler) Class.forName("com.mt.design1.Handler" + id).newInstance(); return handler.handle(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; }
通过反射,我们可以直接根据传入的参数直接生成到具体的类名字,然后拿到这个类的实例。但是这个做法还有一个比较恶心的地方就是我们需要写死类的绝对路径,如果我们这么做,代码发生重构,会出现classnotfound的情况,而且这种传入办法效率并不高,当然我们可以在初始化后放入map中,然后下次冲map的缓存中获得类的实例就可以。然而这么做还是觉得不太舒服。
我们回到第三步的内容,如果我们可以做到map由子类自动去注册,这样子我们似乎就可以在传入的时候不做考虑,也不用专门维护对应关系,有新逻辑只要专注于把具体处理的子类写完就可以了。于是我们可以这样子做:
1、定义一个注解,用于表示该子类的key属性:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface IdAnno { int id(); }
我们将该注解注入到具体的子类中:
@IdAnno(id = 1) public class Handler1 extends Handler { @Override Object handle() { HandleConfigurations handleConfigurations = getConfiguration(); return handleConfigurations.toString(); } }
这种做法可以让我们在一个子类中获得了所有需要的内容,下一步我们要考虑的就是将这个实例在系统启动的阶段就加载到jvm中:
//5.通过注解初始化和刷新map private static Map<Integer, Handler> handlerMapByAnnotation = Maps.newHashMap(); public static void regist(Handler handler) { if (handler.getClass().isAnnotationPresent(IdAnno.class)) { handlerMapByAnnotation.put(handler.getClass().getAnnotation(IdAnno.class).id(), handler); } }
刚才抽象类中的@PostConstrct注解的方法我们就了解到,如果子类在初始化的时候,就会进行注册操作,注册时候会将类的实例放入缓存的map中(其实和spring的beanfactory类似),用的时候直接获得就可以了:
//5.通过注解初始化和刷新map private static Map<Integer, Handler> handlerMapByAnnotation = Maps.newHashMap(); public static void regist(Handler handler) { if (handler.getClass().isAnnotationPresent(IdAnno.class)) { handlerMapByAnnotation.put(handler.getClass().getAnnotation(IdAnno.class).id(), handler); } } public static Object handleByAnnotation(int id) { return handlerMapByAnnotation.get(id).handle(); }
最后,我们考虑的就是在系统的启动阶段就得把子类的初始化的动作给完成,如果你使用了spring框架,那可以在子类上面加上@Service @Component等注解就可以:
@Service @IdAnno(id = 1) public class Handler1 extends Handler { @Override Object handle() { HandleConfigurations handleConfigurations = getConfiguration(); return handleConfigurations.toString(); } }
如果并没有使用spring框架,那这个里面可以自己写一个读取某文件夹下面的类然后进行初始化的util执行,但是这种做法就得不偿失,还是可以用3或者4来进行了。
Ok,探索到此为止。