Achieving single-responsibility principle with python abstract classes

Question:

I want to separate the DB models from the actual classes. But i need two static functions for fetching data from the DB regardless of the subclass type. the implementation for both functions are the same across all DB models.

pyright showing an error that cls inside get() and get_all() functions doesn’t have a db property.

from abc import ABC, abstractstaticmethod


class DogsDB:
    lists = ["DOG1", "DOG2", "DOG3"]

    @classmethod
    def get(cls, id):
        return cls.lists[id]


class CatsDB:
    lists = ["CAT1", "CAT2", "CAT3"]

    @classmethod
    def get(cls, id):
        return cls.lists[id]


class Animal(ABC):
    def __init__(self, name):
        self.name = name

    @abstractstaticmethod
    def save(m):
        pass

    @abstractstaticmethod
    def _from_model(obj):
        pass

    @classmethod
    def get(cls, id):
        obj = cls.db.get(id)
        return cls._from_model(obj)

    @classmethod
    def get_all(cls):
        objs = cls.db.lists

        lists = []
        for obj in objs:
            e = cls._from_model(obj)
            lists.append(e)
        return lists

    def __repr__(self):
        return self.name


class DogSound:
    def __init__(self, name):
        self.name = name

    def sound(self):
        print(self.name, ": DOG SOUND!!")


class Dog(Animal, DogSound):
    db = DogsDB

    def __init__(self, name, age):
        super(Dog, self).__init__(name)
        self.age = age

    @staticmethod
    def save(m):
        print(m)

    @staticmethod
    def _from_model(obj):
        return Dog(obj, 4)


class Cat(Animal):
    db = CatsDB

    def __init__(self,  name, age):
        super().__init__(name)
        self.age = age

    @staticmethod
    def save(m):
        print(m)

    @staticmethod
    def _from_model(obj):
        return Cat(obj, 4)


print(Cat.get(1))
print(Dog.get(1))
print(Cat.get_all())
print(Dog.get_all())
Dog.get(1).sound()

Asked By: ohxdMAGsDCiCJ

||

Answers:

I cannot duplicate your first error.

Your second issue is a result of method sound implicitly returning None since it has no return statement and you have print(Dog.get(1).sound()), which will print out the return value from that method. You either want to change this to just Dog.get(1).sound() or modify the sound method to return what it is currently being printed and remove the print statement (my choice).

As an aside, I found this class structure a bit difficult to follow. Why do you need a separate DogSound class with a name attribute which should belong to Animal? Also, it seems to me that age could/should be an attribute of Animal since both cats and dogs have an age.

from abc import ABC, abstractstaticmethod


class DogsDB:
    lists = ["DOG1", "DOG2", "DOG3"]

    @classmethod
    def get(cls, id):
        return cls.lists[id]


class CatsDB:
    lists = ["CAT1", "CAT2", "CAT3"]

    @classmethod
    def get(cls, id):
        return cls.lists[id]


class Animal(ABC):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @abstractstaticmethod
    def save(m):
        pass

    @abstractstaticmethod
    def _from_model(obj):
        pass

    @classmethod
    def get(cls, id):
        obj = cls.db.get(id)
        return cls._from_model(obj)

    @classmethod
    def get_all(cls):
        objs = cls.db.lists

        lists = []
        for obj in objs:
            e = cls._from_model(obj)
            lists.append(e)
        return lists

    def __repr__(self):
        return self.name

class Dog(Animal):
    db = DogsDB

    def __init__(self, name, age):
        super().__init__(name, age)

    def sound(self):
        return f"{self.name}: DOG SOUND!!"

    @staticmethod
    def save(m):
        print(m)

    @staticmethod
    def _from_model(obj):
        return Dog(obj, 4)


class Cat(Animal):
    db = CatsDB

    def __init__(self,  name, age):
        super().__init__(name, age)
        self.age = age

    @staticmethod
    def save(m):
        print(m)

    @staticmethod
    def _from_model(obj):
        return Cat(obj, 4)


print(Cat.get(1))
print(Dog.get(1))
print(Cat.get_all())
print(Dog.get_all())
print(Dog.get(1).sound())

Prints:

CAT2
DOG2
[CAT1, CAT2, CAT3]
[DOG1, DOG2, DOG3]
DOG2: DOG SOUND!!

If for some reason you want DogSound to be a separate class, then there is no need for the name attribute to be duplicated:

...
class DogSound: # A "Mixin" class
    def sound(self):
        return f"{self.name}: DOG SOUND!!"


class Dog(Animal, DogSound):
    db = DogsDB

    def __init__(self, name, age):
        super().__init__(name, age)

    @staticmethod
    def save(m):
        print(m)

    @staticmethod
    def _from_model(obj):
        return Dog(obj, 4)
...
Answered By: Booboo