Pusty interfejs do łączenia wielu interfejsów

20

Załóżmy, że masz dwa interfejsy:

interface Readable {
    public void read();
}

interface Writable {
    public void write();
}

W niektórych przypadkach obiekty implementujące mogą obsługiwać tylko jeden z nich, ale w wielu przypadkach implementacje będą obsługiwały oba interfejsy. Ludzie korzystający z interfejsów będą musieli zrobić coś takiego:

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

Żadna z tych opcji nie jest szczególnie wygodna, tym bardziej, gdy rozważasz, że chcesz to jako parametr metody.

Jednym rozwiązaniem byłoby zadeklarowanie interfejsu owijania:

interface TheWholeShabam extends Readable, Writable {}

Ma to jednak jeden konkretny problem: wszystkie implementacje obsługujące zarówno Readable, jak i Writable muszą implementować TheWholeShabam, jeśli chcą być kompatybilne z osobami korzystającymi z interfejsu. Mimo że nie oferuje nic oprócz gwarantowanej obecności obu interfejsów.

Czy istnieje czyste rozwiązanie tego problemu, czy powinienem wybrać interfejs opakowania?

AKTUALIZACJA

W rzeczywistości często konieczne jest posiadanie obiektu, który jest zarówno czytelny, jak i zapisywalny, więc po prostu rozdzielenie obaw w argumentach nie zawsze jest czystym rozwiązaniem.

AKTUALIZACJA 2

(wyodrębnione jako odpowiedź, więc łatwiej jest komentować)

AKTUALIZACJA 3

Uwaga: podstawowym przypadkiem użycia tego nie są strumienie (chociaż one również muszą być obsługiwane). Strumienie wprowadzają bardzo konkretne rozróżnienie między nakładem a wynikiem, a obowiązki są wyraźnie rozdzielone. Pomyśl raczej o bytebuferze, w którym potrzebujesz jednego obiektu, do którego możesz pisać i czytać, jednego obiektu, który ma bardzo specyficzny stan związany z nim. Te obiekty istnieją, ponieważ są bardzo przydatne w niektórych rzeczach, takich jak asynchroniczne operacje we / wy, kodowanie ...

AKTUALIZACJA 4

Jedną z pierwszych rzeczy, które wypróbowałem, było to samo, co podana poniżej sugestia (sprawdź przyjętą odpowiedź), ale okazała się zbyt delikatna.

Załóżmy, że masz klasę, która musi zwrócić typ:

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

Jeśli wywołasz tę metodę, ogólna RW jest określana przez zmienną odbierającą obiekt, więc potrzebujesz sposobu na opisanie tej zmiennej.

MyObject myObject = someInstance.getItAll();

To zadziałałoby, ale po raz kolejny wiąże je z implementacją i może generować wyjątki klasycast w czasie wykonywania (w zależności od tego, co jest zwracane).

Dodatkowo, jeśli chcesz mieć zmienną klasową typu RW, musisz zdefiniować ogólną na poziomie klasy.

nablex
źródło
5
Wyrażenie to „cały shebang”
kevin cline
To dobre pytanie, ale myślę, że używanie Czytelnych i „Zapisywalnych” jako przykładowych interfejsów nieco
zamazuje
@Basueta Chociaż nazewnictwo zostało uproszczone, czytelne i możliwe do zapisu, całkiem dobrze przekazują moją skrzynkę użytkową. W niektórych przypadkach chcesz tylko do odczytu, w niektórych przypadkach tylko do pisania, a w zaskakującej liczbie przypadków do odczytu i zapisu.
nablex
Nie mogę myśleć o czasach, w których potrzebowałem pojedynczego strumienia, który byłby zarówno czytelny, jak i zapisywalny, a sądząc po odpowiedziach / komentarzach innych ludzi, nie sądzę, że jestem jedynym. Mówię tylko, że może być bardziej pomocne wybranie mniej kontrowersyjnej pary interfejsów ...
Vaughandroid
@Baqueta Masz coś wspólnego z pakietami java.nio *? Jeśli trzymasz się strumieni, przypadek użycia jest naprawdę ograniczony do miejsc, w których chciałbyś użyć strumienia ByteArray *.
nablex

Odpowiedzi:

19

Tak, możesz zadeklarować parametr metody jako nieokreślony typ, który rozszerza zarówno Readablei Writable:

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

Deklaracja metody wygląda okropnie, ale korzystanie z niej jest łatwiejsze niż znajomość zunifikowanego interfejsu.

Kilian Foth
źródło
2
Wolę tutaj drugie podejście Konrada: process(Readable readThing, Writable writeThing)i jeśli musisz je przywołać za pomocą process(foo, foo).
Joachim Sauer
1
Czy nie jest poprawna składnia <RW extends Readable&Writable>?
PRZYJEDŹ OD
1
@JachachSSauer: Dlaczego wolisz podejście, które łatwo się psuje, niż takie, które jest tylko brzydkie wizualnie? Jeśli wywołam proces (foo, bar) i foo i bar są różne, metoda może zawieść.
Michael Shaw
@MichaelShaw: mówię, że nie powinno zawieść, gdy są to różne przedmioty. Dlaczego warto Jeśli tak, to twierdzę, że processrobi wiele różnych rzeczy i narusza zasadę pojedynczej odpowiedzialności.
Joachim Sauer
@JachachimSauer: Dlaczego nie miałby zawieść? dla (i = 0; j <100; i ++) nie jest tak przydatna pętla jak dla (i = 0; i <100; i ++). Pętla for zarówno odczytuje, jak i zapisuje tę samą zmienną, i nie narusza to SRP.
Michael Shaw
12

