以前在设计DirectUI界面库(该界面库现已开源, 可到 这里 下载)架构时,遇到一个接口继承相关的问题,当时没有太好的解决方案,却一直个耿耿于怀, 现在重新思考整理下。

我们的DirectUI控件层次大概如下: 

其中, 类名以 I 开头的都是接口:
IObject表示框架的基本接口, 要求实现类似COM里IUnknown的功能,
IControl表示控件的基本接口, 所有控件都从该接口继承,
IControlContainer表示容器类控件的基本接口,
IButton表示Button类的基本接口,
IPanel表示某种容器控件接口。
 
当然上面的框架是简化的情况,实际情况比上面的复杂的多, 但该图已经可以帮我们说明这里的情况。

在真正实现Panel和Button时,我们会发现大量的代码是重复和可以共用的,因此在实际实现时, 我们的框架可能会变成这样:

也就是说我们会出现接口和实现交叉继承的情况,实际上我自己在实现时就是用这种方法的, 我想大部分人都会用这种方法(实际上WPF也是用这种方法的)。
这种方法的缺点是显而易见的, 接口中包含了实现,基本上让接口失去了它应有的作用, 这在组件式编程中是致命的,比如本来在C++中我可以封装成DLL,然后以类似COM的方式暴露接口给外部, 现在用这种方式却没法做到了(只能用导出类的方式)。

那么我们怎样才能既基于接口编程, 又能在实现时实现代码重用呢? 这个东西实际上是个语法糖, 即如何既符合C++语法又能实现我们这个需求。

于是,我们想到了如下的实现方式: 

我们的这种实现方式基于C++模板, 总的来说就是把我们要实现的接口通过模板参数传到继承类体系的最底层, 该方式的代码大概如下:
class IObject
{
};

class IControl: public IObject
{
};

class IButton: public IControl 
{
};

template<typename TBase>
class CObjectImpl: public TBase
{
};

template<typename TBase>
class CControlImpl: public TBase
{
};

template<typename T, typename TBase>
class CButtonImpl: public TBase
{
};

class CButton: public CButtonImpl<CButton, IButton>
{
};

该方式基本上完全满足我们上面的需求,既实现了代码重用,又是基于接口编程,但是你有没有发现它有一个致命的缺点, 这个缺点就是C++模板导致的代码膨胀, 我们在 C++模板会使代码膨胀吗 对模板导致的代码膨胀有相关分析。也就是说我们上面的设计会导致每种控件继承类都有一份重复的代码, 即CControlImpl<IButton>和CControlImpl<IPanel>因为是不同的类实例, 因此它们会生成2分代码。你可能会觉得这个不算什么, 但是想想控件的继承类可能有好几十甚至上百,最终的可执行文件会被撑大不少。

那么有没有其他的方法来实现呢?  既能基于接口编程, 又能实现代码重用,还没有代码膨胀的问题。

于是,我们想到了下面这种实现方式:




这种方式是最原始的方式, 实际上就是把接口体系单独独立出来, 把实现体系也单独独立出来,  然后在最终类(Button和Panel)里继承组合起来。 当然这种方式也有缺点, 就是我们要多做些工作,因为我们要在最终类(Button)里实现接口(IButton), 在实现时我们要把所有接口需要实现的方法转发给实现类(CButtonImpl)。

最后,总结下上面三种方法:
第一种实现和接口混合继承的方法最简单,也最容易理解, 缺点是没法完全基于接口编程; 第二种基于模板的方法比较难理解,实现上也比较简单, 缺点是代码膨胀; 第三种多重继承的方法也比较容易理解, 缺点是我们要多做一些工作。

我暂时就想到这些方法, 不知道其他朋友对上面的问题一般是怎么解决的, 有什么好的解决方法?

下面这种方案是评论中的朋友提到而新增的: 

上面这种方案相对于我们最后一种解决方案,可能更通用,缺点是继承体系比较复杂, 会出现菱形继承,只能用虚拟继承。

注: 再深入一点,我们会发现上面的接口和实现其实是接口和抽象类的差别,IObject和IControl是接口, CObjectImpl和CControlImpl是抽象类, 接口强调暴露给外部的行为, 而抽象类是没法直接实例化的抽象实现。


看到云风也遇到过类似的问题: C++ 中的接口继承与实现继承
posted on 2013-02-08 19:40 Richard Wei 阅读(2500) 评论(5)  编辑 收藏 引用 所属分类: 架构体系

FeedBack:
# re: 接口继承中一个常见问题的思考[未登录]
2013-02-20 17:40 | korall
我也遇见过类似的情况,为了重用接口的实现,后来我将接口设计成分离的、互不依赖的形式。

不过像这样的设计好像更普遍:
struct IBase
{
};

struct IA : virtual public IBase
{
};

struct IB : virtual public IA
{
};

class IBaseImpl : virtual public IBase
{
};

class IAImpl : public IBaseImpl,virtual public IA
{
};

class IBImpl : public IAImpl,virtual public IB
{
};

class C : public IBImpl
{};
  回复  更多评论
  
# re: 接口继承中一个常见问题的思考
2013-02-20 20:57 | Richard Wei
@korall
不错 ,这种可能更通用, 虽然我一直没用。
已在文章后面加上你提的方案。:)  回复  更多评论
  
# re: 接口继承中一个常见问题的思考[未登录]
2013-02-27 18:20 | 路人甲
对外暴露的接口如何让外部访问属性...
当一个接口的属性比较多的时候,比如有10个属性,是不是得提供20个方法...
int GetValue1();
void SetValue1(int nValue):

...

int GetValue10();
void SetValue10(int nValue):  回复  更多评论
  
# re: 接口继承中一个常见问题的思考
2013-02-27 19:01 | Richard Wei
@路人甲
是的, C#语言本身支持property, C++中只能通过Get,Set函数了, COM也是这么做的。  回复  更多评论
  
# re: 接口继承中一个常见问题的思考
2013-03-12 13:27 | 堕花月
我原来设计过一个DUI,就是一切控件皆容器,按钮如果也是容器的话能做很多事情  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理