-
Notifications
You must be signed in to change notification settings - Fork 32
Fat Interfaces
Unlike in many other APIs, the AutomataLib interfaces were chosen to be rather "fat". For example, the DeterministicTransitionSystem
(source, javadoc) interface declares (among others) the following three methods:
-
T getTransition(S state, I input)
, retrieving the transition (if existent) for a given input -
S getSuccessor(T transition)
, retrieving the successor state from an existing transition -
S getSuccessor(S state, I input)
, retrieving the successor state (if existent) for a given input
It is pretty obvious that the last of the three methods is redundant, from a strict perspective. It could, for example, be realized as follows:
S getSuccessor(S state, I input) {
T transition = getTransition(state, input);
return (transition != null) ? getSuccessor(transition) : null;
}
Removing this method would save a few lines in DeterministicTransitionSystem
implementations. However, there are two reasons why we decided against that.
The default implementation should always work (provided that the other methods are correctly implemented), but might be unnecessary complicated for some cases. Consider the case of a DeterministicAcceptorTS
(source, javadoc): in an acceptor, a transition is fully characterized by its target state. The transition type parameter T
therefore is bound to be the same type as S
, as the difference between a transition and its target state disappears. The above method could therefore be optimized:
S getSuccessor(S state, I input) {
return getTransition(state, input); // getTransition(S,I) actually returns S!
}
We now turned a 2-line-method into a one-liner, which is not very impressive. However, one can think of arbitrarily more complex cases. Even though maximizing efficiency was not a [design goal](Design Goals) of AutomataLib, it should still allow such optimizations.
One of the main [design goals](Design Goals) of AutomataLib is to provide a very precise mapping between abstract (mathematical) concepts and Java classes and interfaces. This can only be accomplished by excessive use of multiple interface implementation. The interface DeterministicTransitionSystem
actually extends SimpleDTS
, which provides an abstraction on a level that hides transitions, i.e., only in terms of input symbols and succesor states. The getSuccessor(S, I)
is in fact declared in the SimpleDTS
interface, so we can get rid of it only by removing the extension of SimpleDTS
. This, on the other hand, would be a conceptual violation, as the latter is an abstraction of DeterministicTransitionSystem
.
As pointed out above, the chosen approach leaves programmers with (sometimes considerably) more work implementing interface methods. A standard solution to this issue is to declare an abstract base class implementing the considered interface which provides a "default" implementation for some of the interface methods. Concrete implementations then extend this abstract base class instead of implementing the interface directly. However, as Java (for good reasons) does not support multiple inheritance when it comes to classes, and AutomataLib on the other hand heavily makes use of implementing multiple interfaces, this approach can cover only a very limited number of cases.
Therefore, in AutomataLib the abstract base classes do not realize the default implementations directly in the method implementations, but in a separate static
method with the same name but exactly one additional parameter: a substitute for the this
reference. The following listing illustrates this for the above example:
// Abstract base class
public abstract class AbstractDTS<S,I,T> implements DeterministicTransitionSystem<S,I,T> {
// ...
// static helper method
public static <S,I,T> // static method - type arguments must be redeclared
S getSuccessor(DeterministicTransitionSystem<S,I,T> $this, S state, I input) {
T transition = $this.getTransition(state, input);
return (transition != null) ? $this.getSuccessor(transition) : null;
}
// ...
// instance method
S getSuccessor(S state, I input) {
return getSuccessor(this, state, input); // delegate to static helper method
}
// ...
}
Note that in Java, $this
is in fact a legal identifier. The special name was chosen to signal - even though the method has to be public
- it is never intended for normal use, but just for implementing instance methods, and the first argument should never be anything but this
. In all other cases, the respective instance method (which might be implemented in a more specific, more efficient way) should be called.
Also note that the type of the $this
parameter is the one of the interface, not of the abstract class. That way, other concrete implementations of DeterministicTransitionSystem
which - for whatever reason - do not extend AbstractDTS
can still realize getSuccessor(S,I)
by just delegating to the same static helper method in AbstractDTS
. This reduces the code duplication for default implementations to a single delegate call. As the method is static
, we can safely rely on the JIT compiler eliminating any overhead.