Mendesain dengan antarmuka

Salah satu aktivitas fundamental dari setiap desain sistem perangkat lunak adalah mendefinisikan antarmuka antara komponen sistem. Karena konstruksi antarmuka Java memungkinkan Anda untuk mendefinisikan antarmuka abstrak tanpa menentukan implementasi apa pun, aktivitas utama dari setiap desain program Java adalah "mencari tahu apa antarmuka itu". Artikel ini membahas motivasi di balik antarmuka Java dan memberikan panduan tentang cara memanfaatkan bagian penting Java ini semaksimal mungkin.

Menguraikan antarmuka

Hampir dua tahun lalu, saya menulis bab tentang antarmuka Java dan meminta beberapa teman yang tahu C ++ untuk mengulasnya. Dalam bab ini, yang sekarang menjadi bagian dari pembaca kursus Java saya Java Dalam (lihat Sumber), saya menyajikan antarmuka terutama sebagai jenis khusus dari beberapa pewarisan: pewarisan berganda antarmuka (konsep berorientasi objek) tanpa beberapa pewarisan implementasi. Seorang pengulas mengatakan kepada saya bahwa, meskipun dia memahami mekanisme antarmuka Java setelah membaca bab saya, dia tidak benar-benar "mengerti intinya". Persisnya bagaimana, dia bertanya kepada saya, apakah antarmuka Java merupakan peningkatan dari mekanisme pewarisan berganda C ++? Pada saat itu saya tidak dapat menjawab pertanyaannya untuk kepuasannya, terutama karena pada masa itu saya tidakSaya sendiri tidak mengerti maksud dari antarmuka.

Meskipun saya harus bekerja dengan Java cukup lama sebelum saya merasa saya dapat menjelaskan pentingnya antarmuka, saya melihat satu perbedaan langsung antara antarmuka Java dan beberapa warisan C ++. Sebelum munculnya Java, saya menghabiskan lima tahun pemrograman di C ++, dan selama itu saya tidak pernah menggunakan multiple inheritance. Warisan ganda sebenarnya tidak bertentangan dengan agama saya, saya hanya tidak pernah mengalami situasi desain C ++ di mana saya merasa itu masuk akal. Ketika saya mulai bekerja dengan Java, yang pertama kali muncul pada saya tentang antarmuka adalah seberapa sering mereka berguna bagi saya. Berbeda dengan multiple inheritance dalam C ++, yang dalam lima tahun tidak pernah saya gunakan, saya selalu menggunakan antarmuka Java.

Jadi mengingat seberapa sering saya menemukan antarmuka berguna ketika saya mulai bekerja dengan Java, saya tahu sesuatu sedang terjadi. Tapi apa tepatnya? Mungkinkah antarmuka Java memecahkan masalah inheren dalam beberapa pewarisan tradisional? Apakah pewarisan berganda antarmuka secara intrinsik lebih baik daripada pewarisan ganda biasa dan lama?

Antarmuka dan 'masalah berlian'

Salah satu pembenaran antarmuka yang saya dengar sejak awal adalah bahwa mereka memecahkan "masalah berlian" dari warisan berganda tradisional. Masalah berlian adalah ambiguitas yang bisa terjadi ketika kelas mengalikan mewarisi dari dua kelas yang keduanya turun dari superclass umum. Misalnya, dalam novel Jurassic Park karya Michael Crichton ,ilmuwan menggabungkan DNA dinosaurus dengan DNA dari katak modern untuk mendapatkan hewan yang menyerupai dinosaurus tetapi dalam beberapa hal berperilaku seperti katak. Di akhir novel, para pahlawan dalam cerita tersandung pada telur dinosaurus. Dinosaurus, yang semuanya diciptakan betina untuk mencegah persaudaraan di alam liar, berkembang biak. Chrichton menghubungkan keajaiban cinta ini dengan potongan DNA katak yang digunakan para ilmuwan untuk mengisi bagian DNA dinosaurus yang hilang. Dalam populasi katak yang didominasi oleh satu jenis kelamin, kata Chrichton, beberapa katak dari jenis kelamin dominan dapat secara spontan mengubah jenis kelaminnya. (Meskipun ini tampak seperti hal yang baik untuk kelangsungan hidup spesies katak, pasti sangat membingungkan bagi masing-masing katak yang terlibat.) Dinosaurus di Jurassic Park secara tidak sengaja mewarisi perilaku perubahan jenis kelamin spontan ini dari nenek moyang katak mereka,dengan konsekuensi yang tragis.

