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.