-
-
Notifications
You must be signed in to change notification settings - Fork 392
Description
Followup to #1021
Possibly we should use it on... all classes? (Except ABCs.) Possibly not.
Edit on 2020-05-07 to add more rationale: The question here is about whether we should forbid subclassing on Trio classes.
Subclassing is a controversial topic in general – some people argue that it's an anti-pattern in general; some think there are situations where it's appropriate. But I think even subclassing advocates mostly agree that subclassing only works well when the base class is intentionally designed to be subclassed. In particular, without this, you can easily create fragile coupling between the internals of the base class and the subclass. For example, let's say we have a base class that uses the common trick where one method is implemented using another:
class BaseClass:
def my_op_basic(self, *basic_args):
...
def my_op_extended(self, *extended_args):
basic_args = process_extended_args(*extended_args)
return self.my_op_basic(*basic_args)If you make a subclass and want to customize how all the different my_op variations work, the obvious probably just override my_op_basic, because that automatically covers both cases:
class SubClass(BaseClass):
def my_op_basic(self, *basic_args):
# ... custom logic here ...
# no override for my_op_extended; base class version is fineWe test it, and it works great: our custom logic runs on SubClass.my_op_basic and SubClass.my_op_extended, because BaseClass.my_op_extended internally invokes SubClass.my_op_basic. But then, someone refactors BaseClass, and moves the main logic into my_op_extended:
class BaseClass: # version 2
def my_op_basic(self, *basic_args):
extended_args = process_basic_args(*basic_args)
return self.my_op_extended(*extended_args)
def my_op_extended(self, *extended_args):
...Now, BaseClass's public API hasn't changed at all – but this internal refactoring is still a breaking change for SubClass! Now calls to SubClass.my_op_basic will invoke the custom logic, but calls to SubClass.my_op_extended won't!
The point is: subclasses don't just rely on base class public APIs; they also rely on details of how the base class methods are implemented. And then something that looks like an innocent refactoring suddenly becomes a breaking change. That can be fine in some cases: if both classes are in the same project, then the tests will probably catch it and you can refactor SubClass as well. Or, if the base class was explicitly designed for subclassing so these internal implementation details are documented as part of the public API, then it's not an issue.
But for most of the classes that Trio exports as part of its public API, the potential subclassers are Trio's users, and we can't see their code. So if we do an innocent-looking refactoring and it breaks our users code, we have no way to notice that until we ship and their system falls over. And, most of the classes we export aren't carefully designed with subclassing in mind – that takes a lot of work, and isn't useful in most cases. (The main exception are the classes in trio.abc, which are explicitly designed with subclassing in mind, and their inter-method dependencies are explicitly documented.)
We make promises to our users about API stability. For most of our classes, if users are subclassing them, those promises are currently a lie – we actually have no idea which of our changes might break user code. Lying is bad. It's better to explicitly disable subclassing and give users a clear error up front, instead of letting them ship code that relies on subclassing and then starts failing in mysterious ways.