Skenario Jurassic Park ini berpotensi dapat diwakili oleh hierarki warisan berikut:

Masalah berlian bisa muncul dalam hierarki pewarisan seperti yang ditunjukkan pada Gambar 1. Faktanya, masalah berlian mendapatkan namanya dari bentuk berlian dari hierarki pewarisan semacam itu. Salah satu cara masalah berlian dapat muncul dalam hierarki Jurassic Park adalah jika keduanya Dinosaurdan Frog, tetapi tidak Frogosaur, menimpa metode yang dideklarasikan di Animal. Berikut tampilan kode jika Java mendukung multiple inheritance tradisional:

kelas abstrak Hewan {

abstrak void talk (); }

class Frog meluas Hewan {

batal bicara () {

System.out.println ("Ribit, ribit."); }

kelas Dinosaurus meluas Hewan {

void talk () {System.out.println ("Oh, saya dinosaurus dan saya baik-baik saja ..."); }}

// (Tentu saja ini tidak dapat dikompilasi, karena Java // hanya mendukung pewarisan tunggal.) Class Frogosaur extends Frog, Dinosaur {}

Masalah berlian pantat kepalanya yang buruk ketika seseorang mencoba untuk memohon talk()pada Frogosaurobjek dari Animalreferensi, seperti di:

Hewan hewan = Frogosaurus baru (); animal.talk ();

Karena ambiguitas yang disebabkan oleh masalah berlian, tidak jelas apakah sistem runtime harus memanggil Frogatau Dinosaurmengimplementasikan talk(). Akankah Frogosaurparau "Ribbit, Ribbit."atau bernyanyi "Oh, I'm a dinosaur and I'm okay..."?

Masalah berlian juga akan muncul jika Animaltelah mendeklarasikan variabel instance publik, yang Frogosaurkemudian akan diwarisi dari keduanya Dinosaurdan Frog. Ketika merujuk ke variabel ini dalam sebuah Frogosaurobjek, salinan variabel - Frog's atau Dinosaur' s - mana yang akan dipilih? Atau, mungkin, akankah hanya ada satu salinan variabel dalam sebuah Frogosaurobjek?

Di Java, antarmuka menyelesaikan semua ambiguitas yang disebabkan oleh masalah berlian. Melalui antarmuka, Java memungkinkan beberapa pewarisan antarmuka tetapi tidak implementasi. Implementasi, yang mencakup variabel instance dan implementasi metode, selalu diwarisi satu per satu. Akibatnya, kebingungan tidak akan pernah muncul di Java tentang variabel instance yang diwariskan atau implementasi metode yang akan digunakan.

Antarmuka dan polimorfisme

Dalam pencarian saya untuk memahami antarmuka, penjelasan masalah berlian masuk akal bagi saya, tetapi itu tidak benar-benar memuaskan saya. Tentu, antarmuka mewakili cara Java menangani masalah berlian, tetapi apakah itu wawasan utama tentang antarmuka? Dan bagaimana penjelasan ini membantu saya memahami cara menggunakan antarmuka dalam program dan desain saya?

Seiring berjalannya waktu saya mulai percaya bahwa wawasan utama ke dalam antarmuka bukanlah tentang pewarisan ganda melainkan tentang polimorfisme (lihat penjelasan istilah ini di bawah). Antarmuka memungkinkan Anda memanfaatkan polimorfisme lebih besar dalam desain Anda, yang pada gilirannya membantu Anda membuat perangkat lunak Anda lebih fleksibel.

Akhirnya, saya memutuskan bahwa "titik" dari antarmuka adalah:

Antarmuka Java memberi Anda lebih banyak polimorfisme daripada yang bisa Anda dapatkan dengan keluarga kelas yang diwariskan secara tunggal, tanpa "beban" penerapan beberapa warisan.

Penyegaran tentang polimorfisme

Bagian ini akan memberikan penyegaran singkat tentang arti polimorfisme. Jika Anda sudah terbiasa dengan kata mewah ini, silakan langsung ke bagian berikutnya, "Mendapatkan lebih banyak polimorfisme".

Polimorfisme berarti menggunakan variabel superclass untuk merujuk ke objek subclass. Misalnya, pertimbangkan hierarki dan kode pewarisan sederhana ini:

kelas abstrak Hewan {

abstrak void talk (); }

kelas Anjing memperluas Hewan {

void talk () {System.out.println ("Woof!"); }}

kelas Kucing meluas Hewan {

void talk () {System.out.println ("Meong."); }}

Dengan hierarki pewarisan ini, polimorfisme memungkinkan Anda menyimpan referensi ke Dogobjek dalam tipe variabel Animal, seperti di:

Hewan hewan = Anjing baru (); 

Kata polimorfisme didasarkan pada akar kata Yunani yang berarti "banyak bentuk". Di sini, kelas memiliki banyak bentuk: kelas itu dan semua subkelasnya. Sebuah Animal, misalnya, dapat terlihat seperti a Dogatau a Catatau subclass lainnya dari Animal.

Polimorfisme di Java dimungkinkan oleh pengikatan dinamis, mekanisme yang digunakan mesin virtual Java (JVM) untuk memilih implementasi metode yang akan dipanggil berdasarkan deskriptor metode (nama metode dan jumlah serta jenis argumennya) dan kelas dari objek tempat metode dipanggil. Misalnya, makeItTalk()metode yang ditunjukkan di bawah ini menerima Animalreferensi sebagai parameter dan memanggil talk()referensi itu:

class Interrogator {

static void makeItTalk (Animal subject) {subject.talk (); }}

Pada waktu kompilasi, kompilator tidak tahu persis kelas objek mana yang akan diteruskan makeItTalk()saat runtime. Ia hanya tahu bahwa objek tersebut akan menjadi beberapa subclass dari Animal. Selain itu, kompilator tidak tahu persis implementasi mana yang talk()harus dijalankan pada waktu proses.

Seperti disebutkan di atas, pengikatan dinamis berarti JVM akan memutuskan pada saat runtime metode mana yang akan dipanggil berdasarkan kelas objek. Jika objeknya adalah a Dog, JVM akan memanggil Dogimplementasi metode tersebut, yang mengatakan "Woof!",. Jika objeknya adalah a Cat, JVM akan memanggil Catimplementasi metode tersebut, yang mengatakan "Meow!",. Pengikatan dinamis adalah mekanisme yang membuat polimorfisme, "subsitutabilitas" dari subclass untuk superclass, menjadi mungkin.

Polimorfisme membantu membuat program lebih fleksibel, karena di masa mendatang, Anda dapat menambahkan subkelas lain ke Animalkeluarga, dan makeItTalk()metode ini akan tetap berfungsi. Jika, misalnya, Anda nanti menambahkan Birdkelas:

kelas Burung meluas Hewan {

batal bicara () {

System.out.println ("Menciak, menciak!"); }}

Anda dapat mengirimkan Birdobjek ke makeItTalk()metode yang tidak berubah , dan akan berkata "Tweet, tweet!",.

Mendapatkan lebih banyak polimorfisme

Antarmuka memberi Anda lebih banyak polimorfisme daripada keluarga kelas yang diwariskan secara tunggal, karena dengan antarmuka Anda tidak harus membuat semuanya sesuai dengan satu keluarga kelas. Sebagai contoh:

interface Talkative {

void talk (); }

kelas abstrak Hewan mengimplementasikan Talkative {

abstrak public void talk (); }

kelas Anjing memperluas Hewan {

public void talk () {System.out.println ("Guk!"); }}

kelas Kucing meluas Hewan {

public void talk () {System.out.println ("Meong."); }}

class Interrogator {

static void makeItTalk (Subjek banyak bicara) {subject.talk (); }}

Dengan adanya kumpulan kelas dan antarmuka ini, nanti Anda dapat menambahkan kelas baru ke keluarga kelas yang sama sekali berbeda dan masih meneruskan instance dari kelas baru ke makeItTalk(). Misalnya, bayangkan Anda menambahkan CuckooClockkelas baru ke Clockkeluarga yang sudah ada :

jam kelas {}

class CuckooClock mengimplementasikan Talkative {

public void talk () {System.out.println ("Cuckoo, cuckoo!"); }}

Karena CuckooClockmengimplementasikan Talkativeantarmuka, Anda dapat mengirimkan CuckooClockobjek ke makeItTalk()metode:

class Example4 {

public static void main (String [] args) {CuckooClock cc = new CuckooClock (); Interrogator.makeItTalk (cc); }}

Dengan warisan tunggal saja, Anda harus menyesuaikan CuckooClockdiri dengan Animalkeluarga, atau tidak menggunakan polimorfisme. Dengan antarmuka, semua kelas dalam keluarga mana pun dapat diimplementasikan Talkativedan diteruskan makeItTalk(). Inilah mengapa saya mengatakan antarmuka memberi Anda lebih banyak polimorfisme daripada yang bisa Anda dapatkan dengan keluarga kelas yang diwariskan secara tunggal.

'Beban' warisan implementasi

Oke, klaim "lebih banyak polimorfisme" saya di atas cukup jelas dan mungkin terlihat jelas bagi banyak pembaca, tetapi apa yang saya maksud dengan, "tanpa beban penerapan warisan berganda?" Secara khusus, tepatnya bagaimana beberapa warisan implementasi menjadi beban?

Seperti yang saya lihat, beban implementasi warisan berganda pada dasarnya tidak fleksibel. Dan ketidakfleksibelan ini memetakan langsung ke ketidakfleksibelan pewarisan dibandingkan dengan komposisi.

Dengan komposisi, saya hanya bermaksud menggunakan variabel instan yang merupakan referensi ke objek lain. Misalnya, dalam kode berikut, kelas Appleterkait dengan kelas Fruitmenurut komposisi, karena Applememiliki variabel instan yang menyimpan referensi ke Fruitobjek:

kelas Buah {

// ...}

kelas Apple {

Buah pribadi buah = Buah baru (); // ...}

Dalam contoh ini, Appleyang saya sebut kelas front-end dan Fruityang saya sebut kelas back-end. Dalam hubungan komposisi, kelas front-end menyimpan referensi di salah satu variabel instansinya ke kelas back-end.

Di kolom Teknik Desain saya edisi bulan lalu , saya membandingkan komposisi dengan warisan. Kesimpulan saya adalah bahwa komposisi - dengan potensi biaya dalam beberapa efisiensi kinerja - biasanya menghasilkan kode yang lebih fleksibel. Saya mengidentifikasi keuntungan fleksibilitas berikut untuk komposisi:

  • Lebih mudah mengubah kelas yang terlibat dalam hubungan komposisi daripada mengubah kelas yang terlibat dalam hubungan pewarisan.

  • Komposisi memungkinkan Anda menunda pembuatan objek back-end hingga (dan kecuali) diperlukan. Ini juga memungkinkan Anda untuk mengubah objek back-end secara dinamis sepanjang masa pakai objek front-end. Dengan inheritance, Anda mendapatkan gambar superclass di gambar objek subclass Anda segera setelah subclass dibuat, dan itu tetap menjadi bagian dari objek subclass sepanjang masa subclass.

Satu-satunya keuntungan fleksibilitas yang saya identifikasi untuk warisan adalah:

  • Lebih mudah menambahkan subclass baru (inheritance) daripada menambahkan kelas front-end baru (komposisi), karena inheritance hadir dengan polimorfisme. Jika Anda memiliki sedikit kode yang hanya bergantung pada antarmuka kelas super, kode tersebut dapat bekerja dengan subkelas baru tanpa perubahan. Ini tidak benar tentang komposisi, kecuali Anda menggunakan komposisi dengan antarmuka.