Пустой интерфейс для объединения нескольких интерфейсов

20

Предположим, у вас есть два интерфейса:

interface Readable {
    public void read();
}

interface Writable {
    public void write();
}

В некоторых случаях реализующие объекты могут поддерживать только один из них, но во многих случаях реализации будут поддерживать оба интерфейса. Люди, которые используют интерфейсы, должны сделать что-то вроде:

// can't write to it without explicit casting
Readable myObject = new MyObject();

// can't read from it without explicit casting
Writable myObject = new MyObject();

// tight coupling to actual implementation
MyObject myObject = new MyObject();

Ни одна из этих опций не очень удобна, тем более, если учесть, что вы хотите это в качестве параметра метода.

Одним из решений было бы объявить интерфейс упаковки:

interface TheWholeShabam extends Readable, Writable {}

Но это имеет одну конкретную проблему: все реализации, которые поддерживают как Readable, так и Writable, должны реализовывать TheWholeShabam, если они хотят быть совместимыми с людьми, использующими интерфейс. Хотя он не предлагает ничего, кроме гарантированного наличия обоих интерфейсов.

Есть чистое решение этой проблемы или я должен пойти на интерфейс оболочки?

ОБНОВИТЬ

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

UPDATE2

(извлечено как ответ, так что проще комментировать)

Update3

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

UPDATE4

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

Предположим, у вас есть класс, который должен возвращать тип:

public <RW extends Readable & Writable> RW getItAll();

Если вы вызываете этот метод, общая RW определяется переменной, получающей объект, поэтому вам нужен способ описать эту переменную.

MyObject myObject = someInstance.getItAll();

Это будет работать, но еще раз связывает его с реализацией и может фактически генерировать исключения classcastexception во время выполнения (в зависимости от того, что возвращается).

Кроме того, если вам нужна переменная класса типа RW, вам нужно определить универсальный тип на уровне класса.

nablex
источник
5
Фраза "весь Шебанг"
Кевин Клайн
Это хороший вопрос, но я думаю, что использование Readable и «Writable» в качестве ваших примеров интерфейсов несколько
мутит
@Basueta Несмотря на то, что наименования были упрощены, читаемые и записываемые файлы действительно хорошо отражают мой сценарий использования. В некоторых случаях вы хотите только чтение, в некоторых случаях только запись и в удивительном количестве случаев чтение и запись.
Nablex
Я не могу вспомнить время, когда мне нужен был один поток, который можно было бы читать и записывать, и, судя по ответам / комментариям других людей, я не думаю, что я единственный. Я просто говорю, что было бы более полезно выбрать менее спорную пару интерфейсов ...
vaughandroid
@Baqueta Что-нибудь делать с пакетами java.nio *? Если вы придерживаетесь потоков, случай использования действительно ограничен местами, в которых вы будете использовать ByteArray * Stream.
nablex

Ответы:

19

Да, вы можете объявить ваш параметр метода как недостаточно определенный тип, который расширяет Readableи Writable:

public <RW extends Readable & Writable> void process(RW thing);

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

Килиан Фот
источник
2
Я бы предпочел второй подход Konrads здесь: process(Readable readThing, Writable writeThing)и если вы должны вызывать его, используя process(foo, foo).
Йоахим Зауэр
1
Не правильный ли синтаксис <RW extends Readable&Writable>?
Приходите с
1
@JoachimSauer: Почему вы предпочитаете подход, который легко ломается над тем, который просто уродлив? Если я вызываю process (foo, bar), а foo и bar разные, метод может завершиться ошибкой.
Майкл Шоу
@MichaelShaw: то, что я говорю, это то, что он не должен потерпеть неудачу, когда они разные объекты. Зачем это? Если это так, то я бы поспорил, что processделает несколько разных вещей и нарушает принцип единой ответственности.
Иоахим Зауэр
@JoachimSauer: Почему бы не потерпеть неудачу? for (i = 0; j <100; i ++) не такой полезный цикл, как для (i = 0; i <100; i ++). Цикл for выполняет чтение и запись в одну и ту же переменную, и это не нарушает SRP.
Майкл Шоу
12

Если есть место, где вам нужно myObject, Readableи Writableвы можете:

  • Реорганизовать это место? Чтение и письмо - это две разные вещи. Если метод выполняет оба действия, возможно, он не следует принципу единой ответственности.

  • Передайте myObjectдважды, как a Readableи как a Writable(два аргумента). Что заботит метод, является ли он одним и тем же объектом?

