Есть ли обходной путь для интерфейсов композиции и маркеров?

Я регулярно сталкиваюсь со следующей проблемой. У меня есть какой-то интерфейс маркеров (для простоты воспользуемся java.io.Serializable ) и несколько оболочек (адаптер, декоратор, прокси, ...). Но когда вы переносите экземпляр Serializable в другой экземпляр (который не является сериализуемым), вы теряете функциональность. Та же проблема возникает с java.util.RandomAccess, который может быть реализован реализациями List. Есть ли хороший способ ООП справиться с этим?

6
задан whiskeysierra 10 August 2010 в 07:43
поделиться

4 ответа

Вот недавнее обсуждение списка рассылки Guava - мой ответ затрагивает этот довольно фундаментальный вопрос.

http://groups.google.com/group/guava-discuss/browse_thread/thread/2d422600e7f87367/1e6c6a7b41c87aac

Суть такова: Не используйте интерфейсы маркеров, если вы ожидаете, что ваши объекты будут заключены в оболочку . (Ну, это довольно общий вопрос - как сделать , что вы знаете, что ваш объект не будет упакован клиентом?)

Например, ArrayList . Очевидно, он реализует RandomAccess . Затем вы решаете создать оболочку для объектов List . Ой! Теперь, когда вы обертываете, вы должны проверить обернутый объект, и если это RandomAccess, созданная оболочка должна также реализовывать RandomAccess!

Это работает "отлично" ... если у вас только один интерфейс маркера! Но что, если обернутый объект может быть сериализуемым? Что, если он, скажем, «Неизменяемый» (при условии, что у вас есть тип для обозначения этого)? Или синхронно? (С таким же предположением).

Как я также отмечал в своем ответе на список рассылки, этот недостаток дизайна также проявляется в старом добром пакете java.io . Допустим, у вас есть метод, принимающий InputStream . Вы будете читать прямо с него? Что, если это дорогостоящий поток, и никто не позаботился обернуть его в BufferedInputStream за вас? О, это просто! Вы просто проверяете экземпляр потока BufferedInputStream , а если нет, вы обертываете его сами! Но нет. У потока может быть буферизация где-то ниже по цепочке, но вы можете получить ее обертку, , которая не является экземпляром BufferedInputStream . Таким образом, информация о том, что «этот поток буферизован», теряется (и вам, возможно, придется пессимистично тратить память, чтобы буферизовать ее снова).

Если вы хотите делать что-то правильно , просто моделируйте возможности как объекты. Рассмотрим:

interface YourType {
  Set<Capability> myCapabilities();
}

enum Capability {
  SERIALIAZABLE,
  SYNCHRONOUS,
  IMMUTABLE,
  BUFFERED //whatever - hey, this is just an example, 
           //don't throw everything in of course!
}

Изменить: Следует отметить, что я использую перечисление только для удобства. Могут быть интерфейс Capability и открытый набор объектов, реализующих его (возможно, несколько перечислений).

Таким образом, когда вы оборачиваете объект из них, вы получаете Набор возможностей, и вы можете легко решить , какие возможности сохранить, какие удалить, какие добавить .

У этого есть , очевидно, есть свои недостатки, поэтому его следует использовать только в тех случаях, когда вы действительно чувствуете боль от сокрытия возможностей оболочек, выраженных в виде интерфейсов маркеров. Например, предположим, что вы пишете фрагмент кода, который принимает список, но он должен иметь свойства RandomAccess И Serializable.При обычном подходе это легко выразить:

<T extends List<Integer> & RandomAccess & Serializable> void method(T list) { ... }

Но в подходе, который я описываю, все, что вы можете сделать, это:

void method(YourType object) {
  Preconditions.checkArgument(object.getCapabilities().contains(SERIALIZABLE));
  Preconditions.checkArgument(object.getCapabilities().contains(RANDOM_ACCESS));
  ...
}

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

Изменить: Другой недостаток заключается в том, что без явного типа для каждой возможности у нас нет естественного места для размещения методов, которые выражают то, что предлагает эта возможность. Это не слишком важно в данном обсуждении, поскольку мы говорим об интерфейсах marker , то есть о возможностях, которые не выражаются через дополнительные методы, но я упоминаю об этом для полноты.

