C#组成 - 我不相信我完全理解如何实现这个

时间:2017-06-21 06:57:25

标签: c# inheritance composition

好的,所以我最近一直在快速学习类,继承,接口以及它们如何彼此交互。在此期间,我发现了一种普遍的声音蔑视继承和在各种论坛/博客/视频上赞成作曲。好的,冷静一些新的东西来学习。使用this wiki page上的示例,我开始尝试更好地理解它...到目前为止,我的成就似乎只是让自己更加困惑。

我的代码,然后我会解释我认为错误的内容。

MainClass.cs

class MainClass
{
    static void Main(string[] args)
    {
        var player = new Player();
        var enemy = new Enemy();
        var entity = new Entity();

        for(int i = 0; i < GameObject.GameObjects.Count; i++)
        {
            GameObject.GameObjects[i].Type();
            GameObject.GameObjects[i].Draw();
            GameObject.GameObjects[i].Move();
            GameObject.GameObjects[i].Collision();
            GameObject.GameObjects[i].Ai();
            Console.WriteLine();
        }

        Console.ReadKey();
    }
}

GameObject.cs

interface IVisible
{
    void Draw();
}

class Visible : IVisible
{
    public void Draw()
    {
        this.Print("I am visible!");
    }
}

class Invisible : IVisible
{
    public void Draw()
    {
        this.Print("I am invisible!");
    }
}

interface IVelocity
{
    void Move();
}

class Moving : IVelocity
{
    public void Move()
    {
        this.Print("I am moving!");
    }
}

class Stopped : IVelocity
{
    public void Move()
    {
        this.Print("I am stopped!");
    }
}

interface ICollidable
{
    void Collide();
}

class Solid: ICollidable
{
    public void Collide()
    {
        this.Print("I am solid!");
    }
}

class NotSolid: ICollidable
{
    public void Collide()
    {
        this.Print("I am not solid!");
    }
}

interface IAi
{
    void Ai();
}

class Aggressive : IAi
{
    public void Ai()
    {
        this.Print("I am aggressive!");
    }
}

class Passive : IAi
{
    public void Ai()
    {
        this.Print("I am passive!");
    }
}

class GameObject
{
    private readonly IVisible _vis;
    private readonly IVelocity _vel;
    private readonly ICollidable _col;
    private readonly IAi _ai;

    public static List<GameObject> GameObjects = new List<GameObject>();

    public GameObject(IVisible visible, IVelocity velocity)
    {
        _vis = visible;
        _vel = velocity;
        GameObjects.Add(this);
    }

    public GameObject(IVisible visible, IVelocity velocity, ICollidable collision)
    {
        _vis = visible;
        _vel = velocity;
        _col = collision;
        GameObjects.Add(this);
    }

    public GameObject(IVisible visible, IVelocity velocity, ICollidable collision, IAi ai)
    {
        _vis = visible;
        _vel = velocity;
        _col = collision;
        _ai = ai;
        GameObjects.Add(this);
    }

    public void Draw()
    {
        if(_vis != null)
            _vis.Draw();
    }

    public void Move()
    {
        if(_vel != null)
            _vel.Move();
    }

    public void Collision()
    {
        if(_col != null)
            _col.Collide();
    }

    internal void Ai()
    {
        if(_ai != null)
            _ai.Ai();
    }
}

class Player : GameObject
{
    public Player() : base (new Visible(), new Stopped(), new Solid()) { }
}

class Enemy : GameObject
{
    public Enemy() : base(new Visible(), new Stopped(), new Solid(), new Aggressive()) { }
}

class Entity : GameObject
{
    public Entity() : base(new Visible(), new Stopped()) { }
}

Utilities.cs (不会担心这个问题 - 我在处理这个问题时发现了扩展方法,所以也把它们扔进去了)

public static class Utilities
{
    public static void Type(this object o)
    {
        Console.WriteLine(o);
    }

    public static void Print(this object o, string s)
    {
        Console.WriteLine(s);
    }
}

好的,我对基础示例所做的更改是将它从使用3个接口改为总是使用基于类的2-4。但是我立即遇到了如何通过合成来解决这个问题。

我首先尝试为每种类型制作不同类型的GameObject(GameObjectAI(),GameObjectEntity()等),但这似乎只会导致代码重复以及与继承相关的所有类型的问题 - 我不会&#39;我知道我是否在这里正确的轨道,但创建了一个糟糕的非组合实现。虽然如果是这种情况,那么我绝对不会理解有关构图的内容,因为在没有创建这些问题的情况下我无法看到如何做到这一点。

然后我转到了当前的解决方案。虽然这看起来非常不优雅。虽然它产生了预期的输出,但这不是正确的 - 它有类似Player()或Entity()的类,它们不会使用它们必须具有!= null检查的接口停止运行时异常,因为它们显然可以调用它们的关联类。

所以,是的,有一些关于作文的东西,我觉得我现在没有得到。

谢谢!

1 个答案:

答案 0 :(得分:7)

组合物?让我们首先看看继承是否是正确的工具。这些接口是行为特征。实现多个行为的最简单方法是简单地从这些接口继承。例如(简化代码):

sealed class Enemy : IMoveable, ICollidable {
    public void Move() { }
    public void Collide() { }
}

