Restricting Containers with Generics
I'm currently in the process of designing the class hierarchy for a simulation project. It is going to be a discrete-event simulation of a tree of Element
instances. For brevity (and because I'm only interested in the generics part of the problem) I'll only present sketches of the classes / interfaces here, so the syntax will not be perfect. To have something to start with, an element might look like this:
public abstract class Element {
private Set<Element> children = new HashSet<Element>();
public Element getContainer();
public Collection<Element> getChildren() {
return Collections.unmodifiableSet(children);
}
public Position getPosition();
protected void add(Element e) { children.add(e); }
protected void remove(Element e) {children.remove(e); }开发者_如何学C
}
The definition of Position
does not really matter, and what the other methods are supposed to do is self-explanatory I hope. To make things mildly interesting we throw another interface into the pool:
public abstract class Solid extends Element {
public Size getSize();
}
Again, the exact meaning of Size
does not matter, a Solid
is just supposed to be a physical object which fills space in the simulated world. Now that we have solid object we may want to stack them on top of each other, so here's a Stack
:
public abstract class Stack extends Solid {
public void add(Solid s); // <- trouble ahead!
}
And here's where the trouble starts. I really want only instances of Solid
to be added to the Stack
. That's because it's difficult to stack objects that have no size, so the Stack
will need something which has a Size
. Therefore I rework my Element
to allow expressing this:
public abstract class Element<E extends Element<?>> {
private final Set<E> children = new HashSet<E>();
public Element<?> getContainer();
public Collection<E> getChildren();
public Position getPosition();
public void add(E e);
public void remove(E e);
}
My intention here is to express that an Element
can have a restriction on which (sub-)types of Element
it may contain. So far this works and all. But right now I felt the need to have an restriction on the container of an Element
. What does that look like?
public interface Element<E extends Element<?,?>, C extends Element<?, ?>>
Or does it go more like this:
public interface Element<E extends Element<?,C>, C extends Element<E, ?>>
I'm starting to feel a bit fuzzy about this, and there's more to the game:
- there have to be "Ports" (
Input
andOutput
) which transfer Elements from one container to another. I'll have to throw some generics magic at these, too. - there's a GUI editor for the models on my todo list. So I'll have to make these checks available at runtime, too. So I guess there will be type literals and something like
public boolean canAdd(Element<?, ?> e)
the editor will have to rely on. Because this method is so important I'd like to have it anfinal
method of theElement
class, so no drunken developer can get it wrong at 4am. - It would be cool if subclasses of
Element
could decide for themselfs whichCollection
class they are using, because from some of the aLinkedList
or whatever seems a perfect fit.
So here are my questions:
- Am I re-inventing the wheel? Which library does that for me?
- Can and should I tackle this with generics? (if the answer is yes: some hints on the implementation are very welcome)
- Could this be a bit over-engineered?
- Are there a better title or tags for this question? :-)
- (just drop your opinion here)
I'm still a bit fuzzy as to what you're trying to do, but if you want to run with this all the way to its logical conclusion, I'm seeing something like this:
/**
@param P parent
@param C child
@param S self
*/
interface Element<P extends Element<?, S, P>,
C extends Element<S, ?, C> ,
S extends Element<P, C, S>> {
public P getParent();
public Collection<C> getChildren();
}
That expresses (I think) all your constraints: my parent must be something that can have me as a child, and my children must be something that can have me as a parent.
Implementations would include:
/** An agglomeration of stacks: no parent */
class Agglomeration extends Element<?, Stack, Agglomeration> {…}
/** A stack of solids */
class Stack extends Element<Agglomeration, Solid, Stack> {…}
/** A solid in a stack: no children */
class Solid extends Element<Stack, ?, Solid> {…}
I don't have a Java IDE open right now, though, so I can't confirm this will actually compile -- I have a feeling that the compiler will complain about those question marks, and try to drag you into something like
class Agglomeration extends Element<?
extends Element<?
extends Element<?
extends...
You can get around that, though, if you separate Parent
and Child
into separate interfaces (one with getChildren()
and one with getParent()
), so Agglomeration
only implements the first and Solid
only implements the second, while Stack
implements both.
It might be more trouble than it's worth, but run with it for a while and see where it takes you. :)
Just some random remarks:
Instead of Collection<E>
you may or may not want Collection<? extends E>
.
I'm quite sure, you want start with Element<E extends Element<**E**>
since you want to capture the type of this (just look at java.lang.Enum). So you need maybe something like Element<E extends Element<E,C>, C extends Element<C, ?>>
It may get too complicated, both for you and for the compiler (maybe it's got better, but some time ago I got some really strange errors when trying too hard).
Maybe the methods Element.add and Stack.add are in fact different methods? Adding children to Stack means sort of stacking them, adding them to an arbitrary Element may mean something completely different?
It would be cool if subclasses of Element could decide for themselfs which Collection class they are using, because from some of the a LinkedList or whatever seems a perfect fit.
A method like Collection<C> makeCollection()
called in ctor of the base class could do. However, using LinkedList is wrong most of the time (its bad space locality and high space overhead makes it so slow so that it's advantages may be ignored).
So I'll have to make these checks available at runtime, too.
So you need to construct the instances using Class<E>
parameter.
return Collections.unmodifiableSet(children);
This way you're returning a live-view of children. Consider using com.google.common.collect.ImmutableSet.copyOf(children) instead.
I'm basing this answer on my understanding of your design. If I'm wrong in my understanding, please let me know!
The main purpose of generics is to ensure type-safety and not object relationships (as far as I know, anyway). It seems like you can enhance the relationship between objects by ensuring type-safety, but ultimately the purpose is to ensure type-safety (so that you're not sticking something where it doesn't belong).
In your abstract class, you've provided a method called add
. This means that anything that extends this abstract class must implement add
. Now later, you're imposing a restriction on add
saying that you can only add
Solid
objects (which is a subclass of Element
). But in doing that, you're violating the contract of the abstract class, which says that an Element
(or a subclass thereof) can be added to the set.
So there are two options. You either have to rethink your design (maybe add
should not be provided in the abstract class?). Your other option is to throw a runtime exception (UnsupportedOperationException
perhaps) when you try to add
an object that has no size, and also provide an inspection method (like canAdd
) that lets the developer know if the specific object can be added.
Looking at this line:
public interface Element<E extends Element<?,?>, C extends Element<?, ?>>
made me feel that yes, this is certainly over-engineered.
Of course there are many ways out of the situation but the one that sounds appropiate to me is to use the Decorator pattern instead of inheritance (not everywhere, only where you feel the original design is fraying) and maybe that way generics will fit in better too.
精彩评论