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
__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.