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

Posted 编程大观园

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了由一个重构示例引发的对可扩展性的思考相关的知识,希望对你有一定的参考价值。

示例

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


@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 无需添加新代码就能自动支持。

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


以上是关于由一个重构示例引发的对可扩展性的思考的主要内容,如果未能解决你的问题,请参考以下文章

由一个系统重构引发的思考

由重构react组件引发的函数式编程的思考

由重构react组件引发的函数式编程的思考

谈第一个C程序Hello,World!引发的对C语言的思考

由html,body{height:100%}引发的对html和body的思考

由Redis Cluster集群引发的对几种算法的思考