• 由一个重构示例引发的对可扩展性的思考


    示例

    有时,即使使用策略模式来分离差异,也会比较麻烦。 比如保存入侵对象:

    
    @ComponentProperties(purpose = "EntitySaver")
    @Component
    public class DefaultDetectEntitySaver implements EntitySaver<DefaultAgentEventFlowContext> {
     
        private final Logger logger = LogUtils.getLogger(DefaultDetectEntitySaver.class);
     
        @Autowired
        private BounceShellSaverStrategy bounceShellSaverStrategy;
        @Autowired
        private WebshellCheckRecordSaverStrategy webshellCheckRecordSaverStrategy;
     
        DefaultSaverStrategy defaultDetectEntitySaver = new DefaultSaverStrategy();
     
        @Override
        public void save(DefaultAgentEventFlowContext flowContext) {
            DetectDO detectDO = flowContext.getDetectDO();
            select(detectDO).save(detectDO);
            logger.info("DefaultDetectEntitySaver-finished: detectType: {} agentId: {}",
                    flowContext.getEventData().getDetectType(), flowContext.getAgentId());
        }
     
        private DetectEntitySaverStrategy select(DetectDO detectDO) {
            if (detectDO instanceof BaseBounceShell) {
                return bounceShellSaverStrategy;
            }
            if (detectDO instanceof WebShellCheckRecordDO) {
                return webshellCheckRecordSaverStrategy;
            }
            return defaultDetectEntitySaver;
        }
     
        static class DefaultSaverStrategy implements DetectEntitySaverStrategy {
     
            @Override
            public void save(DetectDO detectDO) {}
        }
     
    }
     
    @Component
    public class BounceShellSaverStrategy implements DetectEntitySaverStrategy<BaseBounceShell> {
     
        @Autowired
        private BounceShellService bounceShellService;
        @Autowired
        private DockerBounceShellService dockerBounceShellService;
        @Autowired
        private WindowsBounceShellService windowsBounceShellService;
     
        @Override
        public void save(BaseBounceShell baseBounceShell) {
            select(baseBounceShell).save(baseBounceShell);
        }
     
        private BaseService select(BaseBounceShell baseBounceShell) {
            if (baseBounceShell instanceof BounceShell) {
                return bounceShellService;
            }
            if (baseBounceShell instanceof DockerBounceShell) {
                return dockerBounceShellService;
            }
            if (baseBounceShell instanceof WindowsBounceShell) {
                return windowsBounceShellService;
            }
            throw new RuntimeException("Not valid bounceShell");
        }
    }
     
    @Component
    public class WebshellCheckRecordSaverStrategy implements DetectEntitySaverStrategy<WebShellCheckRecordDO> {
     
        @Autowired
        private WebShellCheckRecordService webShellCheckRecordService;
     
        @Override
        public void save(WebShellCheckRecordDO webShellCheckRecordDO) {
            webShellCheckRecordService.save(webShellCheckRecordDO);
        }
     
    }
    
    

    不同的 SaverStrategy 的逻辑非常简单,就是选择对应的 Service 来保存对应的 DO 对象。 但即使如此简单的逻辑,还需要写这么多东西,而且还要写 if-else。 有没有更好的办法呢 ?

    有的。实际上,对于保存对象来说,差异的部分,无非是 DO 与 Service 的映射关系。 如果能够自动化建立 Map[DO, Service] ,就可以通过 DO 的 class 找到对应的 Service 来保存 DO 对象。

    现在来找一下有没有好的办法。最简单的办法,就是直接建立一个枚举,手动指定。 当然,对于我们的工程来说, 更好的是自动化建立。 看看如下 WebShellCheckRecordService 和 WebShellCheckRecordServiceImpl 的 定义,正好第一个泛型参数的类型与 Service 有着对应关系。 现在看看我们有没有办法提取出来。

    
    public interface WebShellCheckRecordService
            extends BaseService<WebShellCheckRecordDO, WebShellCheckRecordQuery, String>
     
    public class WebShellCheckRecordServiceImpl extends MongoServiceImpl<WebShellCheckRecordDO,
            WebShellCheckRecordQuery, String, WebShellCheckRecordRepository>
            implements WebShellCheckRecordService, OnContextReady, OnContextClose {
    
    

    如下所示: 建立一个 BaseServiceFactory 根据 MongoServiceImpl 的子类自动化解析和建立 DO 与 ServiceImpl 的映射关系。

    
    /**
     * @Description 根据 DO 对象找到 Service 对象
     * @Date 2021/5/14 10:34 上午
     * @Created by qinshu
     */
    @Component
    public class BaseServiceFactory implements ApplicationContextAware {
     
        private final Logger logger = LogUtils.getLogger(BaseServiceFactory.class);
     
        private ApplicationContext applicationContext;
     
        private Map<String, MongoServiceImpl> doServiceMap = new HashMap<>();
     
        @PostConstruct
        public void init() {
            Map<String, MongoServiceImpl> baseServiceMap = applicationContext.getBeansOfType(MongoServiceImpl.class);
            for (MongoServiceImpl bs: baseServiceMap.values()) {
                Class c = bs.getClass();
                // 解决cglib动态代理问题
                if (bs.getClass().getName().contains("CGLIB")) {
                    c = bs.getClass().getSuperclass();
                }
                String typeName = TypeUtils.getParameterizedType(c);
                doServiceMap.put(typeName, bs);
     
            }
            logger.info("doServiceMap: {}", doServiceMap);
        }
     
        public BaseService getService(String doClassName) {
            return doServiceMap.get(doClassName);
        }
     
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
    }
     
     
    public class TypeUtils {
     
        public static String getParameterizedType(Class c) {
            Type t = c.getGenericSuperclass();
            if (t instanceof ParameterizedType) {
                ParameterizedType pt = (ParameterizedType) t;
                Type[] types = pt.getActualTypeArguments();
                if (types != null && types.length > 0) {
                    return types[0].getTypeName();
                }
            }
            return null;
        }
    }
    
    

    现在, 保存入侵实体对象的组件代码可以写成

    @ComponentProperties(purpose = "EntitySaver")
    @Component
    public class DefaultDetectEntitySaver implements EntitySaver<DefaultAgentEventFlowContext> {
     
        private final Logger logger = LogUtils.getLogger(DefaultDetectEntitySaver.class);
     
        @Autowired
        private BaseServiceFactory baseServiceFactory;
     
        @Override
        public void save(DefaultAgentEventFlowContext flowContext) {
            DetectDO detectDO = flowContext.getDetectDO();
            baseServiceFactory.getService(detectDO.getClass().getName()).save(detectDO);
            logger.info("DefaultDetectEntitySaver-finished: detectType: {} agentId: {}",
                    flowContext.getEventData().getDetectType(), flowContext.getAgentId());
        }
    }
    
    

    现在,我们再也不需要写任何策略类和选择策略的代码了。一切都是自动化的: 当你建立 XXXServiceImpl 类时,就会自动建立 DO 与 ServiceImpl 之间的映射,而 DefaultDetectEntitySaver 就会从 BaseServiceFactory 里根据 DO 的类名获取对应的 ServiceImpl 来保存对象了。

    如果觉得获取泛型参数类型不太安全,可以考虑在 BaseService 添加一个 getEntityClassName 的方法,每个子类都实现该方法,这样会稍微麻烦一点,但是更安全。


    基于关联关系建立可扩展性

    如果我们能够进一步思考和建立应用对象之间的关联关系,那么,就很有可能做到自动添加新能力而无需添加新代码。对于本例来说,只要实现了 XXXServiceImpl extends MongoServiceImpl ,就可以使得 DefaultDetectEntitySaver 自动添加对 DO 的 save 支持,而无需对 DefaultDetectEntitySaver 作任何改动。DefaultDetectEntitySaver 就成为了通用组件,虽然它需要去保存不同的入侵对象。

    反过来说,如果对于新功能,我们需要针对每一处都作出改动,这意味着应用对象之间的关联关系还没有建立起来,仍然是孤立状态。


    现在假设入侵业务有 A, B, C, D, E, F, G, H 八个子模块。 假设来一个入侵事件类型,都要针对这八个子模块写一些代码改动,这意味着,这些模块之间的关联关系没有建立起来。 理想情况是,这八个模块中有 A, B 两个是基础模块,而其它模块依赖于 A, B 模块建立的关联关系。 那么,如果我们能够根据 A, B 模块建立应用对象之间的关联,并建立模块之间的关联关系,那么,应用对象之间的关联关系就会自动迁移到其它模块上,从而使得除了 A, B 之外的其它模块几乎无需添加新代码就能支持新事件类型。

    为什么能够做到无需添加新代码就能支持新事件类型?看上去有点魔幻,实际上并不稀奇。 因为我们可以从 C, D, E, F, G, H 解析出应用对象需要依赖的的关联关系,而这种关联关系可以通过 A, B 两个模块建立起来。这样, 其它模块只要根据这种关联关系来编写代码,就能保证根据 A,B 建立的关联关系自动应用到 C, D, E, F, G, H 上,从而使得 C, D, E, F, G, H 无需添加新代码就能自动支持。

    即使达不到理想情况,如果能够建立一些有效的关联,也会使得开发量减少很多,而无需针对每个模块都写相似的代码。


    可扩展性的实质

    变化一直是软件开发的重要关注点之一。可扩展性的实质即是分离不变与变化,识别和表达变化。设计可扩展性良好的软件是实现研发效率提升的关键手段之一。

    业务会朝哪个方向变化,这是值得思考的问题。 比如来一个新的入侵事件类型,哪些方面会发生变化:

    • 新入侵事件有自己特定的属性;

    • 针对特定的属性有特定的处理;

    • 新入侵事件有特定的处理流程。

    一旦我们能够识别变化,就能思考以何种技术手段来实现可读、可理解、可维护地表达变化。常用的技术手段是 SPI 扩展机制:定义接口、提供抽象类,并实现策略子类。如果涉及子系统之间的交互,还需要考虑服务发现。

    而对于不变的部分,则需要建立通用的技术机制(统一表设计、关联关系的抽象与建模、通用流程、工具类、包含共性的基类、代码技巧)来沉淀。


  • 相关阅读:
    day30---内置函数
    day30---绑定方法与非绑定方法
    元类以及属性查找
    python 内置方法/魔法方法
    python 面向对象高级-反射机制
    centos7下jenkins升级
    屏蔽百度右侧热搜
    centos7部署汉化版gitlab
    CentOS 7 安装 Jenkins
    centos7安装与配置ansible
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/14771844.html
Copyright © 2020-2023  润新知