一种通用的接口设计方案

时间:2021-04-16 20:18:49

1.1       背景

目前的XXXX扩展接口定义如下:

class ProductProcesser

{

public:

       virtual long process(const in_type&,out_type&) = 0;

       …. //还有其他扩展接口也定义在一起

}

讨论集中在上面process成员函数的定义。因为产品需求的变化,目前的process函数需要在两种情况下调用,因此考虑两种修改方案:

1)修改process为:long (int flag,const in_type&,out_type&)

flag代表不同应用场合

2)定义不同名字的虚函数

3)修改process为long process(int flag,void* in_pData,void* out_pdata) = 0;这种方式意图是支持不同处理参数

我们都希望这次改动之后系统的可扩展性更好

1.3       分析

首先说明,每种设计方案都有自己的优缺点,不存在一个最好的方案。只能依据软件设计的一些基本原则来分析一个设计是否有改善的余地,

1.            看接口类ProductProcess的定义,因为包含多种处理业务的扩展接口,这些扩展接口之间基本上没有关系,是松散耦合的,很显然违背了ISP(接口分离原则)

2.           long (int flag,const in_type&,out_type&)的定义,首先要求平台和产品在处理flag参数上保持一致,但只是口头或文档约定,无法避免实际代码中出现不一致的情况,造成平台和产品代码之间的紧耦合。另外针对各种业务处理流程都要求在同一个接口中实现,一旦某个业务接口发生变动,将对其他业务接口的实现造成影响,个人认为很明显违背OCP(开放封闭原则)

3.         long process(int flag,void* in_pData,void* out_pdata)的定义,想实现任意扩展业务处理接口,而且对业务处理数据的类型不限制,但实际上也无法做到,因为某个业务处理接口可能有1个参数,也可能有多个参数,对于后者,还需要包装多个参数,使用不方便,关键是利用void*传递各种数据不是类型安全的,C++中应该慎用。

1.4       考虑

能够用一种更好的方式实现long process(int flag,void* in_pData,void* out_pdata)的设计意图,甚至增强其适应范围,即对参数个数和类型都不加限制。当然是用面向对象的思想方式,通过接口,继承等技术来实现。看起来肯定比单纯的一个成员函数复杂得多,但更灵活,甚至可以统一任意扩展接口的实现方式。

1.5       我的实现方式

因为各个业务处理的参数类型,参数个数都是不同的,无法通过一个接口来定义所有的业务处理,考虑一个退化的接口类型的实现方式,如下:

一种通用的接口设计方案

ExpandInterface做为任何扩展接口的基类定义,只是提供一个类型表示,具体业务处理接口有其子接口定义, 二次开发人员的具体实现是这些子接口的子类。比如,上面分别为同步告警和接受实时告警定义了不同的子接口。这两个子接口从形式上看是一摸一样的,为什么不统一呢?我认为这里只是接口定义,重复代码可以忽略不计;他们的行为方式是不同的,定义不同的接口名非常容易理解,这样的代码可以说是自注释的;两种告警业务处理本来不相关,分开之后可以独立变化对另一个没有任何影响。从以上三方面考虑,分开定义更好。

另外,平台应该提供一个供二次开发人员设置其实现类的接口,提供这样一个扩展接口的管理类:

一种通用的接口设计方案

ExpandManager是平台提供的一个实现类,二次开发人员直接使用,它只能看到ExpandInterface接口。

对于平台开发人员来讲,就存在一个问题,它是怎么知道某个ExpandInterfaceRealAlmInterface还是SynAlmInterface呢?因为ExpandManager,ExpandInterface, RealAlmInterface, SynAlmInterface都是平台定义并实现的,从技术上讲,平台开发人员可以利用dynamic_cast转换每个ExpandInterface进行尝试,最终找到自己需要的接口。

很遗憾,我们无法利用typeid来实现ExpandManager::add(ExpandInterface*)

void add(ExpandInterface* p){

_map[typeid(*p).name()] = p; //直接对一个指针类型使用typeid不能得到其真实的子类类型

}

我们无法通过typeid(*p)来获取中间层次(这里的RealAlmInterface,SynAlmInterface)的类型信息。

为了避免在平台内部频繁使用dynamic_cast,我们需要在ExpandInterface接口上增加能够判断子接口类型的功能,很容易想到的方法如下:

class ExpandInterface

{

public:

       virtual int getProtocol() = 0;

};

class RealAlmInterface: public ExpandInterface

{

       virtual int getProtocl() { return REAL_ALM_PROCESSOR; }

};

要求平台为定义的每个业务处理接口,也就是ExpandInterface的子类实现getProtocol纯虚接口,这样ExpandManager::add(ExpandInterface*)的实现就非常简单了:

void add(ExpandInterface* p){    _map[p->getProtocl()] = p; }

通过这种方式平台开发人员也很容易获取某个业务处理接口,新增业务处理接口只需要在平台定义的时候保证其业务标识是唯一的,不重复的即可,与二次开发也没有任何关系。

但是这种方法的确还存在一个比较大的缺点,无法避免二次开发人员也重载了getProtocl接口并返回不同的值,这种重载可能是恶意的,也可能是无意的。为了避免这种情况,我们需要提供文档解释说明二次开发人员不要重载这个接口,但无法保证实际代码中会发生什么。

我们需要一个ExpandInterface中定义的接口,平台可以使用,可以修改,产品无法使用也无法修改,visitor模式可以帮助我们做到这一点。

一种通用的接口设计方案

业务类型也就是每个ExpandInterface的子类的类型信息保存在Visitor类中。二次开发人员无法修改RealAlmInterface::accept接口的关键之处是平台不对外公布Visitor类的定义,只是在ExpandInterface的定义文件中使用Visitor类的前向声明即可,这样二次开发人员多能做的事情就是实现一个空的accept函数,这样,在平台实现代码的时候,调用accept之前赋予Visitor一个合理缺省值,能够检测到这种情况,给出拒绝或警告提示,总之一切又都在平台的控制之下。

1.6       完整类图

一种通用的接口设计方案

对外提供接口:

ExpandInterface, RealAlmInterface,SynAlmInterface

对外提供的实现体,直接使用:

ExpandManager

只对内提供的定义:

Visitor

二次开发需要实现的:

ConcretRealAlm, ConcretSynAlm

1.7       其他考虑

有的业务允许多个扩展接口串行处理,这种方式的支持不建议在ExpandManagerExpandInterface层面进行改动,而是将ExpandInterface的子类,即业务处理接口设计为职责链模式(chain of responsibility),同时结合模板方法(template method)即可:

一种通用的接口设计方案

这种通用接口的设计可以用于任何需要扩展处理的地方,平台和二次开发人员之间只需要通过不会产生任何歧义的接口进行交互,而且避免二次开发人员无意或恶意的错误.