Am I using a feature of Java 8 or misusing it?
Refer the code and explanation below to know as to why it was chosen to be like this.
p
First, if it works, and it does what you want to do, and there's no danger of something breaking in the future, it doesn't make sense to say you're misusing it. After all, it got the job done, right? Features like default methods and static methods were added to interfaces with particular goals in mind, but if they help you achieve other goals, either that's creative use of a new feature or a gross and dirty hack. :-) To a certain extent it's a matter of taste.
With that perspective in mind, what I look for in APIs, and what I try to do when designing APIs, is to distinguish clients of an API from implementors of an API. A typical client, or user, of an API gets a reference of some interface type from somewhere and calls methods on it to make stuff happen. An implementor provides implementations for methods defined in interfaces, overrides methods, and (if subclassing) calls superclass methods. Often, the methods intended to be called by clients are different from those intended to be called from subclasses.
It seems to me that these concepts are being mixed in the Drawable
interface. Certainly, clients of a Drawable
will do things like call the draw
or drawDepthPass
methods on them. Great. But looking at the default implementation of drawDepthPass
, it gets some information using isTessellated
and isInstanced
methods, and then uses these to choose a Program and call methods on it in a particular way. It's fine for these bits of logic to be encapsulated within a method, but in order for it to be done in a default method, the getters have to be forced into the public interface.
I might be wrong about your model, of course, but it seems to me that this kind of logic is more suited for an abstract superclass and subclasser relationship. The abstract superclass implements some logic that handles all Drawables, but it negotiates with the particular Drawable implementations with methods like isTesselated
or isInstanced
. In an abstract superclass, these would be protected methods that subclasses are required to implement. By putting this logic into default methods of an interface, all of these have to be public, which clutters up the client interface. The other methods that seem similar are getDataMode
, isShadowReceiver
, and isShadowCaster
. Are clients expected to call these, or are they logically internal to the implementation?
What this highlights is that, despite the addition of default methods and static methods, interfaces are still oriented toward clients, and less toward supporting subclasses. The reasons are as follows:
Another issue I note with the Drawable
interface family is that it uses the ability of default methods to override each other to allow some simple mixins to the implementation classes like Box
. It is kind of neat that you can just say implements TessellatedDrawable
and avoid the pesky overriding of the isTesselated
method! The problem is that this now becomes part of the implementation class's type. Is it useful for the client to know that a Box
is also a TessellatedDrawable
? Or is this just a scheme for making the internal implementation cleaner? If it's the latter, it might be preferable these mixin interfaces like TessellatedDrawable
and InstancedDrawable
not be public interfaces (i.e., package private).
Note also that this approach clutters up the type hierarchy, which can make code more confusing to navigate. Usually a new type is a new concept, but it seems heavyweight to have interfaces that merely define default methods returning boolean constants.
A further point in this vein. Again, I don't know your model, but the characteristics being mixed in here are very simple: they're just boolean constants. If there's ever a Drawable
implementation that, say, starts off not being instanced and later can become instanced, it can't use these mixin interfaces. The default implementations are really quite restricted in what they can do. They can't call private methods or inspect fields of an implementation class, so their use is quite limited. Using interfaces this way is almost like using them as marker interfaces, with a tiny addition of being able to call a method to get the characteristic, instead of using instanceof
. There doesn't seem to be much use beyond this.
The static methods in the Drawable
interface seem mostly reasonable. They're utilities that seem client-oriented, and they provide reasonable aggregations of logic provided by the public instance methods.
Finally, there are a few points about the model that seem odd, though they're not directly related to the use of default and static methods.
It seems like a Drawable
has-a Program
, as there are instance methods compileProgram
, getProgram
, and delete
. Yet the drawDepthPass
and similar methods require the client to pass in two programs, one of which is selected depending on the result of the boolean getters. It's not clear to me where the caller is supposed to choose the right Programs.
Something similar is going on with the drawAll
methods and the offset
value. It seems like in a list of Drawables, they have to be drawn using particular offsets based on each Drawable's data size. Yet what is apparently the most fundamental method, draw
, requires the caller to pass in an offset. This seems like a big responsibility to push onto the caller. So perhaps the offset stuff really belongs within the implementation as well.
There are a couple methods that take a List of drawables and call stream()
and then forEach()
or forEachOrdered()
. This isn't necessary, as List
has a forEach
method on it, inherited from Iterable
.
I think it's great to explore how this new stuff can be used. It's new enough that a commonly accepted style hasn't yet emerged. Experiments like this, and this discussion, help to develop that style. On the other hand, we also need to be careful not to use these shiny new features just because they're new and shiny.