Antarmuka kosong untuk menggabungkan beberapa antarmuka

20

Misalkan Anda memiliki dua antarmuka:

interface Readable {
    public void read();
}

interface Writable {
    public void write();
}

Dalam beberapa kasus objek implementasi hanya dapat mendukung salah satunya, tetapi dalam banyak kasus implementasi akan mendukung kedua antarmuka. Orang-orang yang menggunakan antarmuka harus melakukan sesuatu seperti:

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

Tidak satu pun dari opsi ini yang sangat nyaman, terlebih lagi ketika mempertimbangkan bahwa Anda menginginkan ini sebagai parameter metode.

Salah satu solusinya adalah dengan mendeklarasikan antarmuka pembungkus:

interface TheWholeShabam extends Readable, Writable {}

Tetapi ini memiliki satu masalah khusus: semua implementasi yang mendukung Readable dan Writable harus mengimplementasikan TheWholeShabam jika mereka ingin kompatibel dengan orang yang menggunakan antarmuka. Meskipun tidak menawarkan apa pun selain jaminan kehadiran kedua antarmuka.

Apakah ada solusi bersih untuk masalah ini atau haruskah saya menggunakan antarmuka pembungkus?

MEMPERBARUI

Sebenarnya seringkali diperlukan untuk memiliki objek yang dapat dibaca dan ditulis sehingga memisahkan kekhawatiran dalam argumen tidak selalu merupakan solusi bersih.

UPDATE2

(diekstraksi sebagai jawaban sehingga lebih mudah untuk mengomentari)

UPDATE3

Berhati-hatilah karena penggunaan utama untuk ini bukan stream (walaupun mereka juga harus didukung). Streaming membuat perbedaan yang sangat spesifik antara input dan output dan ada pemisahan tanggung jawab yang jelas. Sebaliknya, pikirkan sesuatu seperti bytebuffer di mana Anda memerlukan satu objek yang dapat Anda tulis dan baca, satu objek yang memiliki keadaan yang sangat spesifik yang melekat padanya. Objek-objek ini ada karena mereka sangat berguna untuk beberapa hal seperti asynchronous I / O, encodings, ...

UPDATE4

Salah satu hal pertama yang saya coba adalah sama dengan saran yang diberikan di bawah ini (periksa jawaban yang diterima) tetapi ternyata terlalu rapuh.

Misalkan Anda memiliki kelas yang harus mengembalikan tipe:

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

Jika Anda memanggil metode ini, RW generik ditentukan oleh variabel yang menerima objek, jadi Anda perlu cara untuk menggambarkan var ini.

MyObject myObject = someInstance.getItAll();

Ini akan berfungsi tetapi sekali lagi mengikatnya ke implementasi dan mungkin benar-benar membuang classcastexeption saat runtime (tergantung pada apa yang dikembalikan).

Selain itu jika Anda ingin variabel kelas dari tipe RW, Anda perlu mendefinisikan generik di tingkat kelas.

nablex
sumber
5
Ungkapannya adalah "whole shebang"
kevin cline
Ini adalah pertanyaan yang bagus, tapi saya pikir menggunakan Readable dan 'Writable' sebagai contoh antarmuka Anda mengeruhkan air karena mereka biasanya peran yang berbeda ...
vaughandroid
@Basueta Meskipun penamaannya disederhanakan, dapat dibaca & ditulis benar-benar menyampaikan penggunaan kata kunci saya dengan cukup baik. Dalam beberapa kasus Anda hanya ingin membaca, dalam beberapa kasus hanya menulis dan dalam jumlah yang mengejutkan membaca & menulis.
nablex
Saya tidak dapat memikirkan saat ketika saya membutuhkan aliran tunggal yang dapat dibaca & ditulis, dan menilai dari jawaban / komentar orang lain saya tidak berpikir saya satu-satunya. Saya hanya mengatakan mungkin lebih bermanfaat untuk memilih sepasang antarmuka yang kurang kontroversial ...
vaughandroid
@ Baqueta Ada hubungannya dengan paket java.nio *? Jika Anda tetap menggunakan stream, usecase memang terbatas pada tempat-tempat di mana Anda akan menggunakan ByteArray * Stream.
nablex

Jawaban:

19

Ya, Anda dapat mendeklarasikan parameter metode Anda sebagai tipe yang kurang spesifik yang meluas keduanya Readabledan Writable:

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

Deklarasi metode terlihat mengerikan, tetapi menggunakannya lebih mudah daripada harus tahu tentang antarmuka terpadu.

Kilian Foth
sumber
2
Saya lebih suka pendekatan kedua Konrads di sini: process(Readable readThing, Writable writeThing)dan jika Anda harus memintanya menggunakan process(foo, foo).
Joachim Sauer
1
Bukankah sintaks yang benar <RW extends Readable&Writable>?
DATANG DARI
1
@ JoachimSauer: Mengapa Anda lebih suka pendekatan yang mudah rusak daripada yang hanya jelek secara visual? Jika saya sebut proses (foo, bar) dan foo dan bar berbeda, metode ini mungkin gagal.
Michael Shaw
@MichaelShaw: yang saya katakan adalah bahwa itu tidak boleh gagal ketika mereka adalah objek yang berbeda. Kenapa harus begitu? Jika ya, maka saya berpendapat bahwa processmelakukan banyak hal yang berbeda dan melanggar prinsip tanggung jawab tunggal.
Joachim Sauer
@ JoachimSauer: Mengapa tidak gagal? untuk (i = 0; j <100; i ++) tidak berguna sebagai loop seperti untuk (i = 0; i <100; i ++). Untuk for loop membaca dan menulis ke variabel yang sama, dan itu tidak melanggar SRP untuk melakukannya.
Michael Shaw
12

