接口到组件模式和 DynaMix





5.00/5 (6投票s)
面向对象环境中对多态的新看法
引言
现代语言设计的演进很大程度上是通过更好的泛型和元编程特性来改进静态多态。像Java、C#,尤其是C++这样流行的语言,在过去十年左右的时间里在这方面经历了一场复兴。另一方面,像D这样拥有良好元编程支持的语言也重新获得了关注和 popularity。像Nim这样的新语言,其开发的核心就是元编程。而动态多态则被置于次要地位。对于C++来说,C++11标准增加了std::function
和std::bind
,但语言本身几乎没有增加什么来支持面向对象语境下的动态多态(final
和override
是其中微小的例外)。
面向对象编程在其正式定义中并不包含动态多态,但在实践中,它已经隐含了这一概念。在许多语境下(例如Java),反之亦然。鉴于OOP多年来获得的负面宣传,它在C++这样的面向最大性能和类型安全性的语言中被边缘化也就不足为奇了。事实上,许多C++程序员已经忘记,或者刻意选择忘记C++本身也是一种面向对象语言。
当今的面向对象编程
虽然对OOP的一些批评集中在性能上,但大多数批评都集中在某些具体实现(例如Java的实现)在完成某些复杂业务需求方面的不足。作者认为,这不是OOP整体的问题。带有动态多态的OOP通常是表达业务需求的绝佳方式。像Python、Ruby、JavaScript等高度动态的语言在以业务逻辑为主的领域蓬勃发展。许多软件采用某种高性能语言(如C或C++)作为核心,而业务逻辑模块则采用动态且更灵活的语言(例如,lua是游戏领域一个特别受欢迎的选择)来实现,这并不奇怪。
结合语言
除了对更好的OOP特性的支持,这种方法还有其他好处,例如能够“热插拔”正在运行的程序代码,并在某些情况下,利用它们强大的DSL创建机制,将部分代码委托给非程序员。然而,也有一些缺点
性能不可避免地会变差。即使有JIT,除了极少数极端情况,解释型代码也比编译和优化的C++慢。使用JIT,通常会慢2-5倍,但在其他JIT不友好的极端情况下,慢十倍甚至更多也并非不可能。没有JIT,某些语言的性能会更差。Ruby程序比其C++对应程序慢数百倍是很常见的情况。
核心与业务逻辑语言之间需要一个绑定层。这是一段代码(通常相当大),其唯一目的是充当语言桥梁。它增加了项目的复杂性,并且需要投入大量时间来开发和维护。
存在功能重复。即使出发点很好,通过绑定层调用小型实用函数通常非常不切实际。因此,许多此类函数在核心语言和业务逻辑语言中都有实现。成千上万行的重复功能在这样的项目中很常见,这往往是重复性错误的根源。
很明显,如果上述缺点对一个项目来说是不可接受的,那么就需要一种新的方法来在高性能语言中更好地支持OOP。
C++的发展
尽管高性能语言的库开发者在很大程度上忽略了OOP功能,但仍有一些改进的努力。对于C++来说,最近流行的最值得注意的发展是多态类型擦除包装器。其中包括有些古老的Boost.TypeErasure,以及更现代的Dyno,以及Facebook的Folly.Poly。它们在标准的C++ OOP多态基础上进行了重大改进。它们实现了更好的接口和实现分离。它们是非侵入性的(无需继承)。它们更具可扩展性,因为您可以分别定义接口和类。在某些情况下,它们可能更快,但无论如何,它们的速度不会比虚函数慢。
然而……它们在架构上或多或少是相同的。仍然存在接口类型和实现类型。它们在C++中提供了OOP多态的巨大改进,但在软件设计方面,它们与Java或C#相比并没有好太多。它们不足以让人放弃脚本语言。
一个激励性的例子
动态语言中最流行的OOP技术之一是在运行时组合和修改对象。Ruby提供了一种非常简洁易读的方法来实现这一点,所以考虑这段代码——这是一段虚构游戏的玩法代码(在游戏开发术语中,玩法意味着业务逻辑)。
module FlyingCreature
def move_to(target)
puts "#{self.name} flying to #{target.name}"
end
def can_move_to?(target)
true # flying creatures can move anywhere
end
end
module WalkingCreature
def move_to(target)
puts "#{self.name} walking to #{target.name}"
end
def can_move_to?(target)
# walking creatures cannot walk over obstacles
!self.world.has_obstacles_between?(self.position, target.position)
end
end
# composing objects
hero = GameObject.new
hero.extend(WalkingCreature)
hero.extend(KeyboardControl) # controlled by keyboard
objects << hero # add to objects
dragon = GameObject.new
dragon.extend(FlyingCreature)
dragon.extend(EnemyAI) # controlled by enemy AI
objects << dragon # add to objects
main_loop_iteration # possibly the hero can't move
# give wings to hero
hero.extend(FlyingCreature) # overrides WalkingCreature's methods
main_loop_iteration # all fly
# mind control dragon
dragon.extend(FriendAI) # overrides EnemyAI's methods
main_loop_iteration # dragon is a friend
这就是Ruby的mixin。请注意,在C++圈子里,mixin这个术语存在,并且用于类似的东西。这是一种通过构建块组合对象的方式,但在编译时通过CRTP实现。现在,使用组件接口模式,可以在C++和任何其他至少支持Java类OOP的语言中实现类似的功能。
组件接口
该模式基于组合而非继承(就像几乎所有针对OOP特定问题的解决方案一样)。这是使用组件接口模式实现的相同玩法的C++注释示例。
class Component // base to all components
{
public:
virtual ~Component() {}
protected:
friend class GameObject;
GameObject* const self = nullptr; // pointer to owning object
};
// component interface for movement
class Movement : public Component
{
public:
virtual void moveTo(const Point& t) = 0;
virtual bool canMoveTo(const Point& t) const = 0;
};
// component interface for Control
class Control : public Component
{
public:
virtual const Point& decideTarget() const = 0;
};
// Main object
class GameObject
{
// component data
std::unique_ptr<Movement> _movement;
std::unique_ptr<Control> _control;
// ... other components
void addComponent(Component& c) {
const_cast<GameObject*>(c.self) = this;
}
public:
void setMovement(Movement* m) {
addComponent(*m);
_movement.reset(m);
}
Movement* getMovement() {
return _movement.get();
}
void setControl(Control* c) {
addComponent(*c);
_control.reset(c);
}
Control* getControl() {
return _control.get();
}
// ...
// GameObject-specific data
const Point& position() const;
const World& world() const;
const std::string& name() const;
// ...
};
// component implementations
class WalkingCreature : public Movement
{
public:
virtual void moveTo(const Point& t) override {
cout << self->name() << " walking to " << t << "\n";
}
virtual bool canMoveTo(const Point& t) const override {
return !self->world().hasObstaclesBetween(self->position(), t);
}
};
class FlyingCreature : public Movement
{
virtual void moveTo(const Point& t) override {
cout << self->name() << " flying to " << t << "\n";
}
virtual bool canMoveTo(const Point& t) const override {
return true;
}
};
// composing objects
auto hero = new GameObject;
hero->setMovement(new WalkingCreature);
hero->setControl(new KeyboardControl);
objects.emplace_back(hero);
auto dragon = new GameObject;
dragon->setMovement(new FlyingCreature);
dragon->setControl(new EnemyAI);
objects.emplace_back(dragon);
mainLoopIteration(); // possibly the hero can't move
// give hero wings
hero->setMovement(new FlyingCreature); // overriding WalkingCreature
mainLoopIteration(); // all characters fly
// mind-control dragon
dragon->setControl(new FriendAI); // overriding EnemyAI
mainLoopIteration(); // the dragon is a friend now
组件接口模式被广泛用于具有复杂业务逻辑的软件中,例如CAD系统、一些企业软件和游戏。它在移动游戏中尤其受欢迎,因为它们的目标硬件不如PC强大,这使得开发人员不太可能为了额外的动态语言而牺牲性能。它可以(并且经常)与实体-组件-系统模式结合使用,这样一些组件会在其自己的系统中得到适当的更新,而其他组件则充当对象特定功能的“多态实现者”。这是一个易于理解且相对容易实现和根据特定需求修改的模式。例如,要实现多播支持,只需为给定接口创建一个组件向量即可。不幸的是,组件接口模式也有其自身的缺点。
对象是耦合的焦点。每个组件接口都需要在内部声明(或者更糟的是,像上面示例中那样用天真的实现包含进来)。在C++中,对组件和对象结构的频繁更改会改变对象类,并触发项目中整个业务逻辑系统的重新编译。一旦我们有了模块,这一点将得到缓解,但它们还没有实现。
但最重要的是,接口是有限制的。想象一下上面Ruby示例的以下扩展。
module AfraidOfSnow
def can_move_to?(target)
self.world.terrain_at(target) != Terrain::Snow
end
end
dragon.extend(AfraidOfSnow)
main_loop_iteration # dragon won't fly to snow
我们添加了一个mixin,它覆盖了移动接口的一个方法。用组件接口模式几乎没有简单的办法来实现这一点。我们可以继承自飞行生物,但不仅仅是飞行生物才怕雪。这种覆盖适用于所有类型的移动。我们可以尝试解决这个问题,使用前面提到的CRTP mixin,但这会将大量代码放入模板类中,导致可怕的编译时间,即使我们通过显式实例化解决了这个问题,我们仍然需要知道我们覆盖了什么。唯一的解决方案是将接口分成“移动方法”和“移动可用性”……直到我们最终留下一个包含单方法接口的海量代码库,并负担着在不同场景下知道添加或移除哪个接口的问题。
DynaMix
DynaMix是一个C++库,它解决了这些问题。它的名字意味着动态mixin,因为它对于动态多态的作用就像CRTP mixin对于静态多态的作用一样。它允许用户在运行时组合和修改“实时”对象,并提供了大量在项目开发中可能需要的附加功能。它是在2007年为一个PC MMORPG项目构思和开发的专有库,并于2013年进行了重写并开源。自那时以来,已被不同团队和公司在多个移动游戏中广泛使用。
这是使用DynaMix实现的相同玩法的注释示例。
// declare messages
// DynaMix doesn't use class-interfaces. Instead the interface is provided
// through messages, which are delcared with macros like this.
// A message is a standalone function which some mixins may implement through
// methods
DYNAMIX_MESSAGE_1(void, moveTo, const Point&, target);
DYNAMIX_CONST_MESSAGE_1(bool, canMoveTo, const Point&, target);
// define some mixin classes
class WalkingCreature
{
public:
void moveTo(const Point& t) {
// `dm_this` is a pointer to the owning object much like `self` was in
// our previous examples.
// Note that due to the fact that C++ doesn't have unified call
// syntax, we cannot write code like dm_this->name(). Instead messages
// are functions where the first argument is the object.
cout << name(dm_this) << " walking to " << t << "\n";
}
bool canMoveTo(const Point& t) const {
return !world(dm_this).hasObstaclesBetween(position(dm_this), t);
}
};
class FlyingCreature
{
public:
void moveTo(const Point& t) {
cout << name(dm_this) << " flying to " << t << "\n";
}
bool canMoveTo(const Point& t) const {
return true;
}
};
// define mixins
// The mixin definition macros "tell" the library what mixins there are
// and what messages they implement
DYNAMIX_DEFINE_MIXIN(WalkingCreature, moveTo_msg & canMoveTo_msg);
DYNAMIX_DEFINE_MIXIN(FlyingCreature, moveTo_msg & canMoveTo_msg);
// compose objects
auto hero = new dynamix::object;
dynamix::mutate(hero)
.add<WalkingCreature>()
.add<KeyboardControl>();
objects.emplace_back(hero);
auto dragon = new dynamix::object;
dynamix::mutate(dragon)
.add<FlyingCreature>()
.add<EnemyAI>();
objects.emplace_back(dragon);
mainLoopIteration(); // possibly the hero can't move
// Replace WalkingCreature with FlyingCreature
dynamix::mutate(hero)
.remove<WalkingCreature>()
.add<FlyingCreature>();
mainLoopIteration(); // all objects fly
// Replace EnemyAI with FriendAI
dynamix::mutate(dragon)
.remove<EnemyAI>()
.add<FriendAI>();
mainLoopIteration(); // the dragon is friendly
现在,这似乎比我们为组件接口示例创建的实现要差。即,用户似乎需要知道对象中已经存在哪个mixin才能更改已有的功能。这只是这个简单示例的情况。让我们继续讨论AfraidOfSnow
功能。
class AfraidOfSnow
{
public:
bool canMoveTo(const Point& t) const {
return world(dm_this).terrainAt(t) != Terrain::Snow;
}
};
// Here we define the mixin and set a priority to the message.
// This tells the library that when this mixin is added to an object which
// already implements the message with a lower priority (the default being 0)
// this implementation must override the existing one.
DYNAMIX_DEFINE_MIXIN(AfraidOfSnow, priority(1, canMoveTo_msg));
// overriding FlyingCreature::canMoveTo
dynamix::mutate(dragon)
.add<AfraidOfSnow>();
mainLoopIteration(); // the dragon cannot fly to snow
// restoring previous functionality
// A feature which was not available in the Interface to Component
// implementation and even not possible with Ruby's mixins
dynamix::mutate(dragon)
.remove<AfraidOfSnow>();
mainLoopIteration(); // the dragon can fly freely again
DynaMix是一个免费开源库,遵循MIT许可证。它的源代码可以在这里找到,文档可以在这里找到。
历史
- 2018年2月13日 - 文章初稿