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

Mof*_*les 9 c# inheritance composition

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

我的代码,然后我会解释我认为是错的.

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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()) { }
}
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

谢谢!

Adr*_*tti 7

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

sealed class Enemy : IMoveable, ICollidable {
    public void Move() { }
    public void Collide() { }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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 {
}
Run Code Online (Sandbox Code Playgroud)

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

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

    private readonly IMoveable _moveable = new Moveable();
}
Run Code Online (Sandbox Code Playgroud)

通过这种方式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;
}
Run Code Online (Sandbox Code Playgroud)

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

sealed class Ghost : PhysicalObject {
    protected override CreateCollidableBehavior()
        => new ColliderWithProbability(0.1);
}
Run Code Online (Sandbox Code Playgroud)

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

现在事情开始变得更有趣了.你可能有一个多个部分组成的组合物体(例如一个由它的身体和翅膀制成的飞机,可能会有一种不同的武器抗性).简化示例:

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

    public List<ICollidable> Parts { get } = new List<ICollidable>()
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,列表实现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>()
}
Run Code Online (Sandbox Code Playgroud)

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


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

我认为(但这只是我的意见)你不应该在没有真正需要的用例的情况下"学习构图".组合和继承只是你选择建模你的领域的工具(比如模式 BTW)你可能需要先了解何时在"正确"的情况下使用它们,否则你将(ab)将来使用它们因为它们的效果而不是他们的意思.

在你的例子中......为什么它不是最理想的?因为您尝试在基类中添加一些行为(移动,碰撞等),因为它们可能不适用,而您不会重复使用任何代码.您正在创建一个可能会或可能不会实现的行为组合(这就是您null在调用这些方法之前检查的原因,BTW可能会简化为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);
}
Run Code Online (Sandbox Code Playgroud)

一般而言,组合和继承不会相互排斥,并且它们在一起使用时发挥最佳效果.

  • 我认为(但这只是我的意见)你不应该在没有真正需要的用例的情况下"学习构图".组合和继承只是您选择用于建模域名的工具,您可能需要首先了解何时在"正确"的情况下使用它们(或者您将(ab)将来使用它们因为它们的**效果**而不是他们的**意思**). (3认同)