实现可扩展代码的四步曲
对修改关闭,对扩展开放。
可扩展性
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须大规模重构或者重建。
总而言之,就是尽量不改变原有代码。因为改变原有代码,就很可能会影响到现有功能,需要评估影响范围和回归测试,而且未必能够评估和测试完全:BUG 总是藏在很隐晦的角落,且现代软件非常复杂,往往有超距依赖,仅仅通过代码审查和测试难以看出和测出来。
可扩展性的益处: 快速响应业务变化,最大程度降低对现有系统的影响。
开闭原则
实现系统和代码可扩展能力的基本原则是开闭原则。开闭原则的定义是:对于软件实体(模块、类、方法等)而言,通过新增实体,而非修改原有实体来解决。
Software entities should be open for extension,but closed for modification.
不过,开闭原则只是提出了一种重要理念,并没有告知如何去实现。
如何实现开闭原则呢? 可通过四个步骤来实现:
- 识别变化;
- 抽离共性,定义接口;
- 实现子类;
- 注入子类实现,实现处理框架。
下面,用一个例子来说明。
实战例子
在通用入侵检测流程中,往往会针对一些入侵场景做一些特殊的预置处理。比如内存恶意代码的处理:
/**
* 走通用检测流程,检测结果预置处理
*/
@Component
public class DetectResultPreparationHandler implements FlowComponent<DetectionFlowContext> {
private static final Logger LOG = LogUtils.getLogger(DetectResultPreparationHandler.class);
private final MemBackdoorRuleHelper memBackdoorRuleHelper;
@Autowired
public DetectResultPreparationHandler(MemBackdoorRuleHelper memBackdoorRuleHelper) {
this.memBackdoorRuleHelper = memBackdoorRuleHelper;
}
@Override
public FlowResult process(DetectionFlowContext context) {
AgentDetection agentDetection = AgentDetectionHelper.from(context);
List<MatchSegmentDTO> matchSegments = agentDetection.getAgentDetectionDetail().getDetail().getMatchSegments();
if (CollectionUtils.isNotEmpty(matchSegments)) {
if (!handleMemMaliciousCode(context, agentDetection, matchSegments)) {
LOG.info("no yara rules hit. detectionId:{}", agentDetection.getDetectionId());
return FlowResultHelper.terminateResult();
}
}
LOG.info("PreparationHandler ok. detectionId:{}", agentDetection.getDetectionId());
return FlowResultHelper.continueResult();
}
/**
* 处理内存恶意代码的前置
*/
private boolean handleMemMaliciousCode(DetectionFlowContext context, AgentDetection agentDetection, List<MatchSegmentDTO> matchSegments) {
// many codes ...
}
}
这样固然可以解决内存恶意代码预置处理的流程,但如果再来一个入侵场景需要做预置处理呢? 那就需要在这个类里继续堆砌代码,久而久之,这个类就会非常膨胀,难以维护。
如果再来一个类似需求,是否可以做到开闭原则,只新增类,而不是修改已有类?
要做到这一点,其实并不困难。向前一步思考即可。
第一步: 识别变化。
我们意识到,很多入侵场景的检测流程都是在通用检测流程的基础上有小许的变化,这种变化很可能是比较频繁的,需要封装起来。
第二步:抽离共性,定义接口
通过上面的例子,可以看出,这类预置处理通常包含两个部分: 1. 满足什么条件才处理; 2. 在什么条件下处理后继续流程,什么情况下处理后终止后面的流程。
因此,可以定义接口:
/**
* 通用入侵检测流程里上报 detection 前置结果处理
*/
public interface DetectPreparedHandler {
/**
* 是否要处理
* @param context 流程上下文语境
* @return 如果需要做预置处理,返回 true, 否则返回 false
*/
boolean need(DetectionFlowContext context);
/**
* 对 agentDetection 中的预置处理
* @param context 流程上下文语境
* @return 是否要继续处理,继续处理返回 true,否则返回 false
*/
boolean handle(DetectionFlowContext context);
}
第三步: 实现子类
这一步,只要把原有实现封装成接口的一个实现,做一点代码移动即可。
/**
* 内存恶意类预置处理
*/
@Component
public class MemMaliciousCodeDetectPreparedHandler implements DetectPreparedHandler {
private static final Logger LOG = LogUtils.getLogger(MemMaliciousCodeDetectPreparedHandler.class);
private final MemBackdoorRuleHelper memBackdoorRuleHelper;
@Autowired
public MemMaliciousCodeDetectPreparedHandler(MemBackdoorRuleHelper memBackdoorRuleHelper) {
this.memBackdoorRuleHelper = memBackdoorRuleHelper;
}
@Override
public boolean need(DetectionFlowContext context) {
AgentDetection agentDetection = AgentDetectionHelper.from(context);
List<MatchSegmentDTO> matchSegments = agentDetection.getAgentDetectionDetail().getDetail().getMatchSegments();
return CollectionUtils.isNotEmpty(matchSegments);
}
@Override
public boolean handle(DetectionFlowContext context) {
AgentDetection agentDetection = AgentDetectionHelper.from(context);
List<MatchSegmentDTO> matchSegments = agentDetection.getAgentDetectionDetail().getDetail().getMatchSegments();
return handleMemMaliciousCode(context, agentDetection, matchSegments);
}
/**
* 处理内存恶意代码的前置
*/
private boolean handleMemMaliciousCode(DetectionFlowContext context, AgentDetection agentDetection, List<MatchSegmentDTO> matchSegments) {
// many codes ...
}
}
第四步: 注入子类,实现处理框架
/**
* 走通用检测流程,检测结果预置处理框架
*/
@Component
public class DetectionPreparationHandler implements FlowComponent<DetectionFlowContext> {
private static final Logger LOG = LogUtils.getLogger(DetectionPreparationHandler.class);
private List<DetectPreparedHandler> detectPreparedHandlers;
@Autowired
public DetectionPreparationHandler(List<DetectPreparedHandler> detectPreparedHandlers) {
this.detectPreparedHandlers = detectPreparedHandlers;
}
@Override
public FlowResult process(DetectionFlowContext context) {
AgentDetection agentDetection = AgentDetectionHelper.from(context);
for (DetectPreparedHandler detectPreparedHandler: detectPreparedHandlers) {
if (detectPreparedHandler.need(context)) {
if (!detectPreparedHandler.handle(context)) {
LOG.info("no need handle. handler: {}, detectionId:{}", detectPreparedHandler.getClass().getSimpleName(), agentDetection.getDetectionId());
return FlowResultHelper.terminateResult();
}
}
}
LOG.info("PreparationHandler ok. detectionId:{}", agentDetection.getDetectionId());
return FlowResultHelper.continueResult();
}
}
战略与战术
从上述例子可以看出,要实现系统和代码可扩展性,首要的是能够正确识别和预判变化,这是战略层面;其次,才是使用接口、类与设计模式来封装和实现变化,这是战术层面。
战术层面是基于接口的编程,运用设计模式来封装变化及对象职责之间的交互。
战略层面则需要根据具体业务、业务实战经验、直觉来判断。时常问:这里可能出现什么变化?变化频度如何?如何封装这种变化?或许能够更好滴增进对扩展性的理解。
小结
实现代码可扩展的基本设计原则是开闭原则,即对扩展开放、对修改关闭。要做到这一点,需要经过四步: 识别变化、抽离共性和定义接口、实现子类、注入实现和处理框架。