Home > OS >  What is the correct OOP way to add functionality to a set of classes deriving from an abstract base
What is the correct OOP way to add functionality to a set of classes deriving from an abstract base

Time:05-02

Setting

My friend told me that in OOP, you generally don't want to modify any abstract base classes in an existing codebase, because that means you have to implement the new changes to each and every derived class. I'm interested in which modification to the codebase one would prefer in a preferably pythonic way. The emphasis is on changing an existing codebase.

Example scenario

I have an abstract base class called Animal, because the code that uses this library has to interact with Animal objects. I have multiple child class implementations: Dog and Cat that each have a different set of fields that they each need for their own inner functionality. So at this point the code base looks like:

from abc import ABC, abstractmethod


class Animal(ABC):
    @abstractmethod
    def feed(self, foo: str) -> None:
        raise NotImplementedError


class Dog(Animal):
    def __init__(self):
        self.woof = "woof"

    def feed(self, food: str):
        print(f"Dog is being fed {food}. It says {self.woof}")


class Cat(Animal):
    def __init__(self):
        self.meow = "meow"
        self.purr = "purr"

    def feed(self, food: str):
        print(f"Cat is being fed {food}. It says {self.meow}")

Modification

AFTER this is implemented, the developers realize that they want to log the relevant fields (or states) from an Animal objects and what the logged data is varies from child class to child class.

Option A

Originally, my idea would be to implement another abstractmethod and add functionality that way. This forces every Animal to implement the new get_fields() in whatever way they need to.

class Animal(ABC):
    @abstractmethod
    def feed(self, foo: str) -> None:
        raise NotImplementedError

    @abstractmethod
    def get_fields(self) -> list:
        raise NotImplementedError

class Dog(Animal):
    def __init__(self):
        self.woof = "woof"

    def feed(self, food: str):
        print(f"Dog is being fed {food}. It says {self.woof}")

    def get_fields(self) -> list:
        return [self.woof]


class Cat(Animal):
    def __init__(self):
        self.meow = "meow"
        self.purr = "purr"

    def feed(self, food: str):
        print(f"Cat is being fed {food}. It says {self.meow}")

    def get_fields(self) -> list:
        return [self.meow, self.purr]

Option B

My friend is saying we shouldn't modify the abstract class however, the only other option we came up with is to do the following:

def get_field(animal: Animal) -> list:
    if isinstance(animal, Dog):
        return [animal.woof]
    elif isinstance(animal, Cat):
        return [animal.meow, animal.purr]
    else:
        raise TypeError

Which one would you go with? Is there another, better way to do this? Which one is more pythonic?

CodePudding user response:

Implement a generic mechanism on the ABC, as a concrete method, but devolve the configuration to the subclasses and DONT use hardcoded names.

I've used Meta here because that is the type of stuff you see in Django models, name-spacing what is a given class's configuration in an embedded Meta class. Django specifically uses a very similar system to track which fields get display where in the auto-generated admin panels used for data entry.

from abc import ABC, abstractmethod

from typing import Optional, List

class Animal(ABC):

    class Meta:
        #could also set to [] as default...
        fields_of_interest : Optional[List[str]] = None

    @abstractmethod
    def feed(self, foo: str) -> None:
        raise NotImplementedError

    def get_fields(self) -> List:

        if self.Meta.fields_of_interest is None:
            # could also raise NotImplementedError("need to set `fields_of_interest` on class Meta in class {self.__class__.__name__}")
            return []

        res = [getattr(self, field) for field in self.Meta.fields_of_interest]
        return res

class Dog(Animal):

    class Meta:
        fields_of_interest = ["woof"]

    def __init__(self):
        self.woof = "woof"

    def feed(self, food: str):
        print(f"Dog is being fed {food}. It says {self.woof}")


class Cat(Animal):

    class Meta:
        fields_of_interest = ["purr", "meow"]

    def __init__(self):
        self.meow = "meow"
        self.purr = "purr"

    def feed(self, food: str):
        print(f"Cat is being fed {food}. It says {self.meow}")

class Mouse(Animal):

    def feed(self, foo: str) -> None:
        print(f"{self} feed")


for cls in [Cat, Dog, Mouse]:
    animal = cls()
    print(f"{animal} {animal.get_fields()}")

output:

<__main__.Cat object at 0x1079f67d0> ['purr', 'meow']
<__main__.Dog object at 0x1079f6320> ['woof']
<__main__.Mouse object at 0x1079f67d0> []

Also, in terms of abstract vs concrete, it helps to think creatively to keep method behavior uniform (and therefore generic), rather than being overly picky. For example, either the original Design Patterns book or one taking it up was talking the Composite Pattern, which deals with "trees". Well, what they said was that instead of throwing an exception when you are on a Leaf (no children) and trying to iterate its, non-existent, children, they could just return an empty list.

  • Related