2

I have a pretty big class that i want to break down in smaller classes that each handle a single part of the whole. So each child takes care of only one aspect of the whole.

Example "Design" before and after

Each of these child classes still need to communicate with one another. For example Data Access creates a dictionary that Plotting Controller needs to have access to. And then plotting Controller needs to update stuff on Main GUI Controller. But these children have various more inter-communication functions.

How do I achieve this?

I've read Metaclasses, Cooperative Multiple Inheritence and Wonders of Cooperative Multiple Inheritence, but i cannot figure out how to do this.

The closest I've come is the following code:

class A:
    def __init__(self):
        self.myself = 'ClassA'

    def method_ONE_from_class_A(self, caller):
        print(f"I am method ONE from {self.myself} called by {caller}")
        self.method_ONE_from_class_B(self.myself)

    def method_TWO_from_class_A(self, caller):
        print(f"I am method TWO from {self.myself} called by {caller}")
        self.method_TWO_from_class_B(self.myself)


class B:
    def __init__(self):
        self.me = 'ClassB'

    def method_ONE_from_class_B(self, caller):
        print(f"I am method ONE from {self.me} called by {caller}")
        self.method_TWO_from_class_A(self.me)

    def method_TWO_from_class_B(self, caller):
        print(f"I am method TWO from {self.me} called by {caller}")


class C(A, B):

    def __init__(self):
        A.__init__(self)
        B.__init__(self)

    def children_start_talking(self):
        self.method_ONE_from_class_A('Big Poppa')


poppa = C()
poppa.children_start_talking()

which results correctly in:

I am method ONE from ClassA called by Big Poppa
I am method ONE from ClassB called by ClassA
I am method TWO from ClassA called by ClassB
I am method TWO from ClassB called by ClassA

But... even though Class B and Class A correctly call the other children's functions, they don't actually find their declaration. Nor do i "see" them when i'm typing the code, which is both frustrating and worrisome that i might be doing something wrong.

As shown here

Cannot find declaration

Is there a good way to achieve this? Or is it an actually bad idea?

EDIT: Python 3.7 if it makes any difference.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
Andrei M.
  • 313
  • 2
  • 10
  • What do you mean by find their declaration? I don't understand what's wrong with your solution. – A. Abramov May 26 '20 at 14:51
  • It means that in Class B when i write _self._ it does not show the functions from any other class except Class B. You can clearly see in the second picture that it shows "Unresolved attribute reference" for that function, which exists in class A. Here is a image showing that it cannot "see" the methods from the other Child classes. https://i.imgur.com/YkmFb92.png even thought it exists there, and it calls it correctly. – Andrei M. May 26 '20 at 15:08
  • Why do you think self should show other class methods? Are you intending those classes to inherit, or should they contain instances of one another? – A. Abramov May 26 '20 at 15:12
  • The first schema is about a high level modeling of cooperating objects. Fine. But what inheritance are you trying to put there? Remember, inheritance is a *is a* relation, and it looks like you are using the wrong tool here. – Serge Ballesta May 26 '20 at 15:22

1 Answers1

1

Inheritance

When breaking a class hierarchy like this, the individual "partial" classes, we call "mixins", will "see" only what is declared directly on them, and on their base-classes. In your example, when writing class A, it does not know anything about class B - you as the author, can know that methods from class B will be present, because methods from class A will only be called from class C, that inherits both.

Your programming tools, the IDE including, can't know that. (That you should know better than your programming aid, is a side track). It would work, if run, but this is a poor design.

If all methods are to be present directly on a single instance of your final class, all of them have to be "present" in a super-class for them all - you can even write independent subclasses in different files, and then a single subclass that will inherit all of them:

from abc import abstractmethod,  ABC

class Base(ABC):

    @abstractmethod
    def method_A_1(self):
        pass

    @abstractmethod
    def method_A_2(self):
        pass

    @abstractmethod
    def method_B_1(self):
        pass

class A(Base):
    def __init__(self, *args, **kwargs):
        # pop consumed named parameters from "kwargs"
        ...
        super().__init__(*args, **kwargs)
        # This call ensures all __init__ in bases are called
        # because Python linearize the base classes on multiple inheritance

    def method_A_1(self):
        ...

    def method_A_2(self):
        ...


class B(Base):
    def __init__(self, *args, **kwargs):
        # pop consumed named parameters from "kwargs"
        ...
        super().__init__(*args, **kwargs)
        # This call ensures all __init__ in bases are called
        # because Python linearize the base classes on multiple inheritance

    def method_B_1(self):
        ...

    ...


class C(A, B):
    pass

(The "ABC" and "abstractmethod" are a bit of sugar - they will work, but this design would work without any of that - thought their presence help whoever is looking at your code to figure out what is going on, and will raise an earlier runtime error if you per mistake create an instance of one of the incomplete base classes)

Composite

This works, but if your methods are actually for wildly different domains, instead of multiple inheritance, you should try using the "composite design pattern".

No need for multiple inheritance if it does not arise naturally.

In this case, you instantiate objects of the classes that drive the different domains on the __init__ of the shell class, and pass its own instance to those child, which will keep a reference to it (in a self.parent attribute, for example). Chances are your IDE still won't know what you are talking about, but you will have a saner design.


class Parent:
    def __init__(self):
        self.a_domain = A(self)
        self.b_domain = B(self)

class A:
    def __init__(self, parent):
        self.parent = parent
        # no need to call any "super...init", this is called
        # as part of the initialization of the parent class

    def method_A_1(self):
        ...

    def method_A_2(self):
        ...


class B:
    def __init__(self, parent):
        self.parent = parent

    def method_B_1(self):
        # need result from 'A' domain:
        a_value = self.parent.a_domain.method_A_1()
        ...


This example uses the basic of the language features, but if you decide to go for it in a complex application, you can sophisticate it - there are interface patterns, that could allow you to swap the classes used for different domains, in specialized subclasses, and so on. But typically the pattern above is what you would need.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Thank you very much for this very detailed response. The Inheritance part I understand. Basically what i'm missing is the Base Class which contains all the child methods that may be called by other children. The Composite part is probably what I had in mind but I didn't know how it's called. I did use _parent_ at a point, but it seemed as a bad idea because calling other child functions needed _parent.child_a.function()_ which to me looks like spaghetti code. I will probably try both of these tomorrow and see which one feels more sane, and would confuse anyone less in the future. – Andrei M. May 26 '20 at 15:45
  • 1
    The `parent.child_a.method_a` is not bad - it is better than having hundreds of unrelated methods in the same namespace. The "adaptor" pattern can mitigate that - essentially, you'd create a function that fetches and calls `self.parent.child_a.method_a()` by doing `adapt(self, "domain_a", method_a)()` – jsbueno May 26 '20 at 15:54
  • But perceive that y using and adaptor, you may have to type less, may have semantically better looking code, but the chances that your IDE will know what you are talking about will go to zero. – jsbueno May 26 '20 at 15:56
  • Yea it seems that having both a clean solution and making it less verbose is pretty impossible. By the way i tried the inheritance part and it showed that _abstractmethod_ was deprecated in Python 3.3 and it recommended using _classmethod_ instead. Which also seems to work without ABC imports. Anyway thanks a lot again for the help. Will try tomorrow. – Andrei M. May 26 '20 at 16:19