Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strategy pattern and "action" classes explosion

Is it bad policy to have lots of "work" classes(such as Strategy classes), that only do one thing?

Let's assume I want to make a Monster class. Instead of just defining everything I want about the monster in one class, I will try to identify what are its main features, so I can define them in interfaces. That will allow to:

  1. Seal the class if I want. Later, other users can just create a new class and still have polymorphism by means of the interfaces I've defined. I don't have to worry how people (or myself) might want to change/add features to the base class in the future. All classes inherit from Object and they implement inheritance through interfaces, not from mother classes.
  2. Reuse the strategies I'm using with this monster for other members of my game world.

Con: This model is rigid. Sometimes we would like to define something that is not easily achieved by just trying to put together this "building blocks".

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
}

My idea would be next to make a series of different Strategies that could be used by different monsters. On the other side, some of them could also be used for totally different purposes (i.e., I could have a tank that also "swims"). The only problem I see with this approach is that it could lead to a explosion of pure "method" classes, i.e., Strategy classes that have as only purpose make this or that other action. In the other hand, this kind of "modularity" would allow for high reuse of stratagies, sometimes even in totally different contexts.

What is your opinion on this matter? Is this a valid reasoning? Is this over-engineering?

Also, assuming we'd make the proper adjustments to the example I gave above, would it be better to define IWalk as:

interface IWalk {
    void Walk();
}

or

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

being that doing this I wouldn't need to define the methods on Monster itself, I'd just have public getters for IWalkStrategy (this seems to go against the idea that you should encapsulate everything as much as you can!) Why?

Thanks

like image 894
devoured elysium Avatar asked Apr 11 '10 21:04

devoured elysium


People also ask

How can we prevent class explosions?

You can prevent the explosion of a class hierarchy by transforming it into several related hierarchies. Following this approach, we can extract the color-related code into its own class with two subclasses: Red and Blue . The Shape class then gets a reference field pointing to one of the color objects.

Where is strategy pattern used?

Use the Strategy pattern when you want to use different variants of an algorithm within an object and be able to switch from one algorithm to another during runtime. Use the Strategy when you have a lot of similar classes that only differ in the way they execute some behavior.

What is the common class pattern strategy?

Solution. The Strategy pattern suggests that you take a class that does something specific in a lot of different ways and extract all of these algorithms into separate classes called strategies. The original class, called context, must have a field for storing a reference to one of the strategies.

What problem does strategy pattern solve?

The strategy pattern is used to solve problems that might (or is foreseen they might) be implemented or solved by different strategies and that possess a clearly defined interface for such cases.


3 Answers

Walk, Run, and Swim seem to be implementations rather than interfaces. You could have a ILocomotion interface and allow your class to accept a list of ILocomotion implementations.

Growl could be an implementation of something like an IAbility interface. And a particular monster could have a collection of IAbility implementations.

Then have an couple of interfaces that is the logic of which ability or locomotion to use: IMove, IAct for example.

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
  }

}

By composing your class this way you will avoid some of the rigidity.

EDIT: added the stuff about IMove and IAct

EDIT: after some comments

By adding IWalk, IRun, and ISwim to a monster you are saying that anything can see the object should be able to call any of the methods implemented in any of those interfaces and have it be meaningful. Further in order for something to decide which of the three interfaces it should use you have to pass the entire object around. One huge advantage to using an interface is that you can reference it by that interface.

void SomeFunction(IWalk alienMonster) {...}

The above function will take anything that implements IWalk but if there are variations of SomeFunction for IRun, ISwim, and so on you have to write a whole new function for each of those or pass the AlienMonster object in whole. If you pass the object in then that function can call any and all interfaces on it. It also means that that function has to query the AlienMonster object to see what its capabilities are and then decide which to use. All of this ends up making external a lot of functionality that should be kept internal to class. Because you are externalizing all of that and there is not commonality between IWalk, IRun, ISwim so some function(s) could innocently call all three interfaces and your monster could be running-walking-swimming at the same time. Further since you will want to be able to call IWalk, IRun, ISwim on some classes, all classes will basically have to implement all three interfaces and you'll end up making a strategy like CannotSwim to satisfy the interface requirement for ISwim when a monster can't swim. Otherwise you could end up trying call an interface that isn't implemented on a monster. In the end you are actually making the code worse for the extra interfaces, IMO.

like image 52
Felan Avatar answered Nov 01 '22 18:11

Felan


In languages which support multiple inheritance, you could indeed create your Monster by inheriting from classes which represent the things it can do. In these classes, you would write the code for it, so that no code has to be copied between implementing classes.

In C#, being a single inheritance language, I see no other way than by creating interfaces for them. Is there a lot of code shared between the classes, then your IWalkStrategy approach would work nicely to reduce redundant code. In the case of your example, you might also want to combine several related actions (such as walking, swimming and running) into a single class.

Reuse and modularity are Good Things, and having many interfaces with just a few methods is in my opinion not a real problem in this case. Especially because the actions you define with the interfaces are actually different things which may be used differently. For example, a method might want an object which is able to jump, so it must implement that interface. This way, you force this restriction by type at compile time instead of some other way throwing exceptions at run time when it doesn't meet the method's expectations.

So in short: I would do it the same way as you proposed, using an additional I...Strategy approach when it reduces code copied between classes.

like image 22
Daniel A.A. Pelsmaeker Avatar answered Nov 01 '22 18:11

Daniel A.A. Pelsmaeker


From a maintenance standpoint, over-abstraction can be just as bad as rigid, monolithic code. Most abstractions add complication and so you have to decide if the added complexity buys you something valuable. Having a lot of very small work classes may be a sign of this kind of over-abstraction.

With modern refactoring tools, it's usually not too difficult to create additional abstraction where and when you need it, rather than fully architecting a grand design up-front. On projects where I've started very abstractly, I've found that, as I developed the concrete implementation, I would discover cases I hadn't considered and would often find myself trying to contort the implementation to match the pattern, rather than going back and reconsidering the abstraction. When I start more concretely, I identify (more of) the corner cases ahead of time and can better determine where it really makes sense to abstract.

like image 23
Dan Bryant Avatar answered Nov 01 '22 18:11

Dan Bryant