2

I have a range of different animals in my zoo such as turtles, birds etc. As they all share a common trait such as either swimming, flying etc., I thought a strategy pattern would be appropriate to model them. The thing is though that I want to call a method in the composition from the compositor. See this MWE:

Animal.java (abstract class, composition)

public abstract class Animal {
    Movement movement;
    int metersSwam = 0;

    void swimMeters(int meters){
        metersSwam += meters;
    }

    void swim() {
        movement.swim();
    }

    void fly() {
        movement.fly();
    }
}

Turtle.java (extends animal)

public class Turtle extends Animal {
    public Turtle() {
        movement = new TurtleMovement(this);
    }
}

Movement.java (interface, compositor)

public interface Movement {
    void swim();
    void fly();
}

TurtleMovement.java (here is the issue, where I call a method of Turtle)

public class TurtleMovement implements Movement {
    Animal turtle;

    public TurtleMovement(Animal turtle) {
        this.turtle = turtle;
    }

    @Override
    public void swim() {
        turtle.swimMeters(10); //<--- here
        System.out.println("I can swim, just swam 10 meters");
    }

    @Override
    public void fly() {
        System.out.println("I can't fly");
    }
}

main.java

public class Zoo {

    public static void main(String[] args){
        Animal animal = new Turtle();
        animal.fly();
        animal.swim();
    }
}

So my question is basically, am I allowed to call the method of Turtle in TurtleMovement? If not, is there a way to circumvent it or maybe the strategy-pattern isn't ideal for my situation after all?

Tyler D
  • 129

1 Answers1

4

It helps to think in terms of the actual domain you are modelling:

  • Not all animals can fly, swim, etc. Thus, methods like fly, swim, etc. don't really fit in the class Animal because, in the object-oriented sense, calling a method of any class should not appear to "randomly" cause an error. If you are going to call a class Animal, at least make some methods that are collectively shared by all animals. Remember, least astonishment!
  • A movement cannot be swimming and flying at the same time. One movement really represents one style of motion. The problem stems from trying to extend Movement as TurtleMovement etc. Each animal doesn't have its very own movement. All animals share a small variety of movements. Model movements after the actual type of movement, not after the animals.
  • Watch out for new SomeClass(this). The only power you can get from this type of abstraction is to just move code somewhere else or capture ambient context (as arrows do for example).

In short, why create a movement for every animal? Then you will have just as many movements, which will quickly become more than you can handle.

Consider:

abstract class Animal
{
    Movement movement;

    void move()
    {
        movement.move();
    }
}

abstract class Movement
{
    void move();
}

class FlyMovement extends Movement
{
    @Override
    void move()
    {
        //fly somehow
        System.out.println("I fly");
    }
}

class SwimMovement extends Movement
{
    @Override
    void move()
    {
        System.out.println("I swim.");
    }
}

class Turtle : Animal
{
    public Turtle()
    {
        movement = new SwimMovement();
    }
}

This is definitely not the best you could come up with, but it may give you an idea. If you want Animal information within the movement, then you are looking at multiple dispatch. Also, think about not creating concretions of Animal at all. Just a non-abstract Animal, which is completely characterized by its custom movement:

public class Animal
{
    public Animal(List<Movement> movementCapabilities)
    {
        //keep and use as necessary.
    }
}

Just some ideas...

EDIT


Filip's comment is a very good proposition. Generally, try to think of Animal as a simple "combinator" of behaviors. Very few concrete details need to reside in the class. Any detail will probably be relevant to a different behavior. So an animal moves, you have a Movement. Distance crossed is, indeed, more closely related to the movement behavior, than the animal. An animal eats, you have Feeding. Quantities etc. are again more closely related to the Feeding behavior, rather than the Animal.

So, you have disjoint (orthogonal, actually) behaviors and, in the end, you do:

Animal wolf = new Animal(new FourLeggedRunningMovement(...), new CarnivoreDiet(...), new CrepuscularActivityPattern(...), ...);

If you would like two movements, you can always compose two in one using a new class, say:

FourLeggedRunningMovement fourLegRun = new FourLeggedRunningMovement(...);

FourLeggedWalkingMovement fourLegWalk = new FourLeggedWalkingMovement(...);

List<Movement> movements = new List<Movement>()
{
    fourLegRun,
    fourLegWalk
}

CompositeMovement fourLegWalkRun = new CompositeMovement(movements);

//Now you can model two (or more) movements in one go.

The crucial take-home message is: - Before you set up your hierarchies and organize your classes, spend a considerable (well, proportionally) amount of time thinking how you would probably see yourself using them.

Of course the few lines of code here omit exorbitantly many details, but they are very "conceptually dense", in the sense that they can probably guide you all the way back down to the tiniest bit of detail. In object-oriented programming, it is sometimes advantageous to do before you think(!)

Vector Zita
  • 2,502