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:

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.

← Back to all posts