Ограничение контейнеров с помощью дженериков - PullRequest
1 голос
/ 19 января 2011

В настоящее время я нахожусь в процессе разработки иерархии классов для проекта моделирования. Это будет имитация дискретного события дерева Element экземпляров. Для краткости (и потому что меня интересует только общая часть проблемы), я приведу здесь только наброски классов / интерфейсов, поэтому синтаксис не будет идеальным. Чтобы начать с чего-то, элемент может выглядеть так:

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); }
}

Определение Position на самом деле не имеет значения, и то, что должны делать другие методы, самоочевидно, я надеюсь. Чтобы сделать вещи немного интереснее, мы добавляем в пул еще один интерфейс:

public abstract class Solid extends Element {
    public Size getSize();
}

Опять же, точное значение Size не имеет значения, просто предполагается, что Solid - это физический объект, который заполняет пространство в моделируемом мире. Теперь, когда у нас есть твердый объект, мы можем захотеть сложить их друг на друга, так что вот Stack:

public abstract class Stack extends Solid {
    public void add(Solid s); // <- trouble ahead!
}

И вот тут начинается беда. Я действительно хочу, чтобы в Stack были добавлены только экземпляры Solid. Это потому, что трудно сложить объекты без размера, поэтому Stack потребуется что-то с Size. Поэтому я переделываю свой Element, чтобы выразить это:

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);
}

Мое намерение здесь состоит в том, чтобы выразить, что Element может иметь ограничение на то, какие (под) типы Element могут содержать. Пока это работает и все. Но прямо сейчас я почувствовал необходимость ограничения для контейнера для Element. Как это выглядит?

public interface Element<E extends Element<?,?>, C extends Element<?, ?>>

Или это выглядит так:

public interface Element<E extends Element<?,C>, C extends Element<E, ?>>

Я начинаю чувствовать себя немного нечетко по этому поводу, и в игре есть еще кое-что:

  • должны быть «Порты» (Input и Output), которые переносят Элементы из одного контейнера в другой. Мне тоже придется применить магию дженериков.
  • В моем списке задач есть графический редактор для моделей. Поэтому я должен сделать эти проверки доступными также во время выполнения. Поэтому я думаю, что будут литералы типов и что-то вроде public boolean canAdd(Element<?, ?> e), на которое редактор будет полагаться. Поскольку этот метод очень важен, я бы хотел, чтобы он был final методом класса Element, поэтому ни один пьяный разработчик не может ошибиться в 4 часа утра.
  • Было бы здорово, если бы подклассы Element могли бы решить для себя, какой класс Collection они используют, потому что некоторые из LinkedList или что-то вроде идеально подходят.

Итак, вот мои вопросы:

  • Я заново изобретаю колесо? Какая библиотека делает это для меня?
  • Могу ли я решить эту проблему с помощью дженериков? (если ответ да: некоторые советы по реализации очень приветствуются)
  • Может быть, это немного перегружено?
  • Есть ли лучший заголовок или теги для этого вопроса? : -)
  • (просто оставьте здесь свое мнение)

Ответы [ 4 ]

1 голос
/ 20 января 2011

Я все еще немного неясен относительно того, что вы пытаетесь сделать, но если вы хотите довести это до логического завершения, я вижу что-то вроде этого:

/**
@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();
}

Это выражает (я думаю) все ваши ограничения: мой родитель должен быть чем-то, что может сделать меня ребенком, а мои дети должны быть тем, что может сделать меня родителем.

Реализация будет включать:

/** 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> {…}

У меня сейчас нет открытой Java IDE, поэтому я не могу подтвердить, что это на самом деле скомпилируется - у меня такое чувство, что компилятор будет жаловаться на эти вопросительные знаки, ипопытайтесь перетащить вас в нечто вроде

class Agglomeration extends Element<? 
    extends Element<? 
        extends Element<? 
            extends...

Однако вы можете обойти это, если разделите Parent и Child на отдельные интерфейсы (один с getChildren() и один с getParent())таким образом, Agglomeration реализует только первое, а Solid реализует только второе, в то время как Stack реализует оба.

Это может быть больше проблем, чем стоит, но поработайте с ним некоторое время и посмотрите, гдеэто займет у вас:)

1 голос
/ 20 января 2011

Просто несколько случайных замечаний:

Вместо Collection<E> вы можете или не хотите Collection<? extends E>.

Я совершенно уверен, что вы хотите начать с Element<E extends Element<**E**>, так как выхочу захватить тип этого (просто посмотрите на java.lang.Enum).Поэтому вам может понадобиться что-то вроде Element<E extends Element<E,C>, C extends Element<C, ?>> Это может быть слишком сложно, как для вас, так и для компилятора (возможно, это поправилось, но некоторое время назад у меня возникли действительно странные ошибки при попытке слишком сильно).

Может быть, методы Element.add и Stack.add на самом деле разные методы?Добавление дочерних элементов в Stack означает их сортировку, добавление их к произвольному элементу может означать что-то совершенно другое?

Было бы здорово, если бы подклассы Element могли решить для себя, какой класс Collection они используют,потому что из какого-то из LinkedList или чего-то еще кажется идеальным.

Такой метод, как Collection<C> makeCollection(), вызывается в ctor базового класса.Тем не менее, использование LinkedList в большинстве случаев является неправильным (из-за его плохого места в пространстве и большого объема служебной информации это делает его настолько медленным, что его преимущества могут быть проигнорированы).

Поэтому мне придется выполнить эти проверкидоступно и во время выполнения.

Таким образом, вам необходимо сконструировать экземпляры с использованием параметра Class<E>.

return Collections.unmodifiableSet (children);

Таким образом, вы возвращаете детей в живую.Попробуйте вместо этого использовать com.google.common.collect.ImmutableSet.copyOf (children).

0 голосов
/ 20 января 2011

Глядя на эту строку:

public interface Element<E extends Element<?,?>, C extends Element<?, ?>>

заставил меня почувствовать, что да, это, конечно, чрезмерно спроектировано.

Конечно, есть много выходов из ситуации, но мне кажется подходящим использование шаблона Decorator вместо наследования (не везде, только там, где вы чувствуете, что оригинальный дизайн изнашивается) и, возможно, таким образом, дженерики тоже подойдут лучше.

0 голосов
/ 20 января 2011

Я основываю этот ответ на своем понимании вашего дизайна.Если я не прав в моем понимании, пожалуйста, дайте мне знать!

Основная цель обобщений - обеспечить безопасность типов, а не объектные отношения (насколько я знаю, во всяком случае).Кажется, что вы можете улучшить взаимосвязь между объектами, обеспечив безопасность типов, но в конечном итоге цель состоит в том, чтобы обеспечить безопасность типов (чтобы вы не вставляли что-то там, где оно не принадлежит).Ваш абстрактный класс, вы предоставили метод с именем add.Это означает, что все, что расширяет этот абстрактный класс, должно реализовывать add.Позже вы налагаете ограничение на add, говоря, что вы можете только add Solid объектов (который является подклассом Element).Но при этом вы нарушаете контракт абстрактного класса, который говорит, что Element (или его подкласс) может быть добавлен к набору.

Так что есть два варианта.Вы также должны переосмыслить свой дизайн (возможно, add не должен быть представлен в абстрактном классе?).Другой вариант - вызвать исключение времени выполнения (возможно, UnsupportedOperationException) при попытке add объекта без размера, а также предоставить метод проверки (например, canAdd), который позволяет разработчику узнать, является ли конкретный объектможно добавить.

...