The Virtues Of Bastard

Blood is inherited and virtue is acquired.
-- Venezuelan Proverb

引子

在刚刚结束的《权力的游戏》第六季里,最让人热血沸腾的是第九集《The War of Bastards》;而在第十集,Jon Snow,这个具有公正,勇敢,富有荣誉感等多种美德的bastard,正式获得继承权,成为临冬城主,北境之王!

The Virtues Of Bastard_第1张图片
Jon Snow

这让我想起了C++语言中的一个有趣特性:私有继承

私有继承或许是C++语言特有的一种特性。所以你在各种面向对象教材中,很难看到针对这种用法的讨论。在现实项目中,也很难看到它的踪影。

它究竟有何用途?

匿名访问

我们知道继承意味着子类父类之间存在IS-A的关系。所以,子类可自动被当作父类来使用(当然要符合里氏替换原则)。

下面的代码,可以非常顺利的编译通过。

struct Base {};
struct Derived : public Base {}; 

void f(Base* base); 
// ... 

Derived derived; 

f(&derived); // 编译正常 

但如果DerivedBase之间是私有继承的关系,在Derived的外部客户看来, 它和Base之间并不存在IS-A的关系。事实上,在客户眼里,它们什么关系也没有。Base作为 Derived的一种内部实现细节,从逻辑上被彻底隐藏了。比如:

struct Derived : private Base {}; // 私有继承 

// ... 

Derived derived; 

f(&derived); // 不能编译!! 

而在类的设计者看来,DerivedBase的任何外部客户一样,可以自由访问Base的所有public成员,但对其非public成员却毫无权限。也就是说,Base相当于Derived的一个成员变量。所以它们之间的关系相当于委托

但它比委托要更方便。因为在委托方式下,Derived必须定义一个 Base 类型的成员变量,并通过变量访问Base的成员。而私有继承则无须这么做。其差别如下所示:

struct Base 
{ 
  void f(); 
};

// 委托
struct Object 
{ 
  void f1() 
  { 
    base.f(); // 需通过成员变量访问 
  } 
  
private:  
  Base base; 
}; 

// 私有继承
struct Derived : private Base 
{ 
  void f1() 
  { 
    f(); // 可直接访问
  } 
};

避免"中间人"

另外,编程实践中最让人厌烦的活动之一,是编写中间人Middle Man)式的转调代码。(关于中间人,参见Martin Fowler的《重构》)

比如,我们原来有一个类Foo,存在着多个public成员函数接口。现在,系统中另一处需要一个类BarBar只需要提供一个接口int f(),其功能需求和Foo提供的同名接口完全一致。所以,我们想通过委托关系复用Foo的实现。

经典委托的实现方式,是直接转调其接口。比如:

struct Foo 
{ 
  int f();
  int g(); 
  int h(); 
  // ... 
};
 
struct Bar 
{ 
  int f() 
  { 
    return foo.f(); 
  } 

private: 
  Foo foo; 
};

如果使用私有继承,则可以通过部分暴露的方式来简化你的工作:

struct Bar : private Foo // 私有继承 
{ 
  // 部分暴露 Foo 的接口。 
  using Foo::f; 
};

我们知道,实现继承本质上是一种扩展关系。而这种通过私有继承进行部分暴露接口的用法是一种反扩展

避免额外的派生类

公有继承一样,私有继承的子类可以实现父类的虚函数。

所以,如果简单的以委托的方式来实现组合,程序员们则不得不先通过派生类给出实现,然后再组合派生类。如下图所示:

The Virtues Of Bastard_第2张图片
组合派生类

但这样的实现方式,不仅需要定义一个新的类,更重要的是,派生类的实现方式很可能需要使用客户类的内容,而客户类并不想将这些内容公开。两个类该如何配合,就变成了设计者一个非常棘手的问题。

但通过私有继承,则可以完美的解决这中问题。如下图所示:

The Virtues Of Bastard_第3张图片
通过私有继承避免额外派生类

对遗留系统结构体的封装

在遗留系统中,会存在一些只有数据没有行为的结构体。而这样的结构体经常作为参数,在模块之间到处传递。不同模块在获取数据之后,会根据这些数据进行一系列的计算。

比如, 我们又一个名为Rectangle的数据结构:

struct Rectangle
{
   int width;
   int height;
};

对于这样的数据结构,我们当然想进行封装,以享用封装所带来好处。而如果这一切都发生在可控的单个子系统内部,毫无疑问,你应该这么做。

但如果Rectangle跨多个子系统,或者一个子系统过大,你可能就会面临下列问题:

  1. 每个子系统对于Rectangle的行为定义都是不一样的,也就是说,它们唯一共享的就是数据;
  2. 不同子系统由不同的团队维护,你无权修改它们的代码,也无权修改共享的头文件;
  3. 直接进行封装,会造成大面积的代码的修改;
  4. 其它子系统的开发语言仍然是C
  5. ...

