【问题标题】:Strategy pattern and "action" classes explosion策略模式和“动作”类爆炸
【发布时间】:2010-04-11 21:12:00
【问题描述】:

拥有大量只做一件事的“工作”类(例如策略类)是不是很糟糕?

假设我想创建一个 Monster 类。我不只是在一个类中定义我想要的关于怪物的所有内容,而是尝试确定它的主要特征是什么,以便我可以在接口中定义它们。这将允许:

  1. 如果我愿意,请密封班级。稍后,其他用户可以通过我定义的接口创建一个新类,并且仍然具有多态性。我不必担心人们(或我自己)将来可能希望如何更改/向基类添加功能。所有类都从 Object 继承,它们通过接口实现继承,而不是从母类。
  2. 将我对这个怪物使用的策略重用于我游戏世界的其他成员。

缺点:这个模型是刚性的。有时我们想定义一些仅仅通过尝试将这些“构建块”组合在一起并不容易实现的东西。

public class AlienMonster : IWalk, IRun, ISwim, IGrowl {
    IWalkStrategy _walkStrategy;
    IRunStrategy _runStrategy;
    ISwimStrategy _swimStrategy;
    IGrowlStrategy _growlStrategy;

    public Monster() {
        _walkStrategy = new FourFootWalkStrategy();
       ...etc
    }

    public void Walk() { _walkStrategy.Walk(); }
    ...etc
}

接下来我的想法是制定一系列不同的策略,供不同的怪物使用。另一方面,它们中的一些也可以用于完全不同的目的(即,我可以有一个也“游泳”的坦克)。我看到这种方法的唯一问题是它可能导致纯“方法”类的爆炸式增长,即仅具有执行此或其他操作的目的的策略类。另一方面,这种“模块化”将允许策略的高度重用,有时甚至在完全不同的环境中也是如此。

您对此事有何看法?这是一个有效的推理吗?这是过度设计吗?

另外,假设我们对我上面给出的示例进行了适当的调整,将 IWalk 定义为:

interface IWalk {
    void Walk();
}

interface IWalk {
    IWalkStrategy WalkStrategy { get; set; } //or something that ressembles this
}

因为这样做我不需要在 Monster 本身上定义方法,我只需要 IWalkStrategy 的公共 getter(这似乎违背了你应该尽可能多地封装所有东西的想法!) 为什么?

谢谢

