Let me start by abstractly formulating the problem: I have two public interface types. One of them contains a method which receives at least two instances of the other interface type. The implementation of the method depends on the implementation of the passed objects.
Consider the following public API, which consists of two interfaces:
public interface Node {
}
public interface Tree {
void connect(Node parent, Node child);
}
Now, I want to implement that API, like so:
public class NodeImpl implements Node {
private final Wrapped wrapped;
public NodeImpl(Wrapped wrapped) {
this.wrapped = wrapped;
}
public Wrapped getWrapped() {
return wrapped;
}
}
public class TreeImpl implements Tree {
@Override
public void connect(Node parent, Node child) {
// connect parent and child using the wrapped object
}
}
public class Wrapped {
// wrapped object which actually represents the node internally
}
I need to access the wrapped objects in the connect
method, which is impossible, because the getWrapped
method is not part of the API. It is an implementation detail.
So the question is: How can I implement the connect
method without leaking implementation detail to the API?
Here is what I tried so far:
Put the connect
method in the Node
interface and call parent.connect(child)
. This gives me access to the wrapped object of the parent, however the wrapped object of the child is still not available.
Just assume the passed Node
is of type NodeImpl
and use a downcast. This seems wrong to me. There might be other Node
implementations.
Don't put the wrapped object in the node, but use a map in the TreeImpl
that maps Node
's to Wrapped
objects. This is basically the same as above. It breaks down as soon as a Node
instance is passed to the connect
method, which has no associated mapping.
Please note, that the Node
interface might contain methods. However, this is unimportant for this question.
Also, please note that I am in control of both: The interface declaration as well as the implementation.
Another attempt to solve this is to convert the connect
method to a addChild
method in the Node
interface and to make the Node
interface generic:
public interface Node<T extends Node<T>> {
void addChild(Node<T> child);
}
public class NodeImpl implements Node<NodeImpl> {
private final Wrapped wrapped;
public NodeImpl(Wrapped wrapped) {
this.wrapped = wrapped;
}
public Wrapped getWrapped() {
return wrapped;
}
@Override
public void addChild(Node<NodeImpl> child) {
}
}
public class Wrapped {
// wrapped object which actually represents the node internally
}
public Node<NodeImpl> createNode() {
return new NodeImpl(new Wrapped());
}
private void run() {
Node<NodeImpl> parent = createNode();
Node<NodeImpl> child = createNode();
parent.addChild(child);
}
Node
and createNode
are part of the public API. NodeImpl
and Wrapped
should be hidden. run
is the client code. As you can see, NodeImpl
has to be visible to the client, so this is still a leaking abstraction.
The solution is to add the implementing class as a bounded type parameter, without exposing it to the API user. This can be achieved by using a ?
as the type parameter in the return type, when creating the factory. In this way, the public API exposes none of the implementing classes (NodeFactoryImpl
, NodeImpl
, and Wrapped
) to the API user.
The API user can then match the ?
with a bounded type parameter on a method (see connectN
). In this way, the compiler is able to type-check the API usage without knowing the implementing classes and the private API implementation is able to work with the implementing classes without using downcasts.
Note that the method call parent.addChild(child)
only type-checks correctly, because the compiler is able to verify that both parent
and child
are of the same type N
, which extends Node<N>
. This call would not type-check, if parent and child were created by different factory instances.
Note also, that to match the existential type ?
with a bounded type parameter, we need to call a new method. There is currently no other way to work with existential types in Java.
Client code using API:
private void connect() {
connectN(NodeFactory.createFactory());
}
private <N extends Node<N>> void connectN(NodeFactory<N> factory) {
N parent = factory.createNode();
N child = factory.createNode();
parent.addChild(child);
}
Public API:
public interface NodeFactory<N extends Node<N>> {
public static NodeFactory<?> createFactory() {
return new NodeFactoryImpl();
}
N createNode();
}
public interface Node<N extends Node<N>> {
void addChild(N child);
}
Private API implementation:
public class NodeFactoryImpl implements NodeFactory<NodeImpl> {
public NodeImpl createNode() {
return new NodeImpl(new Wrapped());
}
}
public class NodeImpl implements Node<NodeImpl> {
private final Wrapped wrapped;
public NodeImpl(Wrapped wrapped) {
this.wrapped = wrapped;
}
public Wrapped getWrapped() {
return wrapped;
}
@Override
public void addChild(NodeImpl child) {
// use wrapped objects to connect the nodes
}
}
public class Wrapped {
// wrapped object which actually represents the node internally
}