views:

163

answers:

4

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

+2  A: 

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.

Virtlink
+1  A: 

"Find what varies and encapsulate it."

If how a monster walks varies, then encapsulate that variation behind an abstraction. If you need to change how a monster walks, you have probably have a state pattern in your problem. If you need to make sure that the Walk and Growl strategies agree, then you probably have an abstract factory pattern.

In general: no, it is definitely not over-engineering to encapsulate various concepts into their own classes. There is also nothing wrong with making concrete classes sealed or final, either. It forces people to consciously break encapsulation before inheriting from something that probably should not be inherited.

+2  A: 

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.

Felan
Another nice thing about doing it this way is that, when you want to go from A to B, you can just iterate over your list of locomotion interfaces and ask each the cost instead of hardcoding "get walk cost; get run cost; get swim cost; get fly cost; get teleport cost;..." and incurring the technical debt of having to keep that list updated as new locomotion types are implemented.
Dave Sherohman
Could you explain why Walk, Run and Swim seem to you more like implementations than interfaces? Shouldn't generally an interface model something a certain class "can do"? ISwim would mean my monster(or tank, or whatever class I try to implement) can swim. I could then implement several different strategy classes(for example) like SwimInWater SwimInAcid, etc. What is it that I'm missing here?
devoured elysium
Interfaces don't describe what something can do per se. They simply say I expect you to implement x, y, and z. If you do that then the interface can be used in place of the actual class. Then you use the interface everywhere instead of classes.Using the ILocomotion interface you can say AlienMonster wants to get to an island in a lake of acid. Maybe they can walk on acid, fly, teleport, long jump, or swim in acid. You don't really care you just loop through your ILocomotion options and ask each, can you get me to that island and they will let you know yes or no.
Felan
Let me rephrase the "Interfaces don't describe what something can do per se." It is a good idea to use interfaces in a logical and consistent fashion. Using the ILocomotion to Growl would be a bad choice. But walk, swim, run, fly, teleport, and so on are all just forms of movement. By abstracting them to a movement interface you can simplify your code a lot. You can have a 100 implementations of ILocomotion and be better off than 20 IWalk, 20 IRun, 20 ISwim, 20 ISkip, 2 ITeleport, 18 IJump implementations. If something doesn't fit an ILocomotion interface then make a new interface.
Felan
Also suppose you have some creature that can SwimInWater and SwimInAcid ... in the approach you are doing you would have to create a new Implementation SwimInWaterOrAcid, because you were only allowing for one implementation per movement type for a monster.
Felan
I get your point, yes. The only advantage is having to iterate at run-time through the collection of avaliable Abilities or Locomotions. Even that doesn't seem a big problem: most of the time my class will only have 1 or 2 types of locomotion/abilities. And it's not like I'm making the next generation game, so speed is not an issue. Ty!
devoured elysium
No its not whether you iterate over a collection or not. The advantage is that you have one well-defined interface to accomplish something verse N interfaces. Anything that cares about monsters has to be aware of all of those interfaces and whether or not the monster has them. Suppose you have 20 monsters: 5 implement IWalk/IRun/IFly, 7 implement IWalk/IRun, and 8 implement IWalk will all those interfaces really help? Who is calling those interfaces? Why does what is calling those interfaces care? Are the interfaces going to have truly different sets of functions?
Felan
+2  A: 

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.

Dan Bryant