这个(原样)的最大缺点是你会有一些代码重复(假设的NetrualFriend类需要重写Enemy的相同逻辑。 / p>

对此有一些变通办法,最简单的是基于您可能拥有共享某些特征的对象的的假设。如果您可以构建一个良好的继承层次结构,通常您不会需要组合。例如,可移动对象也有一个边界框,然后它可以检查碰撞:

abstract class PhysicalObject : IMoveable, ICollidable {
    public virtual void Move() { }
    public virtual void Collide() { }
}

abstract class SentientEntity : PhysicalObject, IAi
{
    public virtual void Ai() { }
}

sealed class Enemy : SentientEntity {
}

sealed class Player : SentientEntity {
}

sealed class Friend : SentientEntity {
}

每个派生类可能会覆盖基类中定义的默认行为。我尽可能地尝试(直到语言限制)使用继承来描述IS-A关系和组合以描述HAS-A关系。单继承将限制我们或将导致一些代码重复。但是,您可以使用我们的第一个实现并将作业委托给一个单独的对象,是时候介绍组合了:

sealed class Enemy : IMoveable, ICollidable {
    public void Move() => _moveable.Move();

    private readonly IMoveable _moveable = new Moveable();
}

通过这种方式,来自Moveable IMoveable实现的代码可以在不同的具体类之间共享和重用。

有时候这还不够。您可以结合使用这两种技术:

abstract class PhysicalObject : IMoveable, ICollidable {
    protected PhysicalObject() {
        _moveable = CreateMoveableBehavior();
        _collidable = CreateCollidableBehavior();
    }

    public void Move() => _moveable.Move();
    public void Collide() => _collidable.Collide();

    protected virtual IMoveable CreateMoveableBehavior()
        => new Moveable();

    protected virtual ICollidable CreateCollidableBehavior()
        => new Collidable();

    private readonly IMoveable _moveable;
    private readonly ICollidable _collidable;
}

通过这种方式,派生类可以提供他们自己对这些行为的专门实现。例如鬼(假设在我们的游戏中鬼是一个物理对象)可能会以1/10的概率与其他任何东西发生碰撞:

sealed class Ghost : PhysicalObject {
    protected override CreateCollidableBehavior()
        => new ColliderWithProbability(0.1);
}

如果您的游戏引擎需要处理碰撞而没有物理接触(例如两个带电粒子之间),该怎么办?只需编写一个临时行为,它可以应用于任何地方(例如处理电磁和重力)。

现在事情开始变得更有趣了。您可能有一个由多个部分组成的组成的对象(例如,由其身体和翅膀制成的飞机,可能具有,对于武器具有不同的抵抗力)。简化示例:

abstract ComposedPhysicalObject : ICollidable {
    public void Collide() {
        Parts.ForEach(part => part.Collide());
    }

    public List<ICollidable> Parts { get } = new List<ICollidable>()
}

在这种情况下,列表实现ICollidable然后它强制所有部分具有此行为。可能不是这样:

interface IGameObject { }
interface ICollidable : IGameObject { }

abstract ComposedPhysicalObject : ICollidable {
    public void Collide() {
        foreach (var part in Parts.OfType<ICollidable>())
            part.Collide();
    }

    public List<IGameObject> Parts { get } = new List<IGameObject>()
}

组合对象甚至可以覆盖其零件的默认行为或添加/扩展它(该组通常不具有与其单独元素相同的属性)。

我们可能会写1000多个更复杂或更简单的例子。总的来说,我建议不要让你的架构过于复杂,除非你真的需要它(特别是对于游戏......抽象的性能价格)。您通过测试验证了体系结构是正确的,IMO是 TDD 中最重要的部分:如果编写测试,您会发现代码比应该更复杂,那么您需要更改体系结构;如果您首先编写测试和架构,那么它将更加面向域并且易于理解。好的,这是理想的情况......我们选择的语言有时会迫使我们选择一种解决方案而不是另一种解决方案(例如在C#中我们没有多重继承......)

我认为(但这仅仅是我的观点),你不应该学习构图&#34;没有真正需要它的用例。组合和继承只是您选择模拟您的域的工具(如模式 BTW),您可能需要首先了解何时在&#34;右键&#中使用它们34;或者你将(ab)将来使用它们,因为它们的效果而不是它们的含义。

在你的例子中......为什么它不是最理想的?因为您尝试在基类中添加某些行为(移动,碰撞等)时可能不适用,并且您不会重复使用任何代码。您正在创建可能会或可能不会实现的行为组合(这是您在调用这些方法之前检查null的原因,顺便说一句,它可以简化为fieldName?.MethodName())。您在运行时解析在编译时完全已知的东西,并且(几乎)总是更喜欢编译时检查。

如果每个对象都必须实现该逻辑(我宁愿避免这种情况)以简化调用点,那么您不需要所有这些复杂性:

abstract class GameObject {
    public virtual void Move() { }
    public virtual void Collide() { }
}

sealed class Player : GameObject {
    public override void Move()
        => _collisionLogic.Move();

    private readonly CollisionLogic _collisionLogic = new CollisionLogic(this);
}

一般来说,构图和继承不会相互排斥,并且在一起使用时效果最佳。