总而言之,你不能修改原有的结构体Rectangle。 这种情况下,你依然想在正在重构的代码中对Rectangle进行封装,那该怎么
办?

私有继承,是解决这类问题的不错选择。


struct MyRectangle : private Rectangle 
{ 
  int getArea() const 
  { 
    return width * height; 
  }
   
  int getPerimeter() const 
  { 
    return 2 * (width + height); 
  } 
};

为什么是私有继承?因为我们想进行封装,让子系统内部的代码没有人可以自由的访问数据,以享用封装带来的好处。

为什么不使用委托?因为如果没有之前所说的那些约束,这些数据和行为本来就应该属于一个类。而委托关系,会造成我们访问每个数据成员时,都必须通过成员变量进行间接访问,这毫无疑问给我们带来了不必要的负担。

私有继承也意味着,在客户代码那里IS-A关系的丧失。好在我们还有强制转换的武器,我们只需要在子系统边界对其进行类型强换,在子系统内部均使用MyRectangle即可。

尽管看起来让人有些不安,由于这种继承完全没有修改任何内存布局,所以这种强转是绝对安全的。

// 本子系统边界函数
void s1_boundary(Rectangle* rect) 
{ 
  // ...  
  // 强制转换 
  s1_internal1((MyRectangle&)*rect);
  // ...
}

// 子系统内部函数,使用 MyRectangle 
void s1_internal1(MyRectangle& rect) 
{ 
  int perimeter = rect.getPerimeter(); 
  // ...  
  s1_internal2(rect);  
  // ... 
} 

// 子系统内部函数,使用 MyRectangle 
void s1_internal2(MyRectangle& rect) 
{ 
  int area = rect.getArea();  
  // ... 
  s2_boundary((Rectangle*)&rect); 
} 

// 其它子系统的边界函数,仍然使用 Rectangle 
void s2_boundary(Rectangle* rect);

这种方法,在一些以消息作为进程间通信手段的嵌入式系统中,是一种非常有效的封装手段。

私有继承也是继承

Scott Meyers在其著作《Effective C++》中,将私有继承定义为和组合一样的关系(is-implemented-in-terms- of)。

尽管他也提到私有继承的一个优势是可以实现父类的虚函数,但他没有明确的指出,私有继承也是一种继承

只是这种继承关系通过private关键字对外界隐藏了真相;但是,在类的内部, 子类和基类IS-A关系依然成立。我将这种关系称之为:私有继承的子类是父类的私生子

这就意味着,当我们想利用继承关系来完成一些特定的设计,但又不想让这种关系被外部利用时,私有继承就是绝佳的选择。

比如:在一颗二叉树上,正常情况下,每个结点都有存在一个父结点和两个子节点。但存在一些例外情况:根节点没有父节点,而叶子节点则没有子节点。

所以,我们用这样的数据结构来表现一个节点:

struct Node 
{ 
  // ... 
private:  
  Node* parent; 
  Node* leftChild, rightChild; 
}; 

在这个树上,有时候一个节点的状态变化必须通知给其所有父子节点,而其父子也会进一步将此事件向上下传播。如下:

void Node::notifyEvent() 
{ 
  notifyParent();
  notifyChildren();
}

void Node::notifyParent() 
{ 
  if(parent != 0) parent->onChildStateChange(); 
}
 
void Node::notifyChildren() 
{ 
  if(leftChild != 0)
  { 
    leftChild->onParentStateChange();
  }
  
  if(rightChild != 0) 
  {
    rightChild->onParentStateChange(); 
  }
}

可以看到,代码中存在一些空指针判断,如果这些空指针只有少数的几个,也没有什么大问题。但如果这些的事件很多,每个事件的处理手段也不一样,可能就会早就很多的空指针判断语句,这让我们的代码很不干净。

当然解决空指针问题的常用手段是空对象模式Null Object)。所以,我们将设计修改为:

struct NodeEventListener
{
  virtual void onParentStateChange() = 0; 
  virtual void onChildStateChange() = 0;
  // ... 
  virtual ~NodeEventListener(){} 
};

struct Node : NodeEventListener 
{ 
  Node(Node* parent, Node* leftChild, Node* rightChild); 
  
  void onParentStateChange(); 
  void onChildStateChange(); 
  // ... 

private:  
  NodeEventListener* parent; 
  NodeEventListener* leftChild; 
  NodeEventListener* rightChild; 
};
 
namespace 
{  
  struct NullNode : NodeEventListener 
  { 
    void onParentStateChange() {} 
    void onChildStateChange() {}
 
    // ... 
    static NullNode* getInstance() 
    { 
      static NullNode instance;
      return &instance; 
    } 
  }; 

  NodeEventListener* getListener(NodeEventListener* node) 
  { 
    return node == 0 ? NullNode::getInstance() : node; 
  } 
} 

Node::Node
  ( Node* parent
  , Node* leftChild
  , Node* rightChild) 
  : parent(getListener(parent))  
  , leftChild(getListener(leftChild))
  , rightChild(getListener(rightChild)) 
{ } 

