Zoo keeping: dynamic assembling based on subclass attributes
Apr 14, 2024
You are the owner of a zoo conglomerate. You operate a number of zoos in different regions, each with a number of animals. You have a class hierarchy for each animal, for example, lions have a base class BaseLion
and are instances of one of the subclasses AfricanLion
and AsianLion
. You want to start a zoo in Africa and you want to assemble a list of all the animals in the local variety.
A bad way
A bad way to do this would be to manually add each subclass to the list of animals in the zoo. It’s error-prone to manually add each subclass to the zoo, especially when the number of subclasses is large.
# Base classes for animals
class Animal:
region: str
family: str
def __str__(self):
return self.__class__.__name__
class Cats(Animal): ...
class Elephants(Animal): ...
class Bears(Animal): ...
# Derived classes for Lions
class Cheetah(Cats):
region = "african"
family = "cats"
class BengaTiger(Cats):
region = "asian"
family = "cats"
# Derived classes for Elephants
class SavannaElephant(Elephants):
region = "african"
family = "elephant"
class SumatranElephant(Elephants):
region = "asian"
family = "elephant"
# Derived classes for Bears
class AmericanBlackBear(Bears):
region = "american"
family = "bear"
class EurasianBrownBear(Bears):
region = "eurasian"
family = "bear"
# Base Zoo class
class Zoo:
def __init__(self):
self.animals = {}
def add_animal(self, animal):
animal_instance = animal()
family_name = animal_instance.family
if family_name not in self.animals:
self.animals[family_name] = animal_instance
else:
print(f"A {family_name} is already in the zoo. Cannot add another.")
def list_animals(self):
print(f"Animals in the Zoo:")
for family, animal in self.animals.items():
print(animal)
# Manually adding each animal type to the zoo
class ManualAfricanZoo(Zoo):
def __init__(self):
super().__init__()
self.add_animal(Cheetah)
self.add_animal(SavannaElephant)
self.add_animal(EurasianBrownBear)
# Example Usage
manual_zoo = ManualAfricanZoo()
manual_zoo.list_animals()
# Output
# Animals in the Zoo:
# Cheetah
# SavannaElephant
# EurasianBrownBear # Incorrect
A better way
A better way would be to dynamically assemble the list of animals based on the subclass attributes.
# Base classes for animals
class Animal:
region: str
family: str
def __str__(self):
return self.__class__.__name__
class Cats(Animal): ...
class Elephants(Animal): ...
class Bears(Animal): ...
# Derived classes for Lions
class Cheetah(Cats):
region = "african"
family = "cats"
class BengaTiger(Cats):
region = "asian"
family = "cats"
# Derived classes for Elephants
class SavannaElephant(Elephants):
region = "african"
family = "elephant"
class SumatranElephant(Elephants):
region = "asian"
family = "elephant"
# Derived classes for Bears
class AmericanBlackBear(Bears):
region = "american"
family = "bear"
class EurasianBrownBear(Bears):
region = "eurasian"
family = "bear"
class Zoo:
def __init__(self, region):
self.animals = {}
self.region = region
self.load_animals(region)
def add_animal(self, animal):
animal_instance = animal()
family_name = animal_instance.family
if family_name not in self.animals:
self.animals[family_name] = animal_instance
else:
print(f"A {family_name} is already in the zoo. Cannot add another.")
def list_animals(self):
print("\n----------------------------")
print(f"Animals in the {self.region} zoo:")
print("----------------------------")
for family_name, animal_instance in self.animals.items():
print(f"{family_name}: {animal_instance}")
def load_animals(self, region):
animal_classes = sum([a.__subclasses__() for a in Animal.__subclasses__()], [])
for cls in animal_classes:
if hasattr(cls, 'region') and cls.region == region:
self.add_animal(cls)
# Specialized Zoo classes
class AfricanZoo(Zoo):
def __init__(self):
super().__init__("african")
class AsianZoo(Zoo):
def __init__(self):
super().__init__("asian")
# Example Usage
african_zoo = AfricanZoo()
african_zoo.list_animals()
asian_zoo = AsianZoo()
asian_zoo.list_animals()
# Output
# ----------------------------
# Animals in the african zoo:
# ----------------------------
# cats: Cheetah
# elephant: SavannaElephant
# ----------------------------
# Animals in the asian zoo:
# ----------------------------
# cats: BengaTiger
# elephant: SumatranElephant
Dependency inversion
This is a good example of the dependency inversion principle. It is one of the five solid principles proposed by Robert Martin, which encourages decoupling in software systems to promote flexibility and modularity. The principle states:
- High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces or abstract classes).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
In the example above, the Zoo
class is a high-level module that depends on the Cats
, Elephants
, and Bears
classes, which are low-level modules. The Zoo
class depends on the abstractions provided by the Cats
, Elephants
, and Bears
classes, which are the Animal
base class and its subclasses via their region
and family
interface. This allows the Zoo
class to be flexible and extensible, as it can work with any subclass of Animal
without needing to know the specific details of each subclass.