How do you define (and enforce) those rules? My general position when writing library code is that none of my classes should be subclassed.
The very few classes that may be subclassed are documented as such, and the methods that may be overridden are explicitly documented, as well as what behavior is required from the subclass when overriding those methods.
The invariants of complex inheritance hierarchies are very hard to understand. What happens if the superclass method isn't called? What happens if one of those methods is called, but another isn't, and the object is placed into an indeterminate state? What enforces that your subclass -- and all other subclasses -- will conform to these often complex and difficult to describe invariants?
This is very similar to multi-threading with mutable vs. immutable data. By making your data immutable, you grossly simplify the understanding of your system's behavior.
Why do you need to enforce rules, other than documenting the classes/methods defined?
What happens if the superclass method isn't called
Shit doesn't work or it breaks, then the person who sub-classed needs to fix it. Sometimes it also means your class is leaking encapsulation.
This also happens with plain aggregation/composition btw. It also happens with data-immutability (which has nothing to do with inheritance, as your object can be immutable and extend a dozen classes).
Why do you need to enforce rules, other than documenting the classes/methods defined?
The less repetitive work we delegate to human fallibility and instead delegate to a machine, the more time we have for human ingenuity.
Shit doesn't work or it breaks, then the person who sub-classed needs to fix it.
It's not that simple. The more difficult it is to understand the rules of behavior before changing the code, the more difficult it is to change the behavior. It's not just a question of expressing valuable -- but simple -- kindergarden requirements (this value may not be NULL), but higher-level requirements (this method must be called in the context of a READ-COMMITTED transaction).
The more you can express concisely, the easier it is to mutate the system over time. It's not a question of breaking code -- or noticing when it breaks -- but having the language assist in simply not breaking it at all.
This also happens with plain aggregation/composition btw.
Composition makes invariants easier to understand. If you then design your classes so poorly as to fail to enforce correct behavior through their API insofar as it is possible to do so, that is the programmer's failure.
It also happens with data-immutability (which has nothing to do with inheritance, as your object can be immutable and extend a dozen classes).
Data immutability is related to the avoidance of inheritance insofar as they both very significantly facilitate the full and easy comprehension of an implementation invariants.
this method must be called in the context of a
READ-COMMITTED transaction
I get what you're saying, but I like conventions and clear APIs with proper encapsulation.
Here's a sample from Python/Django ...
@transaction.commit_on_success
def do_stuff_with_the_db():
db.execute("insert into tmp values (1)")
raise Exception
Or if you need to supply the DB queries yourself, you can implement your own context-manager than use it with a with block ...
with stuff.inside_transaction() as obj:
obj.execute("query")
No need to extend a class that represents a transaction or some other shit like that.
having the language assist in simply not breaking it at all
You know that's an utopian goal. What I dislike most about languages that try to detect too much shit for me is that it gives me a false sense of security. And the worst offender is Java: not only is its type-system too weak, because it is manifest-typed you get the false impression that it guarantees stuff for you, when it doesn't.
The very few classes that may be subclassed are documented as such, and the methods that may be overridden are explicitly documented, as well as what behavior is required from the subclass when overriding those methods.
The invariants of complex inheritance hierarchies are very hard to understand. What happens if the superclass method isn't called? What happens if one of those methods is called, but another isn't, and the object is placed into an indeterminate state? What enforces that your subclass -- and all other subclasses -- will conform to these often complex and difficult to describe invariants?
This is very similar to multi-threading with mutable vs. immutable data. By making your data immutable, you grossly simplify the understanding of your system's behavior.