组成模式

时间:2013-03-22 03:42:19

标签: c++ design-patterns inheritance encapsulation composition

如何处理组合而不是继承?考虑以下课程:

class GameObject {...};

class Sprite {
public:
    void changeImage(...);
};

class VisibleGameObject: public Sprite, public GameObject {};

class VisibleGameObject : public GameObject {
    protected:
        Sprite m_sprite;
};

第一个VisibleGameObject类使用继承。多重继承。看起来不太好。第二个是我想要使用的,但它不允许我像这样访问Sprite的API:

VisibleGameObject man;
man.changeImage();

如何在没有继承(或代码重复)的情况下实现这一目标?

编辑: 我知道我可以使用继承或使m_sprite成为公共成员,但我无法访问Sprite类,因为它是private。这就是重点,问题是关于更改VisibleGameObject的Sprite的最佳方法,遵循数据封装规则。

3 个答案:

答案 0 :(得分:3)

我认为你仍然是“继承”的思维方式。基类应该知道要合成什么。要更改图像,您应该更改精灵实例,不应该提供组合实例的界面。例如:

class GameObject {
public:
    // you can choose public access or getters and setters, it's your choice
    Sprite sprite;
    PhysicalBody body;
};

object = GameObject();
object.sprite = graphicalSystem.getSpriteFromImage("image.png");
// or if you prefer setters and getters
object.setSprite(sprite);

更一般地说,GameObject应该包含基类Component的实例(或指向实例的指针,取决于你的实现)。在这种情况下使用继承是有意义的,因为这样它们可以在一个存储中,如std :: map。例如:

class Component {
    // ...
};

class Sprite : public Component {
    //...
};

class PhysicalBody : public Component {
    //...
};

class GameObject {
protected:
    std::map<std::string, Component*> components;
    //...
public:
    Component* getComponent(const std::string& name) const;
    void setComponent(const std::string& name, Component* component);
    //...
};

对于主循环中的组件创建和渲染,请使用系统。例如,GraphicalSystem知道它创建的所有Sprite实例,并且渲染它时只渲染附加到某个GameObject实例的sprite。分离的组件可以被垃圾收集。有关位置和大小的信息可能是GameObject的一部分,也可能是“物理”组件。

理解它的最好方法是编写自己的原型或检查现有的实现(ArtemisUnity 3D和其他许多实现)。有关详细信息,请参阅Cowboy programming: Evolve Your Hierarchy或尝试查找实体/组件系统。

答案 1 :(得分:2)

首先,alternative for composition is private inheritance(而不是公开的),因为它们都模拟有一个关系。

重要的问题是,我们如何向Sprite客户公开changeImage公众成员(例如VisibleGameObject)?我介绍了我知道的4种方法:

(私人)继承

我知道您希望避免(多个)继承,但为了完整起见,我提出了一个基于私有继承的建议:

class VisibleGameObject: private Sprite, public GameObject {
...
};

在这种情况下,VisibleGameObject私下派生自Sprite。然后前者的用户无法访问后者的任何成员(就像它是私人成员一样)。特别是,Sprite的公共受保护成员和受保护成员对VisibleGameObject个客户隐藏。

如果继承是公开的,那么所有 Sprite的公共和受保护成员都会被VisibleGameObject公开给其客户。通过私有继承,我们可以更好地控制应该通过使用声明来公开哪些方法。例如,这会暴露Sprite::changeImage

class VisibleGameObject1: private Sprite, public GameObject {
public:
    using Sprite::changeImage;
...
};

转发方法

我们可以提供转发VisibleGameObject的{​​{1}}公开方法,如下所示。

m_sprite

我相信这是最好的设计,特别是就封装而言。但是,与其他替代方案相比,它可能需要大量输入。

结构解除引用运算符

即使普通的旧C提供了暴露另一个类型接口的类型,就好像它是它自己的:指针。

实际上,假设class VisibleGameObject2: public GameObject { public: void changeImage() { m_sprite.changeImage(); } private: Sprite m_sprite; ... }; 属于p类型。然后,通过使用结构解除引用运算符Sprite*,我们可以访问->的成员(由Sprite指向),如下所示。

p

C ++允许我们使用自定义结构解引用运算符赋予类(智能指针使用的功能)。我们的例子变成了:

p->changeImage();

class VisibleGameObject3 : public GameObject {
public:
    Sprite* operator ->() {
        return &m_sprite;
}
private:
    Sprite m_sprite;
...
};

虽然方便,但这种方法有很多缺陷:

  1. 至于公共继承,这种方法无法很好地控制应该公开VisibleGameObject v; v->changeImage(); 个公共成员。
  2. 它仅适用于一个成员(也就是说,您不能使用相同的技巧来公开两个成员接口)。
  3. 它与界面混淆了。实际上,例如,考虑Sprite有方法VisualGameObject。然后,要在对象doSomething()上调用此方法,应该v,而要调用v.doSomething(),则应使用changeImage()。这令人困惑。
  4. 它使v->changeImage()看起来像一个智能指针。这在语义上是错误的!
  5. C ++ 11 Wrapper Pattern

    最后,有Sutter的C ++ 11 Wrapper模式(观看他的presentation,特别是第9页的第二个slide):

    VisibleGameInterface

    客户以这种方式使用它:

    class VisibleGameObject4 : public GameObject {
    private:
        Sprite m_sprite;
    public:
        template <typename F>
        auto operator()(F f) -> decltype(f(m_sprite)) {
            return f(m_sprite);
        }
    };
    

    正如我们所看到的,与转发方法相比,这会将类写入的负担转移到类客户端。

答案 2 :(得分:0)

看起来您正试图直接访问Sprite的功能而不首先引用它。试试这个:

man.m_sprite.changeImage() ;

请注意,m_sprite和changeImage()应该是公共的,以便您执行此操作。否则使用公共访问器函数来操纵私有类成员。