For those without the time to read the full article, the “tl;dr” is
- We should favor composition to inheritance
- We should leverage code generation tooling to reduce boilerplate, maintenance cost, and encourage best practices
- AutoDelegate is a project which does both and is available on Maven Central!
Classical inheritance is the most widely used and hated abstraction in programming languages. It starts small but often results in complex and hard-to-understand behavior in large software projects. It makes the task of refactoring code orders of magnitude riskier. Brian Goetz, Java Language Architect at Oracle says “my one-sentence summary of the history of programming languages is, ‘we have one good trick that works.’ And that trick is composition.". Josh Bloch, in Effective Java Item 18, tells us “Favor composition over inheritance”. This idea is about as much consensus on a topic that programmers are capable of developing, as we love to debate the nuances of each approach. So why is inheritance so common? In my opinion, its because composition has the opportunity to be much more verbose than inheritance, so it’s easier for our minds to first jump to an inheritance hierarchy rather than composing the behavior among multiple distinct classes and interfaces with clear separation of responsibility.
Even though we know that code will be read thousands of times more than it will be written or modified in any meaningful way, it’s challenging to think that way when writing code. As we know, clear is better than clever Also, much of the code that’s being written is “boring” or “repetitive” to write. Luckily, we have robots! 🤖
I’m far from the first person to recognize this problem. Google’s auto project has an excellent track record for using code generation to reduce boilerplate in doing things “the right way”. Indeed, AutoDelegate leverages some utilities exposed by auto, so shoutout to the maintainers for helping lower the barrier to entry for writing annotation processors!
The primary inspiration for AutoDelegate was the Kotlin language feature called delegation. I have no doubts that this is a fundamentally superior approach to any annotation processing and code generation approach could hope to achieve. However, there are lots of Java projects out there that have no desire or bandwidth to integrate Kotlin into their projects. Making delegation more accessible in pure Java should help developers write abstractions on the JVM that work and can be maintained.
My solution to this problem is AutoDelegate! This project releases two components to Maven Central:
- auto-delegate-annotations for decorating your classes with the metadata necessray to generate the delegation boilerplate
- auto-delegate-processor provides a
javax.annotation.processing.Processingimplementation that looks for classes annotated with
auto-delegate-annotationsand generates classes that enable the use of composition!
Processor generates abstract classes that compose an instance of an interface specified on the annotation and automatically forward
Show me the code! The goal of this library is to encourage the use of composition over inheritance as described by Effective Java Item 18 “Favor
composition over inheritance”. In the section of the book, Bloch describes an
InstrumentedSetthat counts the number of
items added to it. To accomplish this, Bloch creates an abstract implementation of
ForwardingSet that simply composes a
java.util.Set instance and forwards all calls to it. This allows Bloch
to write the
InstrumentedSet in a less verbose manner, by extending
ForwardingSet and overriding the “add” related
methods for instrumentation purposes. This is a great solution in the context of Java, but Kotlin lowers the cognitive
barrier of using composition by making it less verbose to do so. In Kotlin, the need for a
ForwardingSetis obviated by
the delegation language feature discussed above. The
InstrumentedSet can be
written concisely without relying on writing a
AutoDelegate strives to enable developers in the same fashion by generating abstract
Forwarding classes that
delegate to the inner composed instance. An equivalent
InstrumentedSet implementation written with
While this is not as concise as the Kotlin implementation, it generates a class called
the same package as the declaring class. The declared class can then extend the generated class and call
APIs where appropriate, only overriding methods that are relevant to the implementation
Author Ryan Dens