In this article I will show how existing interfaces can be extended to provide additional features by using the
Function
interface, introduced in Java 8, without breaking existing code. I will show, through practical examples,
the usages of the apply
, compose
and andThen
methods from the Function
interface.
Let’s consider a use case to solve and we will see how the Function
interface can help us to implement it with
minimal changes to existing code.
Use case
Suppose I have an existing interface SomeInterface
which takes an input SomeInput
and returns an output
SomeOutput
. I want to extend the interface so that:
- it can work with different inputs, say
SomeOtherInput
, and - be able to produce different outputs, say
SomeOtherOutput
, and - the change should be done in a way that no existing code should break.
In code the existing interface and classes will look something like below. I have also shown client code,
SomeClient
, that uses the interface.
interface SomeInterface {
SomeOutput transform(SomeInput someInput);
}
class SomeInterfaceImpl implements SomeInterface {
@Override
public SomeOutput transform(SomeInput someInput) {
// does some processing or
// calls some external systems to build someOutput
SomeOutput someOutput = ...
return someOutput;
}
}
class SomeInput {
// variables and methods not shown
}
class SomeOutput {
// variables and methods not shown
}
class SomeClient {
SomeInterface someInterface;
SomeOutput someClientMethod(SomeInput someInput) {
SomeOutput someOutput = someInterface.transform(someInput);
return someOutput;
}
}
Though this use case can be solved in different ways, I will show how we can use the Function
interface to
solve this use case.
Implementing the use case
Let’s change the SomeInterface
by extending the Function
interface and implement the apply
method from
the Function
interface. This is the only change needed and our use case is done. Really, that’s all that’s needed to
be done. I will use examples in following sections to show how this was the only change required.
In code it will look something like below. Some points, worth noting, in the code below are:
- By making the below change, none of the existing code breaks.
- The Function interface’s
apply
method has been implemented in theSomeInterface
using default methods feature introduced in Java 8. - Even though from the interface definition of
SomeInterface
it seems that it works only withSomeInput
and produces onlySomeOutput
, with the change we didSomeInterface
can now work with any input types and can produce any output types.
import java.util.function.Function;
interface SomeInterface extends Function<SomeInput, SomeOutput> {
default SomeOutput apply(SomeInput someInput) {
return transform(someInput);
}
SomeOutput transform(SomeInput someInput);
}
Let’s see via examples how our use case got fulfilled with just this small change.
Working with different input types
The below code shows how SomeInterface
can work with an unknown input type SomeOtherInput
. The following points
are worth noting in the code below:
- The
compose
method fromFunction
is used to transformSomeOtherInput
toSomeInput
. - The
SomeOtherInput
toSomeInput
transformation logic lives inside thetoSomeInput()
instance method onSomeOtherInput
. This logic is passed intocompose
method via method references feature introduced in Java 8.SomeOtherInput::toSomeInput
is a method reference. - The
apply
method from Function is called to buildSomeOutput
fromSomeInput
.
class SomeClient {
SomeInterface someInterface;
SomeOutput someClientMethod(SomeOtherInput someOtherInput) {
SomeOutput someOutput = someInterface
.compose(SomeOtherInput::toSomeInput)
.apply(someOtherInput);
return someOutput;
}
}
class SomeOtherInput {
// variables and methods not shown
SomeInput toSomeInput() {
// logic to build SomeInput from this object
SomeInput someInput = ...
return someInput;
}
}
Producing different output types
The below code shows how SomeInterface
can produce an unknown output type SomeOtherOutput
. The following points
are worth noting in the code below:
- The
andThen
method from theFunction
interface transforms theSomeOutput
toSomeOtherOutput
. - The transformation logic for
SomeOutput
toSomeOtherOutput
resides in theSomeOtherOutput
class as a static factory method. It is provided to theandThen
method using method references feature of Java 8. - The
apply
method, like in the previous example, buildsSomeOutput
toSomeOtherOutput
.
class SomeClient {
SomeInterface someInterface;
SomeOtherOutput someClientMethod(SomeInput someInput) {
SomeOtherOutput someOtherOutput = someInterface
.andThen(SomeOtherOutput::fromSomeOutput)
.apply(someInput);
return someOtherOutput;
}
}
class SomeOtherOutput {
static SomeOtherOutput fromSomeOutput(SomeOutput someOutput) {
// logic to build SomeOtherOutput from SomeOutput
SomeOtherOutput someOtherOutput = ...
return someOtherOutput;
}
}
Working with and producing different input and output types together
The following points are worth noting in the code below:
- The
compose
andandThen
methods fromFunction
is chained together to buildSomeOtherOutput
fromSomeOtherInput
.
Explanations for compose
and andThen
can be found in above examples.
class SomeClient {
SomeInterface someInterface;
SomeOtherOutput someClientMethod(SomeOtherInput someOtherInput) {
SomeOtherOutput someOtherOutput = someInterface
.compose(SomeOtherInput::toSomeInput)
.andThen(SomeOtherOutput::fromSomeOutput)
.apply(someOtherInput);
return someOtherOutput;
}
}
Summary
By using the Function
interface, you can easily extend your existing interfaces to provide additional features
with minimal changes.
The gist for the above example can be found here at this link.