Порожній інтерфейс для об'єднання декількох інтерфейсів


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 {}

Але в цьому є одна конкретна проблема: всі реалізації, які підтримують і читабельні, і записувані, повинні реалізувати TheWholeShabam, якщо вони хочуть бути сумісними з людьми, які використовують інтерфейс. Хоча він не пропонує нічого, крім гарантованої присутності обох інтерфейсів.

Чи є чітке рішення цієї проблеми чи мені слід перейти до інтерфейсу обгортки?

ОНОВЛЕННЯ

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

ОНОВЛЕННЯ2

(витягується як відповідь, тому простіше коментувати)

ОНОВЛЕННЯ3

Будь ласка, майте на увазі, що основним шаблоном для цього є не потоки (хоча вони теж повинні підтримуватися). Потоки дуже чітко розмежовують вхід і вихід, і чіткий розподіл обов'язків. Скоріше, придумайте щось на зразок байт-буфера, де вам потрібен один об'єкт, з якого ви можете писати та читати, один об’єкт, який має до нього дуже специфічний стан. Ці об'єкти існують, оскільки вони дуже корисні для таких речей, як асинхронний введення / виведення, кодування, ...

ОНОВЛЕННЯ4

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

Припустимо, у вас є клас, який повинен повернути тип:

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

Якщо ви викликаєте цей метод, загальний RW визначається змінною, що приймає об'єкт, тому вам потрібен спосіб описати цей var.

MyObject myObject = someInstance.getItAll();

Це буде працювати, але ще раз прив’язує його до реалізації та може фактично кидати classcastexceptions під час виконання (залежно від того, що повертається).

Крім того, якщо ви хочете змінну класу типу RW, вам потрібно визначити загальну на рівні класу.


5
Фраза - "цілий шебанг"
кевін клайн

Це гарне запитання, але я думаю, що використання "Зчитуваного" та "Записуваного" в якості вашого прикладу інтерфейсів дещо
затуманює

@Basueta Хоча назву було спрощено, читабельна та записана насправді досить добре передає мою корисну скриньку. У деяких випадках ви хочете лише читати, в деяких випадках пишіть лише, а в деяких випадках читайте і записуйте.
nablex

Я не можу придумати час, коли мені знадобився єдиний потік, який читається і записується, і судячи з відповідей / коментарів інших людей, я не думаю, що я єдиний. Я просто кажу, що може бути корисніше вибрати менш суперечливу пару інтерфейсів ...
vaughandroid

@Baqueta Що-небудь спільного з пакетами java.nio *? Якщо ви дотримуєтесь потоків, то використання справді обмежене місцями, де ви б використовували ByteArray * Stream.
nablex

Відповіді:


19

Так, ви можете оголосити параметр методу як не вказаний тип, який розширює Readableі Writable:

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

Оголошення методу виглядає жахливо, але використовувати його простіше, ніж знати про уніфікований інтерфейс.


2
Я б більше віддавав перевагу другому підходу Конрада: process(Readable readThing, Writable writeThing)і якщо ви повинні використовувати його, використовуючи process(foo, foo).
Йоахім Зауер

1
Чи не правильний синтаксис <RW extends Readable&Writable>?
ВІД

1
@JoachimSauer: Чому ти б віддав перевагу підходу, який легко переламуєш той, який просто візуально некрасивий? Якщо я називаю процес (foo, bar) і foo та bar різний, метод може не вдатися.
Майкл Шоу

@MichaelShaw: те, що я говорю, це те, що це не повинно провалюватися, коли вони різні об'єкти. Навіщо це робити? Якщо це так, то я б заперечував, що processробить багато різних речей і порушує принцип єдиної відповідальності.
Йоахім Зауер

@JoachimSauer: Чому б це не вдалося? for (i = 0; j <100; i ++) не є настільки корисним циклом, як для (i = 0; i <100; i ++). Цикл for і читає і записує в ту саму змінну, і це не порушує SRP для цього.
Майкл Шоу

12

Якщо є місце, де вам потрібно myObjectі як, Readableі Writableви можете:

  • Refactor це місце? Читання та письмо - це дві різні речі. Якщо метод робить і те, і інше, можливо, він не відповідає принципу єдиної відповідальності.

  • Пройдіть myObjectдвічі, як a Readableі як Writable(два аргументи). Що стосується методу, незалежно від того, чи це той самий об’єкт?


1
Це може спрацювати, якщо ви використовуєте це як аргумент, і вони є окремими проблемами. Однак іноді ви дійсно хочете, щоб об'єкт, який читався і записувався одночасно (з тієї ж причини, яку ви хочете використовувати, наприклад, ByteArrayOutputStream)
nablex

Як це? Вихідні потоки пишуть, як вказує назва - це вхідні потоки, які можуть читати. Те саме в C # - є StreamWriterпроти StreamReader(та багато інших)
Конрад Моравський

Ви пишете в ByteArrayOutputStream, щоб потрапити в байти (toByteArray ()). Це рівнозначно писати + читати. Фактична функціональність інтерфейсів майже однакова, але більш загальна. Іноді ви хочете лише читати чи писати, іноді ви хочете і те, і інше. Ще один приклад - ByteBuffer.
nablex

2
Я трохи відскочив у той другий момент, але через мить подумав, що насправді не здається поганою ідеєю. Ви не тільки розділяєте читання і письмо, ви робите більш гнучку функцію і зменшуєте кількість вхідного стану, яке вона мутує.
Фоши

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) усі реалізують Container на відміну від окремих інтерфейсів, але якщо хтось забуде це в реалізації, IOUtils надає корисний метод:

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

Я знаю, що це не оптимальне рішення, але це все-таки найкращий вихід на даний момент, оскільки контейнер все ще є досить великим випадком використання.


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

І ви можете зберегти рядок тут, прочитавшиThisWholeShebam () зробити перевірку читабельності.

Це означає, що наші колишні лише для читання повинні реалізовувати isWriteable () (повертаючи помилкові ) та write () (нічого не роблячи), але тепер він може займати всі види місць, до яких він раніше не міг, і весь код, який обробляє TheWholeShabam об'єкти будуть розбиратися з цим без додаткових зусиль з нашого боку.

Ще одне: якщо ви можете обробити виклик читання () у класі, який не читає, та виклик write () у класі, який не пише, не запираючи щось, ви можете пропустити isReadable () та isWriteable () методи. Це було б найелегантнішим способом впоратися - якщо це спрацює.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.