void Node::notifyParent() 
{ 
   parent->onChildStateChange();
}

void Node::notifyChildren() 
{ 
  leftChild->onParentStateChange();
  rightChild->onParentStateChange();
}

void Node::onParentStateChange() 
{ 
  // 真正的事件处理代码  
} 

void Node::onChildStateChange() 
{ 
  // 真正的事件处理代码 
}

作为一个C++的标准空对象模式的实现,上述设计解决了空指针判断的问题。并且Node类和NodeEventListener之间从概念层面的 IS-A也是成立的。所以,我们理应为这样的结果感到欣慰。

但如果你再仔细审视一下,就会产生这样的疑问:这个IS-A的关系,需要被Node外部的用户知道吗?你希望他们可以通过NodeEventListener类型来调用NodeonParentStateChange()onChildStateChange()等方法吗?

答案是否定的,因为这是一个内部设计。

所以我们需要将这些接口隐藏起来。方法很简单:将继承关系改为 private, 并将继承自NodeEventListener的函数也设为 private 即可:

// ... 

struct Node : private NodeEventListener 
{ 
  Node(Node* parent, Node* leftChild, Node* rightChild); 
  // ... 

  // 来自于 NodeEventListener 的函数被设为私有 

private: 
  void onParentStateChange(); 
  void onChildStateChange(); 
  // ... 

private:  
  NodeEventListener* parent; 
  NodeEventListener* leftChild; 
  NodeEventListener* rightChild; 
};

尽管我们将继承关系改为了私有,但请注意,parent等成员变量仍然是NodeEventListener类型,而构造函数的参数类型却是Node类型(想想为什么?)。 从NodeNodeEventListener的自动类型转换在构造函数的实现代码里完成,这说明NodeNodeEventListener 之间的IS-A关系,在Node看来,依然是成立的。

越狱

我们现在知道,私有继承会对外屏蔽子类和父类的继承关系。子类的外部客户不能将其看作父类类型,更不可能调用子类继承自父类的函数。

因此,在下面的代码中,Derived似乎永远也无法被当作 Foo::f(Base*) 的参数,以利用Foo所提供的服务。

struct Base 
{ 
  virtual void f() = 0; 
  virtual ~Base() {} 
}; 

struct Foo 
{ 
  void f(Base* base) 
  { 
    base->f(); 
  } 
};
 
struct Derived : private Base 
{
  // ... 
private:  
  void f() {} 
}; 

Really?你不妨尝试编译一下下面的代码,然后就会惊讶的发现,竟然是通过的。

// ... 
struct Derived : private Base 
{ 
  // ... 
  void doSth(Foo* foo) 
  { 
    foo->f(this); 
  } 

private:  
  void f() {} 
};

其实不必惊讶,这是因为:这种继承关系只是对外界进行了隐藏,但其并没有消失,只是作为Derived的一个私人秘密不为外人所知罢了。

在需要的时候,Derived就可以将这种关系摆出来,让别人看在其父类的面子上为其提供服务。只要这种基于IS-A 关系的类型转换得以成功,别人才不管你是不是私生子。

这样的发现,可以在很多场合给我们的设计带来便利。比如,很多嵌入式设备都提供了Timer服务。而Timer模块要求:用户必须通过注册一个接口,以便于当指定的时间到期后,可以进行回调。比如:

struct TimerEventHandler 
{ 
  virtual void onTimeout() = 0; 
  virtual ~TimerEventHandler() {} 
};
 
struct Timer 
{ 
  static void registerHandler
    ( unsigned int timeout 
    , TimerEventHandler* handler);
}; 

然后,一个类NetworkClient需要Timer服务。但当Timer过期之后,其需要进行的操作都是自己的私有成员。如果创建一个专门的类以实现TimerEventHandler,就要么需要使用友元关系,要么就必须把相关的成员改为public。无论怎样,都会破坏NetworkClient的封装性。

这种情况下,让 NetworkClient直接继承(实现)TimerEventHandler,就是一个最恰当的选择。同样,由于我们不希望这层关系为外界所知,从而产生不必要的依赖,我们应选择私有继承

struct NetworkClient : private TimerEventHandler 
{ 
  void connect() 
  { 
    server.connect(); 
    Timer::registerHandler(30, this); 
    // ...
  } 

private:  
  // 对 TimerEventHandler 的实现 
  void onTimeout() 
  { 
    state = STATE_DISCONNECTED;
    sendAlarm(CONNECT_TIMEOUT);
  }

  void sendAlarm(AlarmType alarmType); 

private: 
   State state; 
  // ... 
};

结论

C++私有继承是一种非常有趣的关系。我们在之前几个大型C++项目里,对于上述场景下,均经常性的使用私有继承。(估计我们是C++社区里使用这个特性最多的团队:D)。

善用它,可以帮助我们在设计的便利性信息隐藏组合方面带来诸多好处。私有继承的美德可以让它成为软件设计一方之王。

你可能感兴趣的:(The Virtues Of Bastard)