A while ago it happened to me again. I was working on a piece of code for which I had to extend a class and override a method; while doing so I ran straight into a brick wall in the form of that miserable throwback of the Java language, the
final method. Let me tell you about it….
About a week and a half ago (from the time of writing), I had to adapt some code in the codebase of one of my projects. The code in question was business rule validation code that was being applied to a data set. It had worked fine in the past, but now the business wanted an exception made to the general rule. In other words, in certain circumstances the business rule validation should not take place. Being a well-mannered developer I work test-first, which meant creating a test to prove that certain code wasn’t being called. So how do you do that? Well, in my case it meant subclassing the class with business rules in my unit test and overriding the method in question to throw an exception if it was called. That, and overriding a setter that I needed to work slightly differently.
So that was the plan and that was what I did. I created my subclass with the overridden methods, created an instance of it and used a setter to inject my new object into the class that the unit test was testing. At least, that was the plan. Really, people, who makes a setter method
Polymorphism and the open-closed principle
So what, at the heart of it, really is the problem with
final as a concept (in Java or any other object oriented programming language)? To answer that question we have to examine two central principles of object oriented software development (one of which is based on the other): polymorphism and the open-closed principle.
open for extension, but closed for modification
In other words, once a software module hasbeen written and published, changes should be made by extension rather than modification. The reason is obvious: if you modify existing code, you risk breaking code that depends on the existing behavior of that code. If specialized behavior is needed, it should be added through specialization and not through change. By consequence, the code should allow for extension and not modification.
The Open-Closed Principle is an application of the concept of polymorphism, the idea that code can be subclassed and specialized subclasses can override behavior defined by superclasses. More specifically, polymorphism is the concept that any one method can occur in (literally) “different shapes” – given a method signature, subclassing and overriding allows you to provide multiple implementations for that method declaration.
To give a very simple example: say that you define a class hierarchy of apples. You create an abstract class
Apple with an abstract method
public abstract Color getColor();. You then introduce subclasses for different kinds of apples (Granny Delicious, Elstar, etc.). Each of these subclasses provides its own implementation of the
getColor() method, since they all have different colors. This is polymorphism in action: one method declaration can be associated with multiple method definitions.
The problem with
final as a concept is obvious in the light of these central principles of object orientation:
final kills of all inheritance possibilities and with it polymorphism and the Open-Closed Principle. In other words,
final takes your object oriented program and turns it into an old-fashioned imperative program instead.
Valid uses of
That isn’t to say there aren’t valid (or at least seemingly valid) uses of
final in Java. Framework builders often have a real need of
final to prevent their framework code from being overwritten. The main application of
final in this context is to allow you to keep on developing and extending your own framework code across releases without breaking code written by users of your framework. The archetypal example of this is the
java.lang.String class that is part of the Java core library. The JCP would not be able to keep on adding to and expanding upon this class if it wasn’t totally frozen; if
String was open to extension, anybody might extend it and any changes in the next Java release might break those extensions. Similar considerations exist in most frameworks.
So here we are, faced with this ostensible contradiction between a mechanism that is anathema to object oriented programming and yet a real need for this mechanism to prevent third-party developers from boxing you in. How can we reconcile these issues and reach a satisfactory middle ground?
The answer starts with stepping back and examining what the
final concept tries to achieve in a more formal sense. Informally, the whole point of
final is to freeze a piece of code with respect to redefinition (i.e. overriding). That is,
final is supposed to prevent everybody from changing the meaning of a piece of code. In a more formal sense,
final is meant to safeguard the trinary relation
that describes the semantics of a method (in this relationship P is the method precondition, S is the method and Q is the method postcondition). Overriding may, obviously, change this relationship.
However, even though it is very effective,
final is a very drastic measure to safeguard this relation. As mentioned before,
final serves to safeguard the relation by freezing the code – and therefore prevents redefinitions of the code that may in fact be quite valid within the contraint of this relation. Specifically, predicate calculus teaches us that there are two types of redefinitions that we can allow within this relation without breaking client code: we can weaken the precondition (thereby expecting less of the client code) and/or strengthen the postcondition (thereby providing the client code with more than it expects). Either one will allow redefined code to serve as a drop-in replacement for the original code. And moreover will allow redefition of the code within constraints that are guaranteed not to break anything (rather than forbidding all redefinition).
In other words: what is needed in Java is a form of compiler-based pre- and postcondition checking. Which should be based on a language extension that allows developers to express these pre- and postconditions in a formal way. Something similar to the Design By Contract mechanism introduced by Bertrand Meyer in his Eiffel language (although preferably a little more capable). Something in any case that allows us to constrict overriding in a way that is less blunt than