Define a multi-file class using mixin

Mixins are like "plugins" to classes, they can be used to extend the functionality of a class in a more modular manner.

Rationale

As the code base grows over time, once in a while we may need to do some restructuring. When a function grows too big, we may consider breaking it down to a few smaller ones, each focusing on a single functionality. When a module grows too big, we may consider distributing its contents over a few modules, grouped by functionality. There is another type of subdivision of code that I never thought I would ever need to, but ended up having to do, which is to break down a single class definition into multiple files.

The need to break down a class emerges as I was writing a GUI application. The main frame in the main window contains several widgets, each with various degrees of complexity. To make all the widgets function and interact with each other, I ended up writing a really long class for the main frame, so long that it makes it increasingly difficult for me to navigate around the file to find what I’m after. So I searched for some methods to split a single class definition into several files, and the solution is to use mixins.

What are mixins?

According to the post by Leonardo Giordani, the concept of mixin originated from the lisp programming language. those are small classes that provide attributes but are not included in the standard inheritance tree, working more as "additions" to the current class than as proper ancestors.

I interpret mixins as a type of "plugin" class, just like you install a plugin to extend the functionality of a software rather than installing a different piece of software to fulfill the need, one uses mixin classes to extend the functionalities of a class.

Mixin classes are defined using the object class as its only parent, and contains no constructors. They may contain attributes or methods, but are preferably kept small.

For instance, the definition of the MainFrame class in my GUI application looks like this:

class MainFrame(QtWidgets.QWidget,_MainFrameLoadData.MainFrameLoadData,
        _MainFrameDataSlots.MainFrameDataSlots,
        _MainFrameToolBarSlots.MainFrameToolBarSlots,
        _MainFrameLibTreeSlots.MainFrameLibTreeSlots,
        _MainFrameFilterListSlots.MainFrameFilterListSlots,
        _MainFrameDocTableSlots.MainFrameDocTableSlots,
        _MainFrameMetaTabSlots.MainFrameMetaTabSlots,
        _MainFrameOtherSlots.MainFrameOtherSlots,
        _MainFrameProperties.MainFrameProperties
        ):
    def __init__(self,settings, parent):
        super(MainFrame,self).__init__()
        ...
        ...

Aside from QtWidgets.QWidget which is the base class that MainFrame inherits from, all other classes in the inheritance list, such as _MainFrameLoadData.MainFrameLoadData and _MainFrameDataSlots.MainFrameDataSlots, are all mixin classes. They are all directly inherited from the object class and object only, without a __init__ constructor in the definition.

For instance, the MainFrameProperties mixin class defined in the file _MainFrameProperties.py stores some properties of the MainFrame. Its definition looks like this:

class MainFrameProperties:

    @property
    def _tabledata(self):
        if hasattr(self,'doc_table'):
            return self.doc_table.model().arraydata
        else:
            return None

    @property
    def _current_doc(self):
        if hasattr(self,'doc_table'):
            ...
            ...

    @property
    def _current_meta_dict(self):
        if hasattr(self,'t_meta'):
            if hasattr(self.t_meta,'fields_dict'):
                return self.t_meta.fields_dict
        return None
    
    ...
    ...

It is a collection of properties. (Note that in Python 3, class Name: is the same as class Name(object):. Some more info can be found in this stackoverflow answer.)

The MainFrameToolBarSlots mixin defined in the file _MainFrameToolBarSlots.py is responsible for providing the functionalities of various buttons in the tool bar. Its definition looks like this:

class MainFrameToolBarSlots:
    #######################################################################
    #                        Tool bar button slots                        #
    #######################################################################

    @pyqtSlot(QtWidgets.QAction)
    def addActionTriggered(self, action):
        """Add new document to current folder

        Args:
            action (QAction): QAction sent by menu button.

        This is a slot to the triggered signal of the Add button in the tool
        bar, and is responsible for handling adding new document to the current
        folder via PDF, bibtex, RIS or manual inputs.
        """

        # action.data() is used in labelling the default add action.
        # if it's not None, return
        if action.data() is not None:
            ...
            ...

    @pyqtSlot(QtWidgets.QAction)
    def addFolderButtonClicked(self, action):
        """Add new folder

        Args:
            action (QAction): QAction sent by menu button.

        This is a slot to the triggered signal of the Add Folder button in the
        tool bar, and is responsible for handling adding new folder to the
        folder tree.
        """

        item=self._current_folder_item
            ...
            ...

    def sortFolders(self):
        '''Sort folders in alphabetic order while keeping sys folder at top'''
            ...
            ...

All other mixins are defined in a similar manner: they focus on providing some specific functionalities for a specific part of the main frame, and each defined in a separate file. In this way, this gigantic MainFrame class is distributed over 10 different files, each kept at a more manageable size, and the entire code base has a clearer structure.

Further reading

Here are some articles on mixins that you may be interested:

Leave a Reply