PS: кстати, если вы просмотрите код коллекций Guava, вы действительно почувствуете боль, которую вызывает эта проблема. Да, некоторые хорошие люди пытаются скрыть это за красивыми абстракциями, но, тем не менее, основная проблема болезненна.

7
ответ дан 8 December 2019 в 17:17
поделиться

Если все интересующие вас интерфейсы являются маркерными, вы можете заставить все ваши классы-обертки реализовать интерфейс

public interface Wrapper {
    boolean isWrapperFor(Class<?> iface);
}

реализация которого будет выглядеть так:

public boolean isWrapperFor(Class<?> cls) {
    if (wrappedObj instanceof Wrapper) {
        return ((Wrapper)wrappedObj).isWrapperFor(cls);
    }
    return cls.isInstance(wrappedObj);
}

Вот как это сделано в java.sql.Wrapper. Если интерфейс не просто маркер, а действительно имеет некоторую функциональность, вы можете добавить метод для разворачивания:

<T> T unwrap(java.lang.Class<T> cls)
5
ответ дан 8 December 2019 в 17:17
поделиться

Для подобных RandomAccess вы мало что можете сделать. Конечно, вы можете выполнить проверку instanceof и создать экземпляр соответствующего класса. Количество классов растет экспоненциально с маркерами (хотя вы можете использовать java.lang.reflect.Proxy), и ваш метод создания должен знать обо всех маркерах когда-либо.

Serializable не так уж плох. Если класс перенаправления реализует Serializable, то все будет сериализуемо, если целевой класс Serializable и нет, если нет.

1
ответ дан 8 December 2019 в 17:17
поделиться

Есть несколько альтернатив, но ни одна из них не очень хороша

  1. Заставьте оболочку реализовывать интерфейс, если это известно во время компиляции, если обернутый объект также реализует интерфейс. Для создания оболочки можно использовать фабричный метод, если до времени выполнения неизвестно, будет ли обернутый объект реализовывать интерфейс. Это означает, что у вас есть отдельные классы-оболочки для возможных комбинаций реализованных интерфейсов. (Для одного интерфейса вам понадобятся 2 обертки, одна с и одна без. Для 2 интерфейсов, 4 обертки и так далее.)

  2. Предоставьте обернутые объекты из оболочки, чтобы клиенты могли пройти по цепочке и протестировать каждый объект в цепочке на предмет наличия интерфейса, используя instanceof . Это нарушает инкапсуляцию.

  3. Имейте специальный метод для получения интерфейса, реализованный как оболочкой, так и обернутым объектом. Например. как SomeInterface () . Оболочка делегирует обернутый объект или создает прокси вокруг обернутого объекта, чтобы сохранить инкапсуляцию.

  4. Создайте один класс-оболочку для каждого интерфейса - оболочка реализована как обычно - она ​​реализует интерфейс и делегирует другой реализации этого интерфейса. Обернутый объект может реализовывать несколько интерфейсов, поэтому несколько экземпляров оболочки объединяются в один логический экземпляр с использованием динамического прокси для делегирования методов интерфейса, реализованных прокси, соответствующему экземпляру оболочки. Необходимо, чтобы набор интерфейсов, реализуемых прокси, не имел общих сигнатур методов.

Microsoft встроила агрегацию ( Википедию ) в свою модель компонентных объектов (COM). Кажется, что он не используется большинством, но приводит к значительной сложности для разработчиков COM-объектов, поскольку существуют правила, которых должен придерживаться каждый объект.Обернутые объекты инкапсулируются за счет того, что обернутые объекты знают о том, что они являются оболочками, и должны поддерживать указатель на оболочку, которая используется при реализации QueryInterface (свободно instanceof ) для открытых общедоступных интерфейсов - завернутый объект возвращает интерфейс, реализованный в оболочке, а не его собственная реализация.

Я не видел чистого, простого для понимания / реализации и правильно инкапсулированного решения этой проблемы. Агрегирование COM работает и обеспечивает полную инкапсуляцию, но это плата, которую вы платите за каждый отдельный объект, который вы реализуете, даже если он никогда не используется в агрегате.

1
ответ дан 8 December 2019 в 17:17
поделиться
Другие вопросы по тегам:

Похожие вопросы: