Java interfaces can mark component boundaries, but their core capability is separation. Thus they can also isolate functions, which, in OO terms, are methods of an object. That means, through interfaces we can separate a method from all other methods of its object context and pass it around like any other object, without losing strong typing.
This Blog is about how you can simulate C function pointers in Java (before and after version 1.8).
Why Would We Need That?
Imagine a class that exposes setter-methods,
but you want to hide these setters from a related class the object is given to as parameter.
In no case the related class should be able to call one of the setters,
you want to expose just a getter.
This is called
information hiding,
an important tool to reduce complexity.
Example
Following example is about having Person
objects in memory
that should be checked for uniqueness using their getId()
method.
public class Person { private final String id; private String name; public Person(String id) { this.id = id; } public String getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
Mind that the id
field is immutable to guarantee
that it never changes in the lifetime of a Person
object.
Here is the UniqueRegistration
class that checks for uniqueness:
public class UniqueRegistration { private final Set<String> registry = new HashSet<>(); public void register(Person person) { final String key = person.getId(); if (key == null) throw new IllegalArgumentException("No id available: "+person); if (registry.contains(key)) throw new IllegalArgumentException("Already registered: "+person); registry.add(key); } }
Unfortunately this registry has full access to all methods of Person
,
it could call setName()
or getName()
or any other public
method.
Example application:
final Person john = new Person("John"); final Person jill = new Person("Jill"); final UniqueRegistration uniqueRegistration = new UniqueRegistration(); uniqueRegistration.register(john); uniqueRegistration.register(jill);
Now it is about hiding person.setName()
and getName()
from uniqueRegistration.register()
, just exposing the
getId
"function pointer".
Interfaces as Object Aspects
We create an abstraction of Person
that focuses on its identity and nothing else:
public interface Identifiable { String getId(); }
Let Person
implement that interface:
public class Person implements Identifiable { // ... implementation same as above }
And rewrite UniqueRegistration
to use the Identifiable
interface instead of Person
:
public class UniqueRegistration { // ... public void register(Identifiable identifiable) { final String key = identifiable.getId(); // ... implementation same as above } }
That's it! We separated the getId()
method from the Person
class,
but we did not tear the two apart. Still the method will be connected to its object and
read the correct id
field at runtime.
Nevertheless UniqueRegistration
will not see anything else than getId
.
Functions since Java 1.8
In Java 1.8, function pointers have been integrated into the language.
Here is an implementation of UniqueRegistration
that uses a built-in functional interface instead
of Identifiable
:
import java.util.function.Supplier; public class UniqueRegistration { // ... public void register(Supplier<String> identifiable) { final String key = identifiable.get(); // ... implementation same as above } }
The
Supplier
interface is a simple functional interface contained in the Java runtime library since 1.8. Its one-and-only function isget()
, without parameters:package java.util.function; @FunctionalInterface public interface Supplier<T> { T get(); }The
@FunctionalInterface
annotation is there to ensure that just one method (the "function") is inside the interface. The compiler would report an error when you put this annotation onto an interface with more than one method, wherebydefault
andstatic
methods do not count.
The example application now accesses the function through a Java syntax extension added in 1.8
(context::function
) like here:
final Person john = new Person("John"); final Person jill = new Person("Jill"); final UniqueRegistration uniqueRegistration = new UniqueRegistration(); uniqueRegistration.register(john::getId); uniqueRegistration.register(jill::getId);
Mind that Person
doesn't need to implement Identifiable
now any more.
We can remove that interface.
Built-in Functional Interfaces
Basic built-in functional interfaces (with their single functions) are:
T Supplier.get()
T = return-typevoid Consumer.accept(T parameter)
T = parameter-typeR Function.apply(T parameter)
R = return-type, T = parameter-typeboolean Predicate.test(T parameter)
always returns boolean, T = parameter-type
These four functional interfaces represent
archetypical method signatures, and they are generically typed to be reusable.
The Java runtime library contains many more variants of these four.
In case none satifies your needs, simply write your own @FunctionalInterface
.
Conclusion
Java interfaces are a powerful technique, well usable in separation concerns. Since Java 1.8, interfaces have become even better than abstract classes, because they also allow implementations.
Anyway we shouldn't hide any class behind an interface. Introducing interfaces just to be able to write class-oriented mock tests goes too far in my opinion. Such tests see any class as component, but in reality it isn't. There are not many classes that can exist without related classes, and only such a graph of classes should be regarded as component.
Keine Kommentare:
Kommentar veröffentlichen