有关具有许多不同子类的类型切片的设计问题

时间:2020-03-05 18:54:56  来源:igfitidea点击:

我经常遇到的一个基本问题,但是找到一个干净的解决方案是,我们要在其中编写行为代码,以便在公共基类或者接口的不同对象之间进行交互。为了更具体一点,我举一个例子。

鲍勃一直在编写支持"酷地理效应"的策略游戏。这些因素汇总到简单的约束条件中,例如,如果部队在水中行走,则速度会降低25%。如果他们在草地上行走,则速度降低5%,如果在人行道上行走,则速度降低0%。

现在,管理层告诉鲍勃,他们需要新的部队。会有吉普车,小船和气垫船。另外,他们希望吉普车进入水中后受到损害,并且气垫船会忽略这三种地形。有传言说,他们可能会增加另一种地形类型,其功能要比放慢单位速度和造成伤害要多。

一个非常粗糙的伪代码示例如下:

public interface ITerrain
{
    void AffectUnit(IUnit unit);
}

public class Water : ITerrain
{
    public void AffectUnit(IUnit unit)
    {
        if (unit is HoverCraft)
        {
            // Don't affect it anyhow
        }
        if (unit is FootSoldier)
        {
            unit.SpeedMultiplier = 0.75f;
        }
        if (unit is Jeep)
        {
            unit.SpeedMultiplier = 0.70f;
            unit.Health -= 5.0f;
        }
        if (unit is Boat)
        {
            // Don't affect it anyhow
        }
        /*
         * List grows larger each day...
         */
    }
}
public class Grass : ITerrain
{
    public void AffectUnit(IUnit unit)
    {
        if (unit is HoverCraft)
        {
            // Don't affect it anyhow
        }
        if (unit is FootSoldier)
        {
            unit.SpeedMultiplier = 0.95f;
        }
        if (unit is Jeep)
        {
            unit.SpeedMultiplier = 0.85f;
        }
        if (unit is Boat)
        {
            unit.SpeedMultiplier = 0.0f;
            unit.Health = 0.0f;
            Boat boat = unit as Boat;
            boat.DamagePropeller();
            // Perhaps throw in an explosion aswell?
        }
        /*
         * List grows larger each day...
         */
    }
}

如我们所见,如果Bob从一开始就拥有可靠的设计文档,情况将会更好。随着单位数量和地形类型的增加,代码的复杂性也随之增加。鲍勃不仅要担心弄清楚可能需要将哪些成员添加到单元界面,而且还必须重复很多代码。新的地形类型很可能需要从基本IUnit接口中获取的其他信息。

每次我们向游戏中添加另一个单位时,必须更新每个地形以处理新单位。显然,这会导致很多重复,更不用说丑陋的运行时检查了,它确定了要处理的单元的类型。在此示例中,我选择了对特定子类型的调用,但是必须进行此类调用。一个例子是,当船撞到陆地时,其螺旋桨应受到损坏。并非所有单位都有螺旋桨。

我不确定这种问题叫什么,但是这是多对多的依赖,我很难解耦。我不希望ITerrain上的每个IUnit子类都有100个重载,因为我想通过耦合来清理。

对此问题的任何追捧都备受追捧。也许我正在一起思考脱离轨道吗?

解决方案

回答

旧主意:

Make a class iTerrain and another
  class iUnit which accepts an argument
  which is the terrain type including a
  method for affecting each unit type
  
  example:

  boat = new
iUnit("watercraft") field = new
iTerrain("grass")
field.effects(boat)

好吧,忘记所有我有更好的主意:

使每个地形的效果成为每个单元的属性

例子:

public class hovercraft : unit {
    #You make a base class for defaults and redefine as necessary
    speed_multiplier.water = 1
}

public class boat : unit {
    speed_multiplier.land = 0
}

回答

地形具有地形属性

地形属性是多维的。

单位有一个推进力。

推进与地形属性兼容。

单位以地形为动力,以地形来访。
那被委托给推进。

参观过程中,单位可能会受到地形的影响。

单位代码对推进一无所知。
地形类型可以更改,而不能更改除"地形属性"和"推进"之外的任何内容。
Propuslion的建设者保护现有单位免受新的旅行方式的影响。

回答

我们在这里遇到的限制是,与某些其他OOP语言不同,C#缺少多次调度。

换句话说,给定这些基类:

public class Base
{
    public virtual void Go() { Console.WriteLine("in Base"); }
}

public class Derived : Base
{
    public virtual void Go() { Console.WriteLine("in Derived"); }
}

该功能:

public void Test()
{
    Base obj = new Derived();
    obj.Go();
}

即使引用" obj"的类型为Base,也将正确输出" in Derived"。这是因为C在运行时会正确找到最派生的Go()进行调用。

但是,由于Cis是单一的调度语言,因此它仅对OOP语言中隐含的" this"的" first parameter"执行此操作。以下代码无法像上面那样工作:

public class TestClass
{
    public void Go(Base b)
    {
        Console.WriteLine("Base arg");
    }

    public void Go(Derived d)
    {
        Console.WriteLine("Derived arg");
    }

    public void Test()
    {
        Base obj = new Derived();
        Go(obj);
    }
}

这将输出" Base arg",因为除" this"之外,所有其他参数都是静态调度的,这意味着它们在编译时已绑定到被调用的方法。在编译时,编译器唯一了解的是传递的参数的声明类型(" Base obj"),而不是其实际类型,因此方法调用绑定到Go(Base b)。

那么,解决问题的方法是,基本上是手动编写一个小的方法分派器:

public class Dispatcher
{
    public void Dispatch(IUnit unit, ITerrain terrain)
    {
        Type unitType = unit.GetType();
        Type terrainType = terrain.GetType();

        // go through the list and find the action that corresponds to the
        // most-derived IUnit and ITerrain types that are in the ancestor
        // chain for unitType and terrainType.
        Action<IUnit, ITerrain> action = /* left as exercise for reader ;) */

        action(unit, terrain);
    }

    // add functions to this
    public List<Action<IUnit, ITerrain>> Actions = new List<Action<IUnit, ITerrain>>();
}

我们可以使用反射来检查传入的每个动作的通用参数,然后选择与给定的单位和地形相匹配的最派生的参数,然后调用该函数。添加到动作的功能可以在任何地方,甚至可以分布在多个程序集中。

有趣的是,我已经遇到过几次这个问题,但是从来没有超出游戏的范围。

回答

将交互规则与Unit和Terrain类分离;互动规则比这更笼统。例如,可以使用哈希表,其中的键是一对交互类型,而值是对这些类型的对象进行操作的"有效"方法。

当两个对象必须交互时,在哈希表中找到所有交互规则并执行它们

这消除了类间的依赖,更不用说原始示例中的丑陋的switch语句了。

如果性能成为问题,并且交互规则在执行过程中没有更改,请在遇到类型对时对它们的规则集进行缓存,并发出新的MSIL方法以一次运行它们

回答

这里肯定有三个对象在玩:

1) Terrain

  2) Terrain Effects

  3) Units

我不建议创建以地形/单位对作为查找动作的关键的地图。随着单位和地形列表的增加,这将使我们难以确保已涵盖所有组合。

实际上,似乎每个地形单元组合都具有独特的地形效果,因此,我们是否会从完全具有相同地形效果的列表中看到好处,这是令人怀疑的。

取而代之的是,我会让每个单元维护自己的地形图到地形效果图。然后,地形可以仅调用Unit-> AffectUnit(myTerrainType),并且该单元可以查找地形将对自身产生的影响。