Jika ada tempat di mana Anda membutuhkan myObjectkeduanya Readabledan WritableAnda dapat:

  • Refactor tempat itu? Membaca dan menulis adalah dua hal yang berbeda. Jika suatu metode melakukan keduanya, mungkin itu tidak mengikuti prinsip tanggung jawab tunggal.

  • Pass myObjectdua kali, sebagai a Readabledan sebagai Writable(dua argumen). Apa peduli metode ini apakah itu objek yang sama atau tidak?

Konrad Morawski
sumber
1
Ini bisa berfungsi ketika Anda menggunakannya sebagai argumen dan itu adalah masalah yang terpisah. Namun kadang-kadang Anda benar-benar menginginkan objek yang dapat dibaca dan ditulis pada saat yang sama (karena alasan yang sama Anda ingin menggunakan ByteArrayOutputStream misalnya)
nablex
Bagaimana bisa? Output stream menulis, seperti namanya - itu adalah input stream yang dapat dibaca. Sama di C # - ada StreamWritervs. StreamReader(dan banyak lainnya)
Konrad Morawski
Anda menulis ke ByteArrayOutputStream untuk mendapatkan byte (toByteArray ()). Ini sama dengan menulis + baca. Fungsionalitas aktual di balik antarmuka hampir sama tetapi dengan cara yang lebih umum. Terkadang Anda hanya ingin membaca atau menulis, terkadang Anda menginginkan keduanya. Contoh lain adalah ByteBuffer.
nablex
2
Saya mundur sedikit pada poin kedua, tetapi setelah beberapa saat berpikir itu benar-benar tidak tampak seperti ide yang buruk. Anda tidak hanya memisahkan membaca dan menulis, Anda membuat fungsi yang lebih fleksibel dan mengurangi jumlah input yang dimutasikan.
Phoshi
2
@ Phoshi Masalahnya adalah bahwa kekhawatiran tidak selalu terpisah. Terkadang Anda menginginkan objek yang dapat membaca dan menulis dan Anda menginginkan jaminan bahwa itu adalah objek yang sama (misalnya ByteArrayOutputStream, ByteBuffer, ...)
nablex
4

Tak satu pun dari jawaban saat ini membahas situasi ketika Anda tidak membutuhkan yang dapat dibaca atau ditulis tetapi keduanya . Anda perlu jaminan bahwa ketika menulis ke A, Anda dapat membaca data itu kembali dari A, bukan menulis ke A dan membaca dari B dan hanya berharap mereka sebenarnya objek yang sama. Usecases berlimpah, misalnya di mana-mana Anda akan menggunakan ByteBuffer.

Lagi pula, saya hampir menyelesaikan modul yang sedang saya kerjakan dan saat ini saya telah memilih antarmuka pembungkus:

interface Container extends Readable, Writable {}

Sekarang setidaknya Anda bisa melakukan:

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

Implementasi container saya sendiri (saat ini 3) semuanya mengimplementasikan Container sebagai lawan dari interface terpisah tetapi jika seseorang melupakan hal ini dalam implementasi, IOUtils menyediakan metode utilitas:

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

Saya tahu ini bukan solusi optimal tetapi masih merupakan jalan keluar terbaik saat ini karena Container masih merupakan usecase yang cukup besar.

nablex
sumber
1
Saya pikir ini baik-baik saja. Lebih baik daripada beberapa pendekatan lain yang diusulkan dalam jawaban lain.
Tom Anderson
0

Diberikan contoh MyObject generik, Anda akan selalu perlu tahu apakah itu mendukung membaca atau menulis. Jadi, Anda akan memiliki kode seperti:

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

Dalam kasus sederhana, saya tidak berpikir Anda bisa memperbaiki ini. Tetapi jika readThisReadableingin menulis Readable ke file lain setelah membacanya, itu menjadi canggung.

Jadi saya mungkin akan pergi dengan ini:

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

Mengambil ini sebagai parameter, readThisReadablesekarang readThisWholeShabamdapat menangani kelas yang mengimplementasikan TheWholeShabam, bukan hanya MyObject. Dan itu dapat menulisnya jika dapat ditulis dan tidak menulisnya jika tidak. (Kami punya "Polimorfisme" nyata.)

Jadi set kode pertama menjadi:

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

Dan Anda dapat menyimpan baris di sini dengan membaca ThisWholeShebam () melakukan pemeriksaan keterbacaan.

Ini berarti bahwa mantan kita yang hanya dapat dibaca harus mengimplementasikan isWriteable () (return false ) dan write () (tidak melakukan apa-apa), tetapi sekarang ia dapat pergi ke semua jenis tempat yang tidak dapat dilalui sebelumnya dan semua kode yang menangani TheWholeShabam objek akan menghadapinya tanpa upaya lebih lanjut dari pihak kita.

Satu hal lagi: jika Anda bisa menangani panggilan untuk membaca () di kelas yang tidak membaca dan panggilan untuk menulis () di kelas yang tidak menulis tanpa merusak sesuatu, Anda dapat melewati isReadable () dan isWriteable () metode. Ini akan menjadi cara paling elegan untuk menanganinya - jika berhasil.

RalphChapin
sumber