示例
有时,即使使用策略模式来分离差异,也会比较麻烦。 比如保存入侵对象:
@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
反过来说,如果对于新功能,我们需要针对每一处都作出改动,这意味着应用对象之间的关联关系还没有建立起来,仍然是孤立状态。
现在假设入侵业务有 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 扩展机制:定义接口、提供抽象类,并实现策略子类。如果涉及子系统之间的交互,还需要考虑服务发现。
而对于不变的部分,则需要建立通用的技术机制(统一表设计、关联关系的抽象与建模、通用流程、工具类、包含共性的基类、代码技巧)来沉淀。