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 them 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!
Edit 1
Based on questions, a small clarification: the main problem here is how to implement shared behavior between many different categories. There is shared behavior:
- between all animals on all planets;
- between all animals on a specific planet;
- between all animals of a specific type (i.e. Cat or Dog), irrespective of what planet they are on
Edit 2
I work mainly in Java and PHP. This question specifically was asked after I encountered this in PHP, though I know I had similar situations in Java too. I know, for PHP, my first proposed solution could work in combination with Traits. That might even be where I end up. But, everywhere online I saw people warning against the use of Traits, saying it is almost always/ always bad code design. That's why I'm searching for the best solution
CodePudding user response:
The solution you are looking for is a design pattern called the Strategy pattern. The pattern is defined as follows:
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
In your case, the Animal class would be the context, and the PlanetAnimal class would be the strategy. The context would be responsible for delegating to the correct strategy for each concrete implementation. In your example, the context would be the Animal class, and the strategy would be the PlanetAnimal class. The context would be responsible for delegating to the correct strategy for each concrete implementation.
Something like this, in Python:
class Animal:
def __init__(self, animalStrategy):
self.animalStrategy = animalStrategy
def walking(self):
raise NotImplementedError()
class Cat(Animal):
def walking(self):
return self.animalStrategy.walking(canJump=True, canFly=False, canSwim=False, speed=10)
class Bird(Animal):
def walking(self):
return self.animalStrategy.walking(canJump=True, canFly=True, canSwim=False, speed=20, walkSpeed=2)
class MarsAnimalStrategy:
def walking(self, canJump, canFly, canSwim, speed):
return f"walking on Mars with speed {speed}"
class EarthAnimalStrategy:
def walking(self, canJump, canFly, canSwim, speed, walkSpeed):
return f"walking on Earth with speed {speed} and walkSpeed {walkSpeed}"
marsCat = Cat(MarsAnimalStrategy())
marsCat.walking()
earthCat = Cat(EarthAnimalStrategy())
earthCat.walking()
In this way, the Animal class is not responsible for knowing how to walk on Mars or Earth, but instead delegates that responsibility to the strategy. The strategy is responsible for knowing how to walk on Mars or Earth. The context/"user" is responsible for delegating to the correct strategy for each concrete instance.
I wouldn't go into details, but consider using a composition of objects instead of class inheritance. In short, each descendant class of Animal
would have to be fully tested, including an internal behavior of a parent class, and Animal
could easily become a God Object, which is not a good thing. Alternatively:
class IAnimal: # Python have only classes, but consider it as an interface
def walking(self):
raise NotImplementedError()
def eating(self):
raise NotImplementedError()
# etc.
class Cat(IAnimal):
def __init__(self, physicalBody, livingEntity):
self.physicalBody = physicalBody
self.livingEntity = livingEntity
def walking(self):
direction = self.livingEntity.goalDirection()
if self.physicalBody.canMove(direction):
self.physicalBody.move(direction)
return self.livingEntity.movedTo(self.physicalBody.position)
return self.livingEntity.wasFrustrated()
Now Cat
is responsible only for its own behavior, and PhysicalBody
and LivingEntity
are responsible for their own behavior. This way, you can easily test each class separately, and you can easily replace PhysicalBody
and LivingEntity
with other implementations, without changing Cat
class. I would repeat, that now Cat
class is highly focused solely on its own behavior and implementation of IAnimal
interface/contract. Now, of course, you need to type self.physicalBody.canMove(direction)
instead of self.canMove(direction)
, but it's a small price to pay for a better design. Differences in the behavior of Earth and Mars animals would now be represented by different implementations of LivingEntity
classes, that could share some common parts.