Конрад Моравский
источник
1
Это может работать, когда вы используете его в качестве аргумента, и это отдельная проблема. Однако иногда вы действительно хотите, чтобы объект был доступен для чтения и записи одновременно (например, по той же причине, по которой вы хотите использовать ByteArrayOutputStream)
nablex
Как придешь? Выходные потоки пишут, как видно из названия - это входные потоки, которые могут читать. То же самое в C # - есть StreamWriterпротив StreamReader(и многие другие)
Конрад Моравский
Вы пишете в ByteArrayOutputStream, чтобы получить байты (toByteArray ()). Это эквивалентно записи + чтение. Реальная функциональность интерфейсов во многом аналогична, но в более общем виде. Иногда вам захочется только читать или писать, иногда вам захочется и того, и другого. Другой пример - ByteBuffer.
Nablex
2
В этот момент я немного отшатнулся, но после минутного размышления это не кажется плохой идеей. Вы не только отделяете чтение и запись, но и делаете более гибкую функцию и сокращаете объем изменяемого состояния ввода.
Phoshi
2
@Phoshi Проблема в том, что проблемы не всегда разные. Иногда вам нужен объект, который может и читать, и писать, и вам нужна гарантия того, что это один и тот же объект (например, ByteArrayOutputStream, ByteBuffer, ...)
nablex
4

Ни один из ответов в настоящее время не относится к ситуации, когда вам не нужны читаемые или записываемые, но оба . Вам нужны гарантии, что при записи в A вы можете читать эти данные обратно из A, а не записывать в A и читать из B, и просто надеяться, что это на самом деле один и тот же объект. Вариантов использования много, например, везде, где вы бы использовали ByteBuffer.

Во всяком случае, я почти закончил модуль, над которым я работаю, и в настоящее время я выбрал интерфейс оболочки:

interface Container extends Readable, Writable {}

Теперь вы можете по крайней мере сделать:

Container container = IOUtils.newContainer();
container.write("something".getBytes());
System.out.println(IOUtils.toString(container));

Мои собственные реализации контейнера (в настоящее время 3) реализуют Контейнер в отличие от отдельных интерфейсов, но если кто-то забудет об этом в реализации, IOUtils предоставляет служебный метод:

Readable myReadable = ...;
// assuming myReadable is also Writable you can do this:
Container container = IOUtils.toByteContainer(myReadable);

Я знаю, что это не оптимальное решение, но это все еще лучший выход на данный момент, потому что Контейнер все еще довольно большой сценарий использования.

nablex
источник
1
Я думаю, что это абсолютно нормально. Лучше, чем некоторые другие подходы, предложенные в других ответах.
Том Андерсон
0

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

if (myObject instanceof Readable)  {
    Readable  r = (Readable) myObject;
    readThisReadable( r );
}

В простом случае, я не думаю, что вы можете улучшить это. Но если после записи его readThisReadableзахочется записать в другой файл для чтения, это будет неловко.

Так что я бы, наверное, пошел с этим:

interface TheWholeShabam  {
    public boolean  isReadable();
    public boolean  isWriteable();
    public void     read();
    public void     write();
}

Принимая это как параметр, readThisReadableтеперь readThisWholeShabamможно обрабатывать любой класс, который реализует TheWholeShabam, а не только MyObject. И это может написать это, если это доступно для записи, и не написать это, если это не так. (У нас есть настоящий «Полиморфизм».)

Таким образом, первый набор кода становится:

TheWholeShabam  myObject = ...;
if (myObject.isReadable()
    readThisWholeShebam( myObject );

И вы можете сохранить строку здесь, сделав readThisWholeShebam (), чтобы сделать проверку читабельности.

Это означает, что наш прежний Readable-only должен реализовывать isWriteable () (возвращающий false ) и write () (ничего не делая), но теперь он может идти во все виды мест, которые он не мог найти раньше, и весь код, который обрабатывает TheWholeShabam объекты будут иметь дело с этим без каких-либо дополнительных усилий с нашей стороны.

Еще одна вещь: если вы можете обработать вызов read () в классе, который не читает, и вызов write () в классе, который не пишет, не удаляя что-то, вы можете пропустить isReadable () и isWriteable () методы. Это был бы самый элегантный способ справиться с этим - если он работает.

RalphChapin
источник