前言
随着大量分布式计算和 SOA 类型应用的加入,企业内部具体应用的服务功能也趋于多元化,为了快速实现不断变化的业务需求、充分利用团队开发资源,很多架构师在高层技术设计阶段往往会在应用架构中集成一些公共库,完成诸如数据访问、日志记录、异常管理、授权控制等常规技术实现,从逻辑视图看他们是一组伴随业务逻辑部分的辅助技术内容:
图 1:常用逻辑控制辅助机制布局
但是就一个具有规模化信息系统的企业环境而言,常常需要对应用实施很多全局性的技术处理,范围可能涉及整个应用层面或整个组织层面。例如:每个企业、每个行业内部总会有一些主要业务数据,这些数据又分散在企业不同的业务系统中——生产、财务、风险分析和决策支持等,但是出于安全的考虑可能要对这类数据在不同系统的处理过程进行使用安全审计,这时候处理上就有了两个方式:
- 逐个修改现有功能公共库,把审计功能嵌入到每个功能库之中。
- 纵向提供一个统一的组织级审计策略控制机制,每个公共库调用之。
采用前者可以在短时间内快速实施改造,但如果这类策略变化相对频繁,而且又增加很多不同数据控制要求(例如:对于高价商品、低诚信度客户进行报警),那么反复改造带来的开发、测试、重新部署成本相对就比较可观,究其原因就是由于每个应用内部的个例处理对象与整体处置要求对象间耦合过于紧密。因此,本文试图通过增加一个策略控制框架来用尽可能小的代价集中解决这类问题,设计目标如下:
- 自身结构要灵活,可以动态的适应多种策略要求。
- 以配置为中心,便于测试和部署人员根据需要以 Plug & Play 的方式修改和维护策略。
- 同时对业务调用的前期和后期提供可以回调的响应机制。
- 定义多种策略匹配规则,确保规则可以按需被公共库的不同实现层次组成调用(程序集、类和接口、方法)。
- 确保本地调用和跨进程调用的界面一致性。
基于策略的控制主要包括两个方面:对操作实体执行逻辑部分的影响和对业务信息内容的影响。前者需要嵌入到其他操作实体的方法内部,因此为了尽量减少对它们的影响,要设计成一个单一的接口,而且接口所约定的控制方法也要尽量唯一;后者则要同时考虑在操作方法实施前后对业务信息内容增加不确定数量的预处理(Pre Process)和后续处理(Post Process)。另外,有时候策略控制的影响还会涉及到对象的构造过程(Constructor),为了屏蔽相关构造影响细节,可以采用一个工厂(Factory)割裂相关依赖。出于说明方便的考虑,下文笔者通过示例为一个简化的多通道日志系统穿插纵向的策略控制,务求从给读者展现一个明晰的梗概实现视图。
(注:说明中为了抽象和进一步扩展的考虑,所有类图的定义尽量采用抽象的接口和抽象类描述,如果说明上提及“某某抽象类实例”请理解为“该抽象类某个实体子类的实例”,提到“某某接口实例”时也请理解为“该接口实现类的实例”。)
初始示例日志系统
下面是示例系统的抽象初始静态结构:
图 2:示例日志系统静态布局
说明:
- 为了隔离客户程序与日志系统的耦合,增加了接口 ISource 负责定义客户程序记录日志的抽象操作。
- 抽象类 LogEntityBase 描述了基本日志实体内容,考虑到分布式系统环境下日志集中收集的要求,相关的串行化工作、时间戳登记等操作也都在该抽象基类完成。
- ISource 经由 LogEntityBase 与抽象类 FilterManagerBase 关联,而 FilterManagerBase 主要管理一组 IFilter 对象,根据每个具体 IFilter 类型的命中规则对 ISource 提交的 LogEntityBase 进行过滤规则的匹配。
- 接口 IPublisher 则是对筛选后的 LogEntityBase 通过 IRender 进行一些实际写入日志持久层前进行一些发布处理的,例如:按照表格方式展开包括 Dictionary 内容的数据、按照层次型的 XML 格式展开日志消息内容的描述部分、对某些复杂消息项目进行串行化。
- 整理好的 LogEntityBase 最后提交给 LogWriter 实体类,它根据预先维护好的 ITraceListener 列表逐个发送日志消息,然后由每个 ITraceListener 完成具体写入持久层的操作。
在设计整个策略控制之前,先设计好一个灵活且很易于扩展的 Policy 对象非常重要,毕竟“九层之台,起于垒土”,因此本文首先花大力气设计好一个具体的策略对象,然后为了适应企业应用环境的需要,在此基础上还会增加必要的辅助机制。整体系统的设计将沿着这样一个主线展开:
图 3:策略控制系统的展开步骤
最终的策略系统也将由如下几个部分组成:
图 4:策略系统的整体逻辑结构
第一个策略(Policy)对象
Policy 对象简单设计
这其中主要涉及两方面问题,一个是策略的适用问题,由于策略一般影响的是目标实例的操作,因此“策略适用”主要解决的也是策略对象如何作用于目标实例的相关方法(Method、Property)的问题,一个是策略适用后的响应问题。前者需要增加一个 MatchRuleCollection 用于管理所有的适用规则,后者需要定义一组回调(Callback)句柄,目的主要是为了为了给策略控制框架提供必要的通知响应机制,下图就是一个抽象策略对象的静态结构。
图 5:抽象 Policy 类型定义
说明:
- PolicyBase 被定义为抽象类,一方面说明它仅仅描述了一个策略对象所应具有的基本抽象特征,另一方面也是为每个具体策略对象提供扩展上的便利。主要属性包括一个名称(Name)、一组与之相关的匹配规则(MatchRuleSet)、一组指向对象实例的回调句柄(Callbase)和一个标示策略有效期的结构体属性(PeriodOfValidity)。同时将策略的执行过程定义为一个序方法(Virtual Method)以供策略实体类覆盖其执行过程。
- 这里引入策略有效期主要为了便于给策略控制的实施过程提供预定式、预警式控制方式,毕竟很多策略的生效时间都在一些公休日期(元旦、五一、十一、某个月初等),生效时间也都是零点,因此考虑到运行维护人员工作时间的安排增加这些可预定内容比较必要。
- Callback 保存了一组委托(Delegate),通过这种对象化的函数指针定义 PolicyBase 各个响应操作的引用。
- MatchRuleSet 则是管理了一组 IMatchRule 的集合类型,负责遍历每一个 IMatchRule 以确定当前的业务操作方法或者业务数据是否满足命中条件。不过抽象考虑到每个 MatchRule 可能还会出现可能的依赖、嵌套关系,为了屏蔽每部组织结构,MatchRuleSet 还需要借用 Iterator 模式隔离内部具体 IMatchRule 组织布局。考虑到效率并充分利用.Net 语言特点的情况,实现一个 Iterator 模式相对很容易,可以采用 IEnumerable< IMatchRule> + yield return IMatchRule 的方式。
图 6:PolicyBase 匹配规则的 Iterator 模式实现
业务化语言的匹配规则设计
从某个角度看,策略对象对于匹配规则的解析能力几乎决定了整个策略控制体系的智能性、决定了它与业务的吻合度,抽象来看匹配规则 MatchRule 的功能如下:
图 7:匹配规则 MatchRule 的功能
从上面的说明可以看出匹配规则本身具有一定的组合特性,即 Context、Data、Logic 和 Type 本身可能通过组合形成一个统一的 PolicyBase,同时为了隔离 MatchRule 管理能力与每个具体匹配规则分析能力,还需要引用一个额外的规则解析部分 IRuleParser。
图 8:匹配规则解析部分的静态结构
说明:
- 每个抽象规则对象 IMatchRule 本身通过 Composite 模式组合在一起,使得其可以同时适应 Context、Data、Logic、Type 及其组合的规则定义情况。
- 由于对于每个具体领域的规则都会有很多不同,因此为了隔离规则解析部分与实际领域规则领域信息的区别,这里增加了一个 RuleParserProxy,也就是通过 Proxy 来间接获得各个领域规则分析。
- 考虑到不同企业都有自身不同的规则语言、不同的计算要求,因此需要为 IRuleParser 增加一个扶助的具体规则函数解析对象 IFunction,IFunction 与 IRuleParser 最大的不同就是 IFunction 计算的结果是不定的,例如:数学计算结果可能是 double、逻辑计算的结果是 bool、字符串计算的结果为 int 或 string,而 IRuleParser 的结果在本文的上下文中主要就是 bool,也就是匹配规则判断后是否匹配这个结果。
- 考虑到企业内部不同项目的建设次序不同,因此技术表示上对于同一个业务信息项目有所不同(例如:税号可能被称为 TaxID、Tax_ID、SH),所以为了避免上层策略逻辑部分对于规则解析的耦合性问题,增加了一个 ISynonym 用于协助解决有关同义词处理问题。
规则匹配部分的性能考虑
对一个执行过程阶段而言,策略与规则的匹配关系是相对固定的,同时每个匹配关系从其持久介质到内存实例化后本身也是相对固定的。如果每次执行过程反复进行解析会浪费过多的处理器计算和内存空间,因此这里会增加策略类型与匹配规则可用示例的匹配关系缓冲。如上文所说为了避免 PolicyBase 与具体 IMatchRule 的一一对应,因此还需要增加一个 MatchRuleSet 与 IMatchRule 对应关系的缓冲。
图 9:为策略对象匹配关系关联增加的缓冲机制
说明:
- 缓冲的加入一方面大大减少了每个 PolicyBase 相对固定部分的创建工作,同时通过这种 Flyweight 模式的应用,最大可能地减少了 IMatchRuleSet(及其各个组成实体)对于内存的占用。
图 10:每个 PolicyBase 类型中相对固定的部分
2. PolicyBase 与 MatchRuleSet 的对应关系由一个 PolicyMathRuleSetCacheRecord 对象保存,他们按照 PolicyName 和 MatchRuleName 两个联合关键字保存在 PolicyMatchRuleSetCache 中。后续 PolicyBase 创建前先从 PolicyMatchRuleSetCache 检索是否已有缓冲好的 MatchRuleSetRecordset 对象,并把相关的 List 解析结构直接引用,否则作为第一个创建者生成对应结构后现保存在 Cache 中,然后也适用 Cache 中既有的这个缓冲对象。这里 PolicyMatchRuleSetCache 被设计为一个静态类(static class),它本身以 Flyweight 的方式向所有 PolicyBase 实例提供既有缓冲记录的实例。
3. 需要说明的是这里 PolicyBase + MatchRuleSet 与 PolicyMathRuleSetCacheRecord 的对应关系是双 Key 的字典关系,所以这里增加了一个 Helper 类型——DualKeyDictionary,它本身继承自 Dictionary,只不过内部增加了一个 K1 + K2 的合并然后映射到 K 的操作(例如:为了确保唯一性,可以采用 K1 + K2 字符串关联后散列处理的结果作为 K)。
4. MatchRuleCache 则是另一个静态缓冲类型,它主要保存的是每个具体解析后的 IMatchRule,因为 MatchRuleSet 与 IMatchRule 本身是 M :N 的关系,也就是一个 IMatchRule 本身可以属于多个 MatchRuleSet,同时它也可能和其他一些 IMatchRule 一起组成一个 MatchRuleSet,因此除了让各个 MatchRuleSet 可以重用既有的 IMatchRule 增加了这个缓冲。
响应机制的设计
响应机制是 PolicyBase 的后续动作阶段,本身是一组委托(Delegate)的集合,而每个委托指派的既有可能是策略控制系统内部的方法也可能是外部控制逻辑的方法,加之每个方法的参数列表、返回值类型、甚至于每个参数的封送(Marshal)模式(按值封送 Marshal by Value / 引用封送 Marshal by Reference)也有很大差异,因此本身响应机制要设计为“响应模式层”和“响应交互层”两个层次,这两个层次的关系类似于 TCP / IP 协议栈中 IP 层与 TCP 层的关系:
- “响应模式层”主要管理每个委托的执行调度过程;
- 而“响应交互层”则负责完成与每个方法实际交互的过程。
响应交互层
交互层的对象描述要最终和可能的方法描述一致,因此设计上最终的类型系统要归到反射(System.Reflection)类型对象系统上,相关映射关系如下表:
图 11:响应类型与反射类型的最终映射关系
同时抽象来看,一个方法的调用过程就是定位到一个 MethodBase,然后向其传入需要的参数集合,最后获得返回结果的过程,因此响应交互层的静态结构如下:
图 12:响应交互层静态结构
说明:
- IParameterInfoCollection 本身继承自接口 IList ,为了使用方便,它本身通过索引器(Indexer)按照参数名称对外提供 ParameterInfo 类型的实例。
- IMethodReturn 接口用于描述一个调用的返回结构,包括三个部分,其中 Return 表示参数的返回值,ReturnContext 包括了调用中生成的一些上下文信息,可以把异常也保存在这里,OutParameters 则是考虑到.Net 语言提供纯 Output 类型的参数,而这个参数本身又不包括在调用方法内部(与引用 ref 型不同),所以 IMethodReturn 部分增加了一个 OutParameters 结构。
- 上层 IMethodCaller 接口负责按照“准备参数”、“准备上下文”、“调用方法”、“从 IMethodReturn 获取调用结果”的次序依据反射直接与一个实际的方法目标进行交互,交互过程被抽象为一个统一的 Invoke 方法。
响应模式层
在完成了响应机制底层结构的设计之后,就要从调用模式层面设计响应机制与策略类型的动态执行过程。为了避免 PolicyBase 具体管理每个响应机制的执行过程,这里采用链式方式让响应机制自动完成一系列联动的相应调用,也就是应用 Chain of Responsibility(职责链)模式的过程,因此这里需要修改一下 IMethodCaller 的 Invoke 方法,增加一个指向下一个 IMethodCaller 实例的引用。
图 13:响应部分的链式调用机制(数据结构逻辑视图)
图 14:响应部分的链式调用机制(静态结构)
策略对象说明
至此,已经完成了整个策略系统的基石——PolicyBase 的设计,通过上面的介绍相信各位读者已经对于最初的静态结构有了一个相对直观的认识。
作者简介:王翔,全国海关信息中心高级架构师,从事海关主要广域分布式系统的设计和实施,多次参与各业务系统的优化。此外,作为信息安全工作组副组长,他还一直致力于应用密码技术和公钥基础设施保障海关业务的安全运行。此外,他还是《程序员》杂志的专栏作者。
评论