一个业务场景:科二学员考试系统,当学员做了错误的操作之后,会扣分。那么如何实现这样的一个扣分引擎呢?
- 检测时机:很显然的我们需要一个定时检测的时机,比如每隔 100 毫秒,或者 1 秒,执行一次检测。这样的话,可以理解为,有一个死循环,持续进行检测。
- 检测事项:每一次检测需要检测哪些数据,当数据不符合条件,就扣这个分。
- 引擎状态:当开始考试时,需要启动引擎,进行检测。结束考试时,就停止检测。
基于这样的一个大体思路,实现一个引擎。根据具体的业务功能,先抽象出一个接口(仅仅为了展示思路,所有代码全部简化,接口只抽取信号检测):
1 | /** |
为了避免检测耗时,导致 App 卡顿,所以我们的引擎需要运行在子线程,而子线程可以接受各种消息,执行相应的任务,理所当然地使用到了 HandlerThread。
1 | /** |
通过 ExamScheduler 发送消息,来执行具体的 ExamHandler 的动作。然后提供一个 ExamEngine,来持有 ExamScheduler。
1 | /** |
然后 ExamEngine 的创建需要 ExamConfig,即考试配置。为了兼容科二、科三,这个配置还是很有必要的(科二、科三的配置不一样,但是检测逻辑一致)。
1 | /** |
这个类只是一些配置,重点在于 create 方法以及 ExamEventCreator 实现。
1 | /** |
这个 ExamEventCreator 来负责检测事项的生成,科二、科三是不一致的。检测事项,需要按照考试项目来,根据不同的项目,生成对应的 StepHandler。
1 | /** |
一个项目可能会有很多个检测事项,所以定义了 SubStepHandler,代表这个项目真正需要检测的子项。
1 | /** |
具体的检测,则是执行到 SubStepHandler handle 方法,结合当前车辆信号,ExamProcessor(ExamHandler 接口实现类),以及所处的项目 StepHandler,来进行相应的评判,来返回 SubStepResult。
1 | public class SubStepResult { |
就是累计扣的分,和需要回调出去的具体的扣分信息 DeductInfo。大多数情况下,list 的 size = 1。
因为分科二、科三,所以还需要再抽像一个 EngineManager,由它来持有 ExamEngine。这个 Manager 就只负责包装 ExamEngine 的一些方法,同时提供一些回调。科二、科三分别继承这个类,传入自身的 config,来创建相应的 ExamEngine 即可。
至此,大体的引擎设计便差不多了。
题外话
科三有几百个扣分项,理论上我们需要生成几百个 SubStepHandler,如果手动创建则会十分繁杂,所以使用了反射:
1 | private List<SubStepHandler> fixedEvents(Step step, StepConfig config) { |
拿到具体的项目和项目的配置,通过反射生成所有的扣分项,那么这些扣分项必然需要满足同一个路径,以及命名规范了。举个例子:
1 | package cn.xxxxxx.android.yyyyyy.examk2.rule.cefangtingche; |
可能存在性能损耗,但好在方便,使用之后发现性能影响甚微,便持续使用了。