Composition in Python
I'm a big fan of Composition over inheritance. I mainly use Kotlin for my job and it has a built-in delegation mechanism which I like very much. Recently I have worked on a Python project with my friend. Python is very different from Kotlin.
Let's suppose that we create a simple program that counts down and prints a message when it's finished. I will look:
5
4
3
2
1
Boom!
But if it's in hurry, it counts down two steps:
5
3
1
Boom!
I know that it's pretty simple and we can do it with for-loop. But let's think in a bit of an object-oriented way. The main function will look like this. How should we design those counters?
At first, I will define the interface of the Counter. This is how we do it in Python.
Python doesn't have interface
keyword. It is a dynamically typed language that was popular with duck-typing. It introduced type-hinting at 3.5 but it still doesn't have the interface. Instead, we can define an abstract class that has only abstract methods. It is supported by ABC (Abstract Base Classes).
Let's define actual counters.
If you forget to override abstract methods, Python raises an error when the class is instantiated, not when the method is called. It's not as safe as compiled language but we can notice the error in the early stage.
Traceback (most recent call last):
File "/Users/alpaca0984/count_down.py", line 61, in <module>
main()
File "/Users/alpaca0984/count_down.py", line 50, in main
counter = StepTwoCounter(initial)
TypeError: Can't instantiate abstract class StepTwoCounter with abstract method count_down
Okay, good. Now the program works. But you realize that the alert()
is duplicated in StepOneCounter and StepTwoCounter. It's not clean. But I won't move it into the Counter
because, as I said in the first line, I believe in composition over inheritance. Then, what should I do?
In Python, we can create Mixin. It's not the language feature but a convention. We create a class that doesn't inherit anything and, just has portable functionalities. For this case, I will create AlertMixIn
and include it in counters.
Of course, you can add any other mixins to those counters. Keep in mind that the order StepOneCounter(AlertMixIn, Counter)
is very important. AlertMixIn overrides Counter and, StepOneCounter overrides AlertMixIn. So if you do StepOneCounter(Counter, AlertMixIn)
, you will see an error saying like "undefined method alert()" because the implementation in AlertMixIn is overwritten by the abstract function in the Counter.
Django also uses mixins and it's well described. It is really helpful to understand how mixins are supposed to be used.