由一次重构引发的对可扩展性的思考

示例

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


@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 扩展机制:定义接口、提供抽象类,并实现策略子类。如果涉及子系统之间的交互,还需要考虑服务发现。

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


posted @ 2021-05-15 16:24  琴水玉  阅读(177)  评论(0编辑  收藏  举报