How to implement a Objective-C-like class cluster in Python
There’s an Objective-C design pattern known as class cluster. A class cluster consist of a public abstract base class and private concrete subclasses. The process of instantiating and working with the instances of the concrete classes is completely transparent, i.e. it is as if we were instantiating the base class and working with its instances, except that “behind the scenes” the most suitable concrete classes are used (usually based on the parameters passed to the -init
or factory method). The concrete subclasses are thus an implementation detail virtually invisible in the base class’s interface. (It can get complicated if you need to derive from the base class, though. But that’s another story.)
One way to implement this would be to use a custom factory method. But in Objective-C usually the normal -init
method is used for class clusters as well. After all being a class cluster is an implementation detail and is no reason to have a factory method in and of itself.
After some tinkering I came up with this way to accomplish the same in Python 3:
The two calls return a SmallThing
and a LargeThing
, respectively.
Why use __call__
and metaclasses instead of just __new__
? We can define __call__
to take any arguments we need without further consequences. If we defined __new__
with some arguments, Python would then want to be able to call __init__
with the same arguments. While this isn’t visible from the example above, the __call__
approach allows for different __init__
method signatures for the concrete classes. Furthermore, we could make Thing
into a proper ABC, with implemented but abstract __init__
with yet another signature – something quite likely in a real-world setting. Also note that this approach requires exactly two definitions of __call__
regardless of the number of concrete subclasses.
That said, using the __new__
method or a custom factory method is also possible, but IMHO it’s an inferior approach in the general case, as explained above.
Why define two metaclasses? Because subclasses inherit the metaclass in Python, and “the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases” (that’s what Python will yell at you if you try to supply something else). The ConcreteThingMeta
metaclass overrides ThingMeta
and reverts to the behaviour of the default metaclass type
(i.e. instantiating the class and calling its __init__
method). For some reason, simply setting the metaclass of our concrete classes to type
is allowed, even though it clearly is not a subclass of ThingMeta
, but it results in infinite recursion. Thus the ConcreteThingMeta
metaclass is also necessary.
Happy clustering!