仿基因编程导论
基于接口是面向对象编程的一个重要的基础。使用基于接口的编程模式,程序可以获得兼容方面的好处。新的程序只要遵循以前的接口定义就可以与以前的应用完全兼容。但这种编程模式,没法应对功能扩展。这篇文章所要介绍的仿基因编程正是为了解决功能扩展这个问题而产生的一种编程模式。
本文所要讲述的是基因编程的基本原理。文中涉及到了一些面向对象和泛型编程中所有的一些概念。如果读者有这两方面的经验很快就会理解和运用仿基因编程技术。
主要原因有三个:一个是基于接口的编程扩展能力差。 一个是接口用法太复杂。一个是接口有一种天然的排它性。
第一点我想程序员们比我还要清楚,就不讲了。
说到第二点程序员们也许会惊讶,会说:“用法太复杂! 不会吧,不就是建立一个接口,然后再直接调用它的方法不就行了嘛。不复杂呀。”但是想一想,接口也是一些功能块,它的职责也是数据加工处理,要是只暴露数据不是更好吗?而且如果要进行双向通信,客户端做回调函数的设置,这看起来就有点怪,要是把这个也做的象一个一般的数据处理就好了。仿基因编程则能很好的处理这两个问题。在仿基因编程模式下,你的接口可以任意地扩展功能,而且客户端、服务器端的概念也不在有严格的区分了。再也没有设置回调函数这一说了,双向通信是模型的天然特性。
接口的排它性所指的是这样的一个问题,一般我们很难在同一个应用中同时使用同一接口的不同版本或同一接口的不同实现。比如你想同时使用接口Icontrl的1.0版本和2.0版本,再比如你想同时使用A公司和B公司的接口Icontrl。在这两种情况下,如果你不做一些特殊处理的话你将除了能得到一个编译错误(类型重定义)外什么也得不到。仿基因编程模式为解决这个问题提供一个简单、易用的方法。
好了,关于为什么要使用仿基因编程,如果这三个理由还不能说服你的话,我就真的没有必要再说别的了。
仿基因编程是在传统的基于接口的基础上接合双向多态和模板技术及动态联编这三项技术而产生的。但它的基本精神却和基于接口的基本精神正好相反。 基于接口的编程是通过对外暴露方法隐藏数据,实现时利用动态多态来完成实际的任务。而仿基因编程却是通过对外暴露数据隐藏方法,实现时利用双向动态多态来完成实际的任务。仿基因编程有一个重要目标,那就是方便的代码扩张。
在进行仿基因编程的具体方法之前我们先把仿基因、基于接口这两种编程模式作一个对比,来进一步地了解仿基因编程所要解决的问题。这里有一个两者的模型图。我们将结合这两个图来说明这些问题。见图1-1、图1-2
图1-1
图1-2
图1-2中的一对交互的接口从形状看有点象DNA的双螺旋结构。实际上它的一些生长过程也与遗传和变异很相象。这正是仿基因编程这个名字由来。从这两个模型的对比中我们能发现这样一些问题。
l 仿基因编程中不再有严格意义是的客户端和服务器端的区分了。而基于接口的编程中这两者有明显的区分。
l 仿基因编程中的接口只向外公开数据,再没有其它。而基于接口的编程中接口要向外公开三种类型的成分,方法中、属性、回调机制。
l 仿基因编程中数据交换这一种操作外再没有其它。而接口的使用就要区分三种性质的使用方法。
l 如果要进行功能扩充,在仿基因编程中只要新加一种数据就行了。而且这个新数据可以由接口的使用方进行扩充。而基于接口的编程想要对接口进行功能扩充是不可能的。
从这些对比中我们发现相比基于接口的编程,仿基因编程的用法简单了,但灵活性却极大地提升了。
对所要做的工作进行抽象化是编程的根本。只有从实际要完成的任务中抽象出一个一个的概念,再对这些概念进行进一步的加工整理,进行更高一级的抽象化,最后才能确定怎么样编码来实现实际的工作。在进行仿基因编程的实现方法之前我们先对编程这种工作进行一下抽象化,来抽象出编程工作中最最根本的概念。经过抽象,我们发现所有的编程工作中只有两个概念是必不可少的。一个就是数据,一个就是数据处理的方法。我们就把这两个概念作为什么基因编程的源头,依据这个源头一步步地深入来讲述要进行仿基因编程所必需要处理的几件事情。
为了实现前面提到的哪些功能,我们需要一些基本的东西。这就一个什么也没有的数据和一个什么也不做的接口。它们看起来像这个样子。见图2-1。
图2-1
这是我们在画布上的第一笔。来看看这第一笔为我们提供了哪些基础。首先我们有了一个抽象数据类型DataBase,这个数据类型没有任何内容,只有一个纯虚的析构函数。另外我们还有了一个很简单的接口InterfaceBase,这个接口接受DataBase类型的数据的流入和流出。
这时我们有了无限多种可以接受无限多种数据类型的接口了。不是吗?从InterfaceBase派生而来的任意的接口可以接受从DataBase派生而来的任意数据。可是由于子类型向上转型的过程中,子类型的信息丢失了,我们的接口不能鉴别子类型的具体类型了,而是只把它当做DataBase来处理。而DataBase却是一个什么内容也没有的类型,这样我们什么了也做不了。我们需要一个机制来把子类型鉴别出来。
由于子类型在向上转型中类型信息丢失,导致我们上面的模型成了一个什么事也干不了的模型。要是能把这些子类型信息想个办法找回来就好了。要找回这些信息我们首先会想到RTTI。但使用RTTI会让我们的扩展被限制。所以要放弃RTTI的方法。(关于RTTI详细的情况在这里就不说了。)现在要是每一个子类型都能提供一个能力来让接口确定它的类型就好了。要实现这样的一个功能并不难。只要用dynamic_cast进行转换就行了。有了这些我们就可以在不对接口进行任何改动的情况下,对接口进行功能扩充了。而且这个扩充是双向的,可以由接口的提供者扩充,也可以由接口的使用者扩充。
可以想象,如果依照上面的方法来做模型,那么数据类型的增长速度会不可思议的快。最终我们将会得到一个数量不可思议多的数据类型集。要管理这个庞大类型集将会非常困难。为此,我们需要对数据类型进行分类,让每一个接口所能处理的数据类型都能与这个接口产生天然的联系。这里有两个天然的分类标准,一个是公司,一个接口类型。首先为自己的公司定义一个这样的辅助类。
class company
{
public:
static const string& Name_s()
{
static string name = "company name";
return name;
}
virtual const string& Name_o()
{
return Name_s();
}
};
为了让分类工作自动化我们定义这样一个模板
template<class INTERFACE, class COMPANY >
class LocatData:public DataBase{};
现在每一个公司的每一个接口所支持的数据类型都可以有一个自己的独立的子类型分类域了。我们再也不用为飞速增长的数据类型发愁了。
错误信息和版本信息这两种数据类型应该是每一个接口都支持,对它们进行公司和接口的分类是不合适的。所以我们不对它们两个分类。同时我们也发现这两种类型的数据太常见了,也没有必要对它们进行动态子类形识别。所以我们在接口中直接宣告支持这两种数据类型。现在我们的基础结构变成了这个样子。见图2-2。
图2-2
程序员可以派生自己的ErrorInfo类,以便其它使用者能根据ErrorInfo的更具体的类型信息来判定出错的地方。同样我们也可以为这样的错误类型定义一个模板如下。
template<class INTERFACE, class DATA, class COMPANY>
class LocatError:public ErrorInfo
{
//………
};
定义一个嵌套类来做处理错误也是一个不错的选择。只要把这个错误类型的名称固定下来,就可以被使用者统一的处理了。它不是被嵌套在内部嘛我们就把它叫做InsideError好了。有了LocateError和InsideError,现在我们的错误类型不仅可以依理分类,而且可以依理扩张,而且可以顺理成章的扩张。
如你所知,赋值运算和拷贝构造总会给我们的程序带来一些很不必要的麻烦。我们来把它们禁用了。现在我们的基础结构看上去应该是这个样子,见图2-3。
图2-3
在众多的数据类型中有这样一些特殊的数据类型,它们在整个程序运行期间会进行非常频繁的设置或获取操作。如果对这样的数据每一次的操作都按照我们前面的操作进行,这会造成巨大的浪费。所以我们有必要对这样的数据做一些特殊处理。我们可以让这样的数据被指定到一个特定的数据对象上,以后不论是接口的使用端还是接口的提供端,都会把自己对这个绑定的数据的修改立即反映给对方。所以我们要对接口加入一个数据绑定的方法,和一个解除绑定的方法。现在我们接口看上去应该象这个样子。见图2-4。
图2-4
现在还有一个问题,如果公司CompanyA的接口InterfaceA数据DataA传给公司CompanyB的InterfaceB,那么在现在这种模式下,这样的传递是合法的,但这样的传递是没有意义的。怎样避免出现这种情况呢?这里有一个方案。那就是做一个接口匹配器,让不是这个接口的数据类型不能传递给这个接口进行处理。我们做一个这样的匹配器。
template <class INTERFACE, class COMPANY>
class IMatch
{
public:
IMatch():
_myIp(0)
{
//创建接口
}
~IMatch()
{
//消毁接口
}
typedef IMatch<INTERFACE, COMPANY> _myType;
typedef LocatData<INTERFACE, COMPANY> _myDataType;
_myType& operator << (_myDataType& data)
{
if(_myIp)
_myIp->Set(data);
return *this;
}
_myType& operator >> (_myDataType& data)
{
if(_myIp)
_myIp->Get(data);
return *this;
}
_myType& Bind(_myDataType& data)
{
if(_myIp)
_myIp->Bind(data);
return *this;
}
_myType& DeBind(_myDataType& data)
{
if(_myIp)
_myIp->DeBind(data);
return *this;
}
private:
INTERFACE * _myIp;
};
现在我们不用为非法的数据传递担心了。 如果有不匹配的数据传到某个接口匹配器,编译器会站出来说:“没有一个方法接受这种数据类型”。当然你也可以发挥你的想象力来特化或偏特化这个模板,以现实更为强悍的功能。(注意返回类型_myType)
我们有一个接口匹配器,可以让我们不用为数据匹配的问题操心了。可是我们的接口匹配器却把一个bool型的返回给私呑了。这导致我们不能判断每一次数据交换的成功与失败的状态了。为此我们要修改接口匹配器,不让它私呑bool型的返回。最快捷的办法是把接口匹配器的_myType的返回改为bool,但是这样我们就失去了对接口匹配器进行链式调用的功能。所以我们的修改就不能简单地把_myType的返回改为bool。而是这样,我们以Bind为例来说明这种修改。
_myType& DeBind(_myDataType& data)
{
if(!_myIp)
throw ErrorInfo(0,"接口为空");
if(! _myIp->Bind(data))
{
throw _myIp->GetError();
}
return *this;
}
这样,我们在进行链式调用时,如果有不成功时候,就会有一个异常被抛出。可是我们得承认有些程序员非常地不喜欢异常。好的,我们得有一办法来应对这些程序员的爱好。让这样的程序员直接使用接口是一个好办法。所以我们修改接口匹配器,添加一个函数,让这个函数返回接口指针,如下。
INTERFACE* GetInterface()
{
return _myIp;
}
重载一个指针运算符会更方便,如下。
INTERFACE* operator ->()
{
return _myIp;
}
好了,现在不喜欢异常和程序员也可以以他们喜欢的方式来使用接口匹配器了。但还有一个问题就是接口匹配器的使用者不能方便地判定接口匹配器是否可用。所以我们再给接口匹配器加上一个转型运算,来分辩它的是否可用,如下。
operator bool()
{
return _myIp?true:false;
}
不要忘记你可以特化或偏特化这个转型运算。
现在我们来解决仿基因编程中的最后一个问题:接口排它性。解决接口的排它性问题,使用动态装载是一个好办法。为了能够实现统一的动态装载过程,我们要在建立接口实例的地方做一些统一的规定。我们规定建立接口实例的函数有这样的命名规则。
InterfaceBase* Create_接口名();其中的接口名要根据实际的接口换为对应的接口的名称。比如要声明接口ICommand的建立函数,就是这样的InterfaceBase* Create_ICommand()。接下来我们改造接口匹配器,让它把建立接口的实例的过程封装在构造函数中。现在我们的接口匹配器的构造函数看起来像这样。
IMatch(string url = "", string IName =""):
_myIp(0)
{
_myIp = dynamic_cast<INTERFACE*>(CreateInterface(url,IName));
}
函数CreateInterface大体上是这样一个流程,见图2-5。
图2-5
我们在前面说子类型识别的时候,讲到我们的子类型识别所用的技术是dynamic_cast。事实上这种转型有一些很不好的副作用。如强耦合,和低效。我们可以采用其它的一些方式。这里有两种方式可供我们选择。一种是在子类型中加一个识别标志,一种是让子类型抛出一个是它自身的异常。这三种方法各有利弊。我们把这三种方法都加入到我们的模型中让使用者自己决定采用哪一种方法是一个好主意。
命名冲突是一个讨厌的问题,我们可不想有一天突然因为一个名字冲突倒致我们得到处修改我们的代码。所以我们为仿基因编程定一个名字空间。就叫gwl吧。Global world language吗?你自由想象去好了。
图片不太清楚, 请访问QQ707351736的相册,看清楚的图图片。http://707351736.qzone.qq.com
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1485705