How the Java Language Could Better Support Composition and Delegation
This article outlines a way of improving the Java language to better support composition and delegation. Engage in the discussion and contribute to evolving the Java Language.
The Java language lacks explicit semantic support for composition and delegation. This makes delegating classes hard to write, error-prone, hard to read and maintain. For example, delegating a JDBC ResultSet interface entails writing more than 190 delegating methods that essentially provide no additional information, as illustrated at the end of this article, and only add ceremony.
More generally, in the case of composition, Σ m(i) delegating methods need to be written where m(i) is the number of methods for delegate i (provided that all delegate method signatures are disjunct across all the delegates).
The concept of language support for delegation is not new and there are numerous articles on the subject, including [Bettini08] and [Kabanov11]. Many other programming languages like Kotlin (“Derived”) and Scala (“export”) have language support for delegation.
In one of my previous articles ”Why General Inheritance is Flawed and How to Finally Fix it”, I described why composition and delegation are so important.
External Tools
Many IDEs have support for generating delegated methods. However, this neither impacts the ability to read nor understand a delegating class. Studies show that code is generally more read than written. There are third-party libraries that provide delegation (e.g. Lombok) but these are non-standard and provide a number of other drawbacks.
More generally, it would be possible to implement a subset of the functionality proposed here in third-party libraries leveraging annotation processors and/or dynamic proxies.
Trends and Industry Standards
As the drawbacks with inheritance were more deeply understood, the trend is to move towards composition instead. With the advent of the module system and generally stricter encapsulation policies, the need for semantic delegation support in the Java language has increased even more.
I think this is a feature that is best provided within the language itself and not via various third-party libraries. Delegation is a cornerstone of contemporary coding.
In essence, It should be much easier to “Favor composition over inheritance” as stated in the book “Effective Java” by Joshua Bloch [Bloch18, Item 18].
Java Record Classes
Many of the problems identified above were also true for data classes before record classes were introduced in Java 14. Upon more thorough analysis, there might be a substantial opportunity to harvest many of the findings made during the development of records and apply these in the field of delegation and composition.
On the Proposal
My intention with this article is not to present a concrete proposal of a way to introduce semantic support for composition and delegation in Java. On the contrary, if this proposal is one of the often 10-15 different discarded initial proposals and sketches on the path that needs to be traversed before a real feature can be proposed in the Java language, it will be a huge success. The way towards semantic support for composition and delegation in Java is likely paved with a number of research papers, several design proposals, incubation, etc. This feature will also compete against other features, potentially deemed to be more important to the Java ecosystem as a whole.
One motto for records was “model data as data” and I think that we should also “model delegation as delegation”. But what is delegation? There are likely different views on this within the community.
When I think of delegation, the following springs to mind: A delegating class has the following properties:
Has one or more delegates
Delegates methods from its delegates
Encapsulates its delegates completely
Implements and/or uses methods from its delegates (arguably)
An Outline - The Emissary
In the following, I will present an outline to tackle the problem. In order to de-bikeshed the discussion, I will introduce a new keyword placeholder called “emissary” which is very unlikely ever to be used in a real implementation. This word could later be replaced by “delegator” or any other descriptive word suitable for the purpose or perhaps even an existing keyword.
An emissary class has many similarities to a record class and can be used as shown in the example below:
public emissary Bazz(Foo foo, Bar bar);
As can be seen, the Bazz class has two delegates (Foo and Bar) and consequently an equivalent desugared class is created having two private final fields:
private final Foo foo;
private final Bar bar;
An emissary class is also provided with a constructor. This process could be the same as for records with canonical and compact constructors:
public final class Bazz {
private final Foo foo;
private final Bar bar;
public Bazz(Foo foo, Bar bar) {
this.foo = foo;
this.bar = bar;
}
}
It also makes the emissary class implement Foo and Bar. Because of this, Foo and Bar must be interfaces and not abstract or concrete classes. (In a variant of the current proposal, the implementing interfaces could be explicitly declared).
public final class Bazz implements Foo, Bar {
private final Foo foo;
private final Bar bar;
public Bazz(Foo foo, Bar bar) {
this.foo = foo;
this.bar = bar;
}
}
Now, in order to continue the discussion, we need to describe the example classes Foo and Bar a bit more which is done hereunder:
public interface Foo() {
void f();
}
public interface Bar() {
void b();
}
By declaring an emissary class we, unsurprisingly, also get the actual delegation methods so that Bazz will actually implement its interfaces Foo and Bar:
public final class Bazz implements Foo, Bar {
private final Foo foo;
private final Bar bar;
public Bazz(Foo foo, Bar bar) {
this. Foo = foo;
this.bar = bar;
}
@Override
void f() {
foo.f();
}
@Override
void b() {
bar.b();
}
}
If the delegates contain methods with the same signature, these would have to be explicitly “de-ambigued”, for example in the same way as default methods in interfaces. Hence, if Foo and Bar both implements c() then Bazz needs to explicitly declare c() to provide reconciliation. One example of this is shown here where both delegates are invoked:
@Override
void c() {
foo.c();
bar.c();
}
Nothing prevents us from adding additional methods by hand, for example, to implement additional interfaces the emissary class explicitly implements but that is not covered by any of the delegates.
It is also worth noting that the proposed emissary classes should not get a hashCode(), equals() or toString() methods generated. If they did, they would violate property C and leak information about its delegates. For the same reason, there should be no de-constructor for an emissary class as this bluntly would break encapsulation. Emissary classes should not implement Serializable and the likes by default.
An emissary class, just like a record class, is immutable (or at least unmodifiable and therefore shallowly immutable) and is hence thread-safe if all the delegates are.
Finally, an emissary class would extend java.lang.Emissary, a new proposed abstract class similar to java.lang.Enum and java.lang.Record.
Comparing Record with Emissary
Comparing the existing record and the proposed emissary classes yield some interesting facts:
record
Provides a generated hashCode() method
Provides a generated equals() method
Provides a generated toString() method
Provides component getters
Cannot declare instance fields other than the private final fields which correspond to components of the state description
emissary
Does not provide a generated hashCode() method
Does not provide a generated equals() method
Does not provide a generated toString() method
Provides delegating methods
Implements delegates (in one variant)
Can declare additional final instance fields other than the private final fields which correspond to delegates
both
A private final field for each component/delegate of the state description
A public constructor, whose signature is the same as the state/delegate description, that initializes each field from the corresponding argument; (canonical constructor and compact constructor)
Gives up the ability to decouple API from representation
Implicitly final, and cannot be abstract (ensuring immutability)
Cannot extend any other class (ensures immutability)
Extends a java.lang class other than Object.
Can declare additional methods not covered by the properties/delegates
Anticipated Use Cases
Here are some use cases of the emissary class:
Composition
Providing an Implementation for one or several interfaces using composition:
public emissary FooAndBar(Foo foo, Bar bar);
Encapsulation
Encapsulating an existing instance of a class, hiding the details of the actual implementation:
private emissary EncapsulatedResultSet(ResultSet resultSet);
…
ResultSet rs = stmt.executeQuery(query);
return new EncapsulatedResultSet(rs);
Disallow down-casting
Disallow the down-casting of an instance. I.e. an emissary class implements a restricted sub-set of its delegate’s methods where the non-exposed methods cannot be invoked via casting or reflection.
String implements CharSequence and in the example below, we provide a String viewed as a CharSequence whereby we cannot down-cast the CharSequence wrapper back to a String.
private emissary AsCharSequence(CharSequence s);
return new AsCharSequence(“I am a bit incognito.”);
Services and Components
Providing an implementation of an interface that has an internal implementation. The internal component package is typically not exported in the module-info file:
public emissary MyComponent(MyComponent comp) {
public MyComponent() {
this(new InternalMyComponentImpl());
}
// Optionally, we may want to hide the public
// constructor
private MyComponent(MyComponent comp) {
this.comp = comp;
}
}
MyComponent myComp = ServiceLoader.load(MyComponent.class)
.iterator()
.next();
Note: If InternalMyComponentImpl is composed of an internal base class, contains annotations, has non-public methods, has fields etc. These will be completely hidden from direct discovery via reflection by the emissary class and under JPMS, it will be completely protected from deep reflection.
Comparing Two ResultSet Delegators
Comparison between two classes delegating a ResultSet:
Emissary Class
// Using an emissary class. A one-liner
public emissary EncapsulatedResultSet(ResultSet resultSet);
IDE Generation
// Using automatic IDE delegation. About 1,000 lines!
public final class EncapsulatedResultSet implements ResultSet {
private final ResultSet delegate;
public EncapsulatedResultSet(ResultSet delegate) {
this.delegate = delegate;
}
@Override
public boolean next() throws SQLException {
return delegate.next();
}
// About 1000 additional lines are not shown here for brevity…
Conclusions
We may conceptually reuse record classes for providing semantic composition and delegation support in the Java language. This would greatly reduce the language ceremony needed for these kinds of constructs and would very likely nudge developers towards using composition just like record classes nudged developers towards immutability.
The scientific field of composition and delegation and what is related to is much bigger than indicated in this article. Further studies are needed before arriving at a concrete proposal. Perhaps this is just a part of something bigger?
Language support for composition and delegation in some form would make Java an even better language in my opinion.
References
[Bettini08]
Bettini, Lorenzo. “Typesafe dynamic object delegation in class-based languages”, PPPJ '08: Proceedings of the 6th international symposium on Principles and practice of programming in Java, September 2008, Pages 171–180, https://doi.org/10.1145/1411732.1411756
[Kabanov11]
Kabanov, Jevgeni. “On designing safe and flexible embedded DSLs with Java 5”, Science of Computer Programming, Volume 76, Issue 11, November 2011 pp 970–991, https://doi.org/10.1016/j.scico.2010.04.005
[Bloch18]
Bloch, Joshua., Effective Java, Third Edition, ISBN 0-13-468599-7, 2018
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.