战略模式和“行动”类爆炸

时间:2010-04-11 21:12:00

标签: c# design-patterns oop

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

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

  1. 如果我愿意,请密封课程。之后,其他用户可以创建一个新类,并通过我定义的接口仍然具有多态性。我不必担心将来人们(或我自己)想要如何更改/添加功能。所有类都继承自Object,它们通过接口实现继承,而不是来自母类。
  2. 将我正在使用的策略重新用于游戏世界的其他成员。
  3. Con:这个模型很僵硬。有时我们想要通过组合这个“构建块”来定义一些不容易实现的东西。

    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
    }
    

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

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

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

    interface IWalk {
        void Walk();
    }
    

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

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

    由于

4 个答案:

答案 0 :(得分:2)

在支持多重继承的语言中,您确实可以通过继承代表它可以执行的操作的类来创建Monster。在这些类中,您将为其编写代码,以便在实现类之间不必复制代码。

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

重用和模块化是好东西,在这种情况下,我认为只有少数方法的许多接口并不是真正的问题。特别是因为您使用接口定义的操作实际上是不同的东西,可能会以不同的方式使用。例如,方法可能需要一个能够跳转的对象,因此它必须实现该接口。这样,您可以在编译时按类型强制执行此限制,而不是在不满足方法期望的情况下在运行时抛出异常。

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

答案 1 :(得分:2)

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的内容

编辑:经过一些评论后

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

void SomeFunction(IWalk alienMonster) {...}

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

答案 2 :(得分:2)

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

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

答案 3 :(得分:1)

  

“查找不同的内容并对其进行封装。”

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

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