【问题讨论】:

    标签: c# design-patterns oop


    【解决方案1】:

    Walk、Run 和 Swim 似乎是实现而不是接口。您可以拥有一个 ILocomotion 接口并允许您的类接受 ILocomotion 实现的列表。

    Growl 可以是 IAbility 接口之类的实现。并且一个特定的怪物可能有一个 Iability 实现的集合。

    然后有几个接口是使用哪种能力或运动的逻辑:例如 IMove,IAct。

    public class AlienMonster : IMove, IAct
    {
      private List<IAbility> _abilities;
      private List<ILocomotion> _locomotions;
    
      public AlienMonster()
      {
        _abilities = new List<IAbility>() {new Growl()};
        _locomotion = new List<ILocomotion>() {new Walk(), new Run(), new Swim()}
      }
    
      public void Move()
      {
        // implementation for the IMove interface
      }
    
      public void Act()
      {
        // implementation for the IAct interface
      }
    
    }
    

    通过以这种方式编写课程,您将避免一些僵化。

    编辑:添加了有关 IMove 和 IAct 的内容

    编辑:在一些 cmets 之后

    通过将 IWalk、IRun 和 ISwim 添加到怪物中,您是在说任何可以看到的对象都应该能够调用在任何这些接口中实现的任何方法并使其有意义。此外,为了决定它应该使用三个接口中的哪一个,您必须传递整个对象。使用接口的一个巨大优势是您可以通过该接口引用它。

    void SomeFunction(IWalk alienMonster) {...}
    

    上述函数将采用任何实现 IWalk 的函数,但如果 IRun、ISwim 等存在 SomeFunction 的变体,则您必须为每个函数编写一个全新的函数,或者整个传递 AlienMonster 对象。如果您传入对象,则该函数可以调用其上的任何和所有接口。这也意味着该函数必须查询 AlienMonster 对象以查看其功能,然后决定使用哪个。所有这一切最终使外部的许多功能应该保留在类内部。因为您将所有这些都外化了,并且 IWalk、IRun、ISwim 之间没有共性,所以某些函数可能会无辜地调用所有三个接口,而您的怪物可能会同时跑-走-游泳。此外,由于您希望能够在某些类上调用 IWalk、IRun、ISwim,因此所有类基本上都必须实现所有三个接口,并且您最终会制定一个类似CannotSwim 的策略来满足 ISwim 的接口要求。不会游泳。否则,您最终可能会尝试调用未在怪物上实现的接口。最后,您实际上使额外接口的代码变得更糟,IMO。

    【讨论】:

    • 这样做的另一个好处是,当你想从 A 到 B 时,你可以迭代你的运动接口列表并询问每个成本而不是硬编码“get walk成本;获得运行成本;获得游泳成本;获得飞行成本;获得传送成本;...”并产生技术债务,即在实施新的运动类型时必须更新该列表。
    • 您能否解释一下为什么 Walk、Run 和 Swim 在您看来更像是实现而不是接口?通常,接口模型不应该是某个类“可以做”的事情吗? ISwim 意味着我的怪物(或坦克,或我尝试实施的任何课程)可以游泳。然后我可以实现几个不同的策略类(例如),例如 SwimInWater SwimInAcid 等。我在这里缺少什么?
    • 让我重新表述一下“接口并不能描述某些东西本身可以做什么。”以合乎逻辑且一致的方式使用接口是一个好主意。使用 ILocomotion 咆哮将是一个糟糕的选择。但是走路、游泳、跑步、飞行、传送等等都只是运动的形式。通过将它们抽象为移动接口,您可以大大简化代码。你可以拥有 100 个 ILocomotion 实现,并且比 20 个 IWalk、20 个 IRun、20 个 ISwim、20 个 ISkip、2 个 ITeleport、18 个 IJump 实现更好。如果某些内容不适合 ILocomotion 界面,则创建一个新界面。
    • 另外假设你有一些可以游泳和游泳的生物......在你正在做的方法中,你必须创建一个新的实施 SwimInWaterOrAcid,因为你只允许每个运动类型的一个实施一个怪物。
    • 不,不管你是否迭代一个集合。优点是您有一个定义明确的接口来完成与 N 个接口相比的某些事情。任何关心怪物的东西都必须知道所有这些接口以及怪物是否拥有它们。假设你有 20 个怪物:5 个实现 IWalk/IRun/IFly,7 个实现 IWalk/IRun,8 个实现 IWalk 所有这些接口真的有用吗?谁在调用这些接口?为什么调用这些接口的东西在乎?这些接口是否会有真正不同的功能集?
    【解决方案2】:

    在支持多重继承的语言中,您确实可以通过从代表它可以做的事情的类继承来创建您的Monster。在这些类中,您将为它编写代码,因此不必在实现类之间复制代码。

    在 C# 中,作为一种单一继承语言,我认为除了为它们创建接口之外别无他法。类之间是否有很多代码共享,那么您的IWalkStrategy 方法可以很好地减少冗余代码。在您的示例中,您可能还希望将几个相关的动作(例如步行、游泳和跑步)组合到一个类中。

    重用和模块化是好事,在我看来,只有几个方法就拥有许多接口在这种情况下并不是一个真正的问题。特别是因为您使用接口定义的操作实际上是不同的东西,可能会以不同的方式使用。例如,一个方法可能需要一个能够跳转的对象,因此它必须实现该接口。这样,您可以在编译时按类型强制此限制,而不是在运行时不符合方法预期时以其他方式引发异常。

    简而言之:我会按照您建议的方式执行此操作,在减少类之间复制的代码时使用额外的I...Strategy 方法。

    【讨论】:

      【解决方案3】:

      从维护的角度来看,过度抽象可能与死板的单体代码一样糟糕。大多数抽象都会增加复杂性,因此您必须决定增加的复杂性是否能为您带来有价值的东西。有很多非常小的工作类可能是这种过度抽象的标志。

      使用现代重构工具,在您需要的时间和地点创建额外的抽象通常不会太难,而不是预先完全构建一个宏伟的设计。在我非常抽象地开始的项目中,我发现,当我开发具体的实现时,我会发现我没有考虑过的案例,并且经常会发现自己试图扭曲实现以匹配模式,而不是去返回并重新考虑抽象。当我开始更具体时,我会提前确定(更多)极端案例,并且可以更好地确定抽象的真正意义。

      【讨论】:

        【解决方案4】:

        “找出变化并封装它。”

        如果怪物走路的方式不同,那么将这种变化封装在抽象后面。如果你需要改变怪物的行走方式,你的问题中可能有一个状态模式。如果您需要确保 Walk 和 Growl 策略一致,那么您可能有一个抽象工厂模式。

        总的来说:不,将各种概念封装到自己的类中绝对不是过度工程。制作具体类sealedfinal 也没有错。它迫使人们在从可能继承的东西继承之前有意识地打破封装。

        【讨论】:

          猜你喜欢
          • 2011-06-21
          • 2010-12-18
          • 2011-01-31
          • 1970-01-01
          • 2016-03-01
          • 2021-06-30
          • 1970-01-01
          • 1970-01-01
          • 2020-06-17
          相关资源
          最近更新 更多