In my organization I came across versions of the following code:
from abc import ABC
class Animal(ABC):
def __init__(self, name=" "):
self.name = name
class Dog(Animal):
pass
class Cat(Animal):
pass
class Fish(Animal):
pass
collection: list[Animal] = [
Dog(),
Cat(),
Fish(),
]
The intended purpose (there might be other intentions as well) of this is to have the IDE (VSCode) have type hints for the various objects. However, we will have many Animal
s and none of the derived classes implements any features.
Wouldn't this follow DRY more closely:
class Animal:
def __init__(self, type, name=" "):
self.type = type
self.name = name
collection: list[Animal] = [
Animal(type="Dog"),
Animal(type="Cat"),
Animal(type="Fish"),
]
I do understand that the second implementation has the downside of allowing for arbitrary arguments for type
, maybe this could be avoided?
What's the right way to go about this? How to deal with a situation where classes are just existing to name things? Is this best practice?
CodePudding user response:
I also find it weird to define empty classes just for the type checking, but still no this is not getting against the DRY principle. In the contrary if you need to distinguish between types, then the attribute approach goes against it since you will need to place checks if type == 'Dog'
all over the place.
You could avoid the arbitrary arguments for type by defining an Emun that takes specific, values.
There are several pros for choosing inheritance over the type argument. It allows you for example to define stricter APIs you could have for example a function that only takes Dog
argument and let the type system do the necessary checks.
It is also leading to more flexible code in terms of extensibility and maintainability. You can introduce a new type of Animal by simply introducing a new subclass. You can choose to ignore how old classes would behave in any new function by simple restricting the input argument to the new class only. You may check the Open close principle from SOLID principles for more examples.
On the other hand inheritance is the stronger coupling you can get between two objects and the hardest to remove. Any change to the Animal class propagates all the way down to the subclasses and can lead to unintended behaviour. Using the type attribute could allow you to use a functional approach where you can apply a pipeline of functions to achieve a given result. You may look for fluent design, real world examples of this would be Pyspark, C# Linq or Java Streams.
Python allows you to use any of the two paradigms or mix and match as you like.