dimanche 11 décembre 2022

General code design: shared and alternating patterns

The situation
My question applies to all programming languages with classes and inheritance. Im sure the answer is out there somewhere already, but I could not find it as i don’t have the right terminology to use.

Lets take the classic example. We got Dogs and Cats which are both of the type Animal:

abstract class Animal {
}

class Dog extends Animal {
}

class Cat extends Animal {
}

Simple enough. Some behavior is shared in the Animal class, anything that differs between Cat and Dog is coded in their respective classes.

Now suppose we got 2 planets. Earth and Mars. Both planets have Cats and Dogs. But, behavior for all animals on Earth differs from all animals on Mars. They, for example, experience a difference in gravity which affects the way they move.

There is no difference between specific animal types between the planets. Thus, all differences between animals on Earth and Mars can be coded at the parent level, that of the Animal class.

Not only that, but some behavior is available for all Animal instances on Mars that does not exist on Earth.

Ideally, in code dealing with these animals, we deal with a MarsAnimal or EarthAnimal class that is implemented by either a Dog or Cat. The implementing code does not need to know if they are Dogs or Cats. It does already know on what planet the Animal lives though.

What I thought about already
One solution would be the following:

abstract class Animal {
}
abstract class Cat extends Animal {
}
abstract class Dog extends Animal {
}
interface MarsAnimal {
}
interface EarthAnimal {
}
class MarsCat extends Cat implements MarsAnimal {
}
class MarsDog extends Dog implements MarsAnimal {
}
class EarthCat extends Cat implements EarthAnimal {
}
class EarthDog extends Dog implements EarthAnimal {
}

Ofc, the obvious problem with this is, that any behavior specific to MarsAnimal would need to be implemented in both the MarsCat and MarsDog classes. That’s ugly code duplication and definitely not what I was looking for.

The only semi-acceptable method I could think of was the following:

abstract class Animal {

    private PlanetAnimal planetAnimal;

    public function myBehavior() {
        this.planetAnimal.myBehavior();
    }
}

class Cat extends Animal {
}

class Dog extends Animal {
}

interface PlanetAnimal {
    function myBehavior();
}

class MarsAnimal implements PlanetAnimal {
    public function myBehavior() {
        // marsAnimal- specific behavior here
    }
}

class EarthAnimal implements PlanetAnimal {
    public function myBehavior() {
        // earthAnimal- specific behavior here
    }
}

Thus, when creating a Cat or Dog instance, since we know what planet they are from at that point in the code, we give then the needed PlanetAnimal instance in their constructor (either MarsAnimal or EarthAnimal).

This is close. The only problem with this is, like I said, some behavior exists only for all animals on Mars. I’d have to still implement a method in both the Animal and PlanetAnimal classes that is used only for Mars. If this is the only solution then sure, but it feels like there should be some better method out there.

So, any ideas? I’d love to hear!

Aucun commentaire:

Enregistrer un commentaire