Jeśli jest miejsce, w którym potrzebujesz myObjectzarówno a, jak Readablei Writablemożesz:

  • Refaktoryzować to miejsce? Czytanie i pisanie to dwie różne rzeczy. Jeśli metoda spełnia oba te warunki, być może nie przestrzega zasady pojedynczej odpowiedzialności.

  • Przekaż myObjectdwukrotnie, jako a Readablei jako Writable(dwa argumenty). Co obchodzi metoda, czy jest to ten sam obiekt, czy nie?

Konrad Morawski
źródło
1
Może to działać, gdy użyjesz go jako argumentu i są to osobne obawy. Jednak czasami naprawdę chcesz obiektu, który jest jednocześnie czytelny i zapisywalny (z tego samego powodu, dla którego chcesz na przykład użyć ByteArrayOutputStream)
nablex
Dlaczego? Strumienie wyjściowe zapisują, jak sama nazwa wskazuje - to strumienie wejściowe mogą czytać. To samo w C # - jest StreamWriterkontra StreamReader(i wiele innych)
Konrad Morawski
Piszemy do ByteArrayOutputStream, aby uzyskać bajty (toByteArray ()). Jest to równoważne z zapisem + odczytem. Rzeczywista funkcjonalność interfejsów jest w dużej mierze taka sama, ale w bardziej ogólny sposób. Czasami będziesz chciał tylko czytać lub pisać, a czasem jedno i drugie. Innym przykładem jest ByteBuffer.
nablex
2
Odskoczyłem trochę w tym drugim momencie, ale po chwili namysłu to naprawdę nie wydaje się złym pomysłem. Nie tylko oddzielasz czytanie i pisanie, czynisz bardziej elastyczną funkcję i zmniejszasz ilość wejściowego stanu, który mutuje.
Phoshi
2
@ Phoshi Problem polega na tym, że obawy nie zawsze są oddzielne. Czasami potrzebujesz obiektu, który może zarówno czytać, jak i pisać, i chcesz gwarancji, że jest to ten sam obiekt (np. ByteArrayOutputStream, ByteBuffer, ...)
nablex
4

Żadna z odpowiedzi nie odnosi się obecnie do sytuacji, gdy nie potrzebujesz czytelnego ani zapisywalnego, ale jedno i drugie . Potrzebujesz gwarancji, że pisząc do A, możesz odczytać te dane z powrotem z A, nie pisać do A i czytać z B i mieć tylko nadzieję, że w rzeczywistości są one tym samym obiektem. Przypadki użycia są obfite, na przykład wszędzie, gdzie można użyć ByteBuffer.

W każdym razie, prawie skończyłem moduł, nad którym pracuję i obecnie zdecydowałem się na interfejs opakowania:

interface Container extends Readable, Writable {}

Teraz możesz przynajmniej:

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

Moje własne implementacje kontenera (obecnie 3) implementują kontener w przeciwieństwie do oddzielnych interfejsów, ale jeśli ktoś zapomni o tym w implementacji, IOUtils zapewnia metodę użyteczności:

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

Wiem, że to nie jest optymalne rozwiązanie, ale w tej chwili jest to najlepsze wyjście, ponieważ Container wciąż jest dość dużą obudową.

nablex
źródło
1
Myślę, że to absolutnie w porządku. Lepsze niż niektóre inne podejścia zaproponowane w innych odpowiedziach.
Tom Anderson
0

Biorąc pod uwagę ogólną instancję MyObject, zawsze będziesz musiał wiedzieć, czy obsługuje ona odczyty czy zapisy. Więc miałbyś kod taki jak:

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

W prostym przypadku nie sądzę, że można to poprawić. Ale jeśli po przeczytaniu readThisReadablechce zapisać plik Readable do innego pliku, robi się niezręcznie.

Więc prawdopodobnie wybrałbym to:

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

Biorąc to za parametr, readThisReadableteraz readThisWholeShabammoże obsłużyć każdą klasę, która implementuje TheWholeShabam, nie tylko MyObject. I może napisać, jeśli jest do zapisu, a nie napisać, jeśli nie. (Mamy prawdziwy „polimorfizm”.)

Tak więc pierwszy zestaw kodu staje się:

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

Możesz tutaj zapisać wiersz, zlecając readThisWholeShebam () sprawdzenie czytelności.

Oznacza to, że nasz poprzedni tylko do odczytu musi zaimplementować isWriteable () (zwracając wartość false ) i write () (nie robiąc nic), ale teraz może przejść do wszystkich miejsc, w których wcześniej nie mógł przejść i całego kodu, który obsługuje TheWholeShabam obiekty sobie z tym poradzą bez żadnego dalszego wysiłku z naszej strony.

Jeszcze jedno: jeśli potrafisz obsłużyć wywołanie read () w klasie, która nie czyta oraz wywołanie write () w klasie, która nie pisze bez usuwania czegoś, możesz pominąć isReadable () i isWriteable () metody. Byłby to najbardziej elegancki sposób, aby sobie z tym poradzić - jeśli to działa.

RalphChapin
źródło