Seorang kolega saya datang dengan aturan praktis untuk memilih antara membuat kelas dasar atau antarmuka.
Dia berkata:
Bayangkan setiap metode baru yang akan Anda implementasikan. Untuk masing-masing dari mereka, pertimbangkan ini: akankah metode ini diterapkan oleh lebih dari satu kelas dalam bentuk yang persis seperti ini, tanpa perubahan apa pun ? Jika jawabannya "ya", buat kelas dasar. Dalam setiap situasi lain, buat antarmuka.
Sebagai contoh:
Pertimbangkan kelas
cat
dandog
, yang memperluas kelasmammal
dan memiliki metode tunggalpet()
. Kami kemudian menambahkan kelasalligator
, yang tidak memperpanjang apa pun dan memiliki metode tunggalslither()
.Sekarang, kami ingin menambahkan
eat()
metode untuk semuanya.Jika implementasi
eat()
metode akan persis sama untukcat
,dog
danalligator
, kita harus membuat kelas dasar (katakanlah,animal
), yang mengimplementasikan metode ini.Namun, jika implementasi
alligator
berbeda sedikit, kita harus membuatIEat
antarmuka dan membuatmammal
danalligator
mengimplementasikannya.
Dia bersikeras bahwa metode ini mencakup semua kasus, tetapi sepertinya terlalu menyederhanakan bagi saya.
Apakah layak mengikuti aturan praktis ini?
sumber
alligator
Implementasi darieat
perbedaan, tentu saja, dalam hal ia menerimacat
dandog
sebagai parameter.is a
dan interface adalahacts like
atauis
. Jadiis a
mamalia danacts like
pemakan anjing . Ini akan memberi tahu kita bahwa mamalia harus kelas dan pemakan harus antarmuka. Itu selalu menjadi panduan yang sangat membantu. Sidenote: Contohis
akanThe cake is eatable
atauThe book is writable
.Jawaban:
Saya tidak berpikir bahwa ini adalah aturan praktis yang baik. Jika Anda khawatir tentang penggunaan kembali kode, Anda dapat menerapkan
PetEatingBehavior
peran yang menentukan fungsi makan untuk kucing dan anjing. Kemudian Anda dapat memilikiIEat
dan menggunakan kembali kode bersama.Hari-hari ini saya melihat semakin sedikit alasan untuk menggunakan warisan. Satu keuntungan besar adalah kemudahan penggunaan. Ambil kerangka kerja GUI. Cara populer untuk merancang API untuk ini adalah dengan mengekspos kelas dasar yang besar, dan mendokumentasikan metode mana yang dapat ditimpa pengguna. Jadi kita dapat mengabaikan semua hal rumit lain yang perlu dikelola aplikasi kita, karena implementasi "default" diberikan oleh kelas dasar. Tetapi kita masih dapat menyesuaikan hal-hal dengan menerapkan kembali metode yang benar ketika kita perlu.
Jika Anda tetap menggunakan antarmuka untuk api Anda, pengguna Anda biasanya memiliki lebih banyak pekerjaan yang harus dilakukan untuk membuat contoh sederhana berfungsi. Tetapi API dengan antarmuka seringkali kurang digabungkan dan lebih mudah dipelihara karena alasan sederhana yang
IEat
mengandung lebih sedikit informasi daripadaMammal
kelas dasar. Jadi konsumen akan bergantung pada kontrak yang lebih lemah, Anda mendapatkan lebih banyak peluang refactoring, dll.Mengutip Rich Hickey: Sederhana! = Mudah
sumber
PetEatingBehavior
implementasi?PetEatingBehavior
mungkin yang salah. Saya tidak bisa memberi Anda implementasi karena ini hanya contoh mainan. Tapi saya bisa menjelaskan langkah-langkah refactoring: ia memiliki konstruktor yang mengambil semua informasi yang digunakan oleh kucing / anjing untuk menentukaneat
metode (contoh gigi, contoh perut, dll). Ini hanya berisi kode umum untuk kucing dan anjing yang digunakan dalameat
metode mereka . Anda hanya perlu membuatPetEatingBehavior
anggota dan meneruskannya panggilan ke dog.eat/cat.eat.Ini adalah aturan praktis yang baik, tetapi saya tahu beberapa tempat di mana saya melanggarnya.
Agar adil, saya menggunakan kelas dasar (abstrak) jauh lebih banyak daripada rekan-rekan saya. Saya melakukan ini sebagai pemrograman defensif.
Bagi saya (dalam banyak bahasa), kelas dasar abstrak menambahkan batasan kunci bahwa mungkin hanya ada satu kelas dasar. Dengan memilih untuk menggunakan kelas dasar daripada antarmuka, Anda mengatakan "ini adalah fungsi inti dari kelas (dan pewaris apa pun), dan tidak pantas untuk menggabungkan mau tidak mau dengan fungsi lainnya". Antarmuka adalah sebaliknya: "Ini adalah beberapa sifat dari suatu objek, belum tentu tanggung jawab intinya".
Pilihan desain semacam ini membuat panduan implisit untuk implementator Anda tentang bagaimana mereka harus menggunakan kode Anda, dan dengan ekstensi menulis kode mereka. Karena dampaknya yang lebih besar pada basis kode, saya cenderung mempertimbangkan hal-hal ini lebih kuat ketika membuat keputusan desain.
sumber
Masalah dengan penyederhanaan teman Anda adalah menentukan apakah suatu metode perlu diubah atau tidak sangat sulit. Lebih baik menggunakan aturan yang berbicara dengan mentalitas di balik kacamata super dan antarmuka.
Alih-alih mencari tahu apa yang harus Anda lakukan dengan melihat apa yang menurut Anda fungsionalitasnya, lihat hierarki yang Anda coba buat.
Beginilah cara seorang profesor saya mengajarkan perbedaan:
Superclasses harus
is a
dan antarmukaacts like
atauis
. Jadiis a
mamalia danacts like
pemakan anjing . Ini akan memberi tahu kita bahwa mamalia harus kelas dan pemakan harus antarmuka. Contohis
akanThe cake is eatable
atauThe book is writable
(membuat keduanyaeatable
danwritable
antarmuka).Menggunakan metodologi seperti ini cukup sederhana dan mudah, dan menyebabkan Anda membuat kode ke struktur dan konsep daripada apa yang Anda pikir akan dilakukan oleh kode. Ini membuat perawatan lebih mudah, kode lebih mudah dibaca, dan desain lebih mudah untuk dibuat.
Terlepas dari apa kode sebenarnya mungkin mengatakan, jika Anda menggunakan metode seperti ini, Anda bisa kembali lima tahun dari sekarang dan menggunakan metode yang sama untuk menelusuri kembali langkah-langkah Anda dan dengan mudah mengetahui bagaimana program Anda dirancang dan bagaimana ia berinteraksi.
Hanya dua sen saya.
sumber
Atas permintaan exizt, saya memperluas komentar saya ke jawaban yang lebih panjang.
Kesalahan terbesar yang dilakukan orang adalah percaya bahwa antarmuka hanyalah kelas abstrak yang kosong. Antarmuka adalah cara bagi seorang programmer untuk mengatakan, "Saya tidak peduli apa yang Anda berikan kepada saya, asalkan mengikuti konvensi ini."
Pustaka .NET berfungsi sebagai contoh yang bagus. Misalnya, ketika Anda menulis fungsi yang menerima
IEnumerable<T>
, apa yang Anda katakan adalah, "Saya tidak peduli bagaimana Anda menyimpan data Anda. Saya hanya ingin tahu bahwa saya bisa menggunakanforeach
loop.Ini mengarah pada beberapa kode yang sangat fleksibel. Tiba-tiba untuk diintegrasikan, yang perlu Anda lakukan hanyalah bermain sesuai aturan antarmuka yang ada. Jika mengimplementasikan antarmuka sulit atau membingungkan, maka mungkin itu adalah petunjuk bahwa Anda mencoba untuk mendorong pasak persegi di lubang bundar.
Tetapi kemudian muncul pertanyaan: "Bagaimana dengan penggunaan kembali kode? Profesor CS saya mengatakan kepada saya bahwa warisan adalah solusi untuk semua masalah penggunaan kembali kode dan bahwa warisan memungkinkan Anda untuk menulis sekali dan menggunakan di mana-mana dan itu akan menyembuhkan menangitis dalam proses menyelamatkan anak yatim piatu. dari laut yang naik dan tidak akan ada lagi air mata dan seterusnya dll dll. "
Menggunakan pewarisan hanya karena Anda menyukai bunyi kata "penggunaan kembali kode" adalah ide yang sangat buruk. The panduan kode styling oleh Google membuat titik ini cukup ringkas:
Untuk mengilustrasikan mengapa pewarisan tidak selalu jawabannya, saya akan menggunakan kelas yang disebut MySpecialFileWriter †. Seseorang yang secara buta percaya bahwa pewarisan adalah solusi untuk semua masalah akan berargumen bahwa Anda harus mencoba mewarisi
FileStream
, jangan sampai Anda menduplikasiFileStream
kode. Orang pintar menyadari bahwa ini bodoh. Anda seharusnya hanya memilikiFileStream
objek di kelas Anda (baik sebagai variabel lokal atau anggota) dan menggunakan fungsinya.The
FileStream
contoh mungkin tampak dibikin, tapi tidak. Jika Anda memiliki dua kelas yang keduanya mengimplementasikan antarmuka yang sama dengan cara yang sama persis, maka Anda harus memiliki kelas ketiga yang merangkum operasi apa pun yang diduplikasi. Sasaran Anda adalah menulis kelas-kelas yang berisi blok-blok yang dapat digunakan sendiri dan dapat disatukan seperti lego.Ini tidak berarti bahwa warisan harus dihindari dengan cara apa pun. Ada banyak hal yang perlu dipertimbangkan, dan sebagian besar akan dibahas dengan meneliti pertanyaan, "Komposisi vs Warisan." Stack Overflow kami sendiri memiliki beberapa jawaban bagus tentang masalah ini.
Pada akhirnya, sentimen rekan kerja Anda tidak memiliki kedalaman atau pemahaman yang diperlukan untuk membuat keputusan. Teliti subjeknya dan cari tahu sendiri.
† Saat menggambarkan warisan, semua orang menggunakan hewan. Itu tidak berguna. Dalam 11 tahun pengembangan, saya tidak pernah menulis sebuah kelas bernama
Cat
, jadi saya tidak akan menggunakannya sebagai contoh.sumber
FileStream
)Contoh-contoh jarang membuat titik, terutama ketika mereka tentang mobil atau hewan. Cara makan binatang (atau mengolah makanan) tidak benar-benar terikat pada warisannya, tidak ada cara makan tunggal yang akan berlaku untuk semua hewan. Ini lebih tergantung pada kemampuan fisiknya (cara input, pemrosesan, dan output, demikian dikatakan). Mamalia dapat menjadi karnivora, herbivora atau omnivora misalnya, sementara burung, mamalia dan ikan dapat memakan serangga. Perilaku ini dapat ditangkap dengan pola-pola tertentu.
Anda dalam hal ini lebih baik dengan mengabstraksi perilaku, seperti ini:
Atau menerapkan pola pengunjung tempat
FoodProcessor
mencoba memberi makan hewan yang kompatibel (ICarnivore : IFoodEater
,MeatProcessingVisitor
). Pikiran tentang binatang hidup dapat menghalangi Anda dalam berpikir untuk merobek saluran pencernaan mereka dan menggantinya dengan yang umum, tetapi Anda benar-benar membantu mereka.Ini akan membuat hewan Anda menjadi kelas dasar, dan bahkan lebih baik, yang tidak perlu Anda ubah lagi saat menambahkan jenis makanan baru dan prosesor yang sesuai, sehingga Anda dapat fokus pada hal-hal yang membuat kelas hewan khusus ini menjadi bekerja sangat unik.
Jadi ya, kelas dasar memiliki tempat mereka sendiri, aturan praktis berlaku (sering, tidak selalu, karena merupakan aturan praktis) tetapi tetap mencari perilaku yang dapat Anda isolasi.
sumber
IConsumer
antarmuka. Karnivora dapat mengonsumsi daging, herbivora mengonsumsi tanaman, dan tanaman mengonsumsi sinar matahari dan air.Antarmuka benar-benar kontrak. Tidak ada fungsi. Terserah implementor untuk mengimplementasikan. Tidak ada pilihan karena seseorang harus melaksanakan kontrak.
Kelas abstrak memberikan pilihan implementor. Seseorang dapat memilih untuk memiliki metode yang harus diimplementasikan setiap orang, menyediakan metode dengan fungsionalitas dasar yang dapat ditimpa, atau menyediakan fungsionalitas dasar yang dapat digunakan semua pewaris.
Sebagai contoh:
Saya akan mengatakan aturan praktis Anda dapat ditangani oleh kelas dasar juga. Khususnya karena banyak mamalia mungkin "makan" yang sama kemudian menggunakan kelas dasar mungkin lebih baik daripada antarmuka, karena orang dapat menerapkan metode makan virtual yang dapat digunakan sebagian besar pewaris dan kemudian kasus luar biasa dapat menimpa.
Sekarang jika definisi "makan" sangat bervariasi dari hewan ke hewan maka mungkin dan antarmuka akan lebih baik. Ini terkadang merupakan area abu-abu dan tergantung pada situasi individu.
sumber
Tidak.
Antarmuka adalah tentang bagaimana metode diimplementasikan. Jika Anda mendasarkan keputusan Anda kapan saja untuk menggunakan antarmuka tentang bagaimana metode akan diimplementasikan, maka Anda melakukan sesuatu yang salah.
Bagi saya, perbedaan antara antarmuka dan kelas dasar adalah tentang di mana persyaratan berdiri. Anda membuat antarmuka, ketika beberapa bagian kode membutuhkan kelas dengan API spesifik dan perilaku tersirat yang muncul dari API ini. Anda tidak dapat membuat antarmuka tanpa kode yang benar-benar akan memanggilnya.
Di sisi lain, kelas dasar biasanya dimaksudkan sebagai cara untuk menggunakan kembali kode, jadi Anda jarang repot dengan kode yang akan memanggil kelas dasar ini dan hanya mempertimbangkan objek-objek hiu yang menjadi bagian dari kelas dasar ini.
sumber
eat()
pada setiap hewan? Kita bisamammal
menerapkannya, bukan? Saya mengerti maksud Anda tentang persyaratan.Ini sebenarnya bukan aturan praktis yang baik karena jika Anda menginginkan implementasi yang berbeda, Anda selalu dapat memiliki kelas dasar abstrak dengan metode abstrak. Setiap pewaris dijamin memiliki metode itu, tetapi masing-masing mendefinisikan implementasinya sendiri. Secara teknis Anda bisa memiliki kelas abstrak dengan apa-apa selain serangkaian metode abstrak, dan pada dasarnya akan sama dengan antarmuka.
Kelas-kelas dasar berguna ketika Anda menginginkan implementasi aktual dari beberapa keadaan atau perilaku yang dapat digunakan di beberapa kelas. Jika Anda menemukan diri Anda dengan beberapa kelas yang memiliki bidang dan metode yang identik dengan implementasi yang identik, Anda mungkin bisa mengubahnya menjadi kelas dasar. Anda hanya harus berhati-hati dalam bagaimana Anda mewarisi. Setiap bidang atau metode yang ada di kelas dasar harus masuk akal di pewaris, terutama ketika itu dilemparkan sebagai kelas dasar.
Antarmuka berguna ketika Anda perlu berinteraksi dengan satu set kelas dengan cara yang sama, tetapi Anda tidak dapat memprediksi atau tidak peduli dengan implementasi aktualnya. Ambil contoh sesuatu seperti antarmuka Koleksi. Hanya Tuhan yang tahu banyak cara seseorang dapat memutuskan untuk mengimplementasikan koleksi barang. Daftar tertaut? Daftar array? Tumpukan? Antre? Metode SearchAndReplace ini tidak peduli, itu hanya perlu melewati sesuatu yang dapat memanggil Tambah, Hapus, dan GetEnumerator aktif.
sumber
Saya semacam memperhatikan sendiri jawaban atas pertanyaan ini, untuk melihat apa yang orang lain katakan, tetapi saya akan mengatakan tiga hal yang cenderung saya pikirkan tentang ini:
Orang menaruh terlalu banyak iman ke dalam antarmuka, dan terlalu sedikit ke dalam kelas. Antarmuka pada dasarnya sangat dipermudah kelas dasar, dan ada kalanya menggunakan sesuatu yang ringan dapat menyebabkan hal-hal seperti kode boilerplate yang berlebihan.
Tidak ada yang salah dengan mengganti metode, memanggilnya
super.method()
, dan membiarkan seluruh tubuhnya melakukan hal-hal yang tidak mengganggu kelas dasar. Ini tidak melanggar Prinsip Pergantian Liskov .Sebagai aturan praktis, menjadi kaku dan dogmatis dalam rekayasa perangkat lunak adalah ide yang buruk, bahkan jika sesuatu umumnya merupakan praktik terbaik.
Jadi saya akan mengambil apa yang dikatakan dengan sebutir garam.
sumber
People put way too much faith into interfaces, and too little faith into classes.
Lucu. Saya cenderung berpikir sebaliknya.Saya ingin mengambil pendekatan bahasa dan semantik terhadap hal ini. Antarmuka tidak ada untuk
DRY
. Meskipun dapat digunakan sebagai mekanisme sentralisasi bersama dengan kelas abstrak yang mengimplementasikannya, itu tidak dimaksudkan untuk KERING.Antarmuka adalah kontrak.
IAnimal
antarmuka adalah kontrak semua-atau-tidak sama sekali antara buaya, kucing, anjing dan gagasan tentang binatang. Apa yang dikatakannya pada dasarnya adalah "jika Anda ingin menjadi binatang, Anda harus memiliki semua atribut dan karakteristik ini, atau Anda bukan binatang lagi".Warisan di sisi lain adalah mekanisme untuk digunakan kembali. Dalam hal ini, saya pikir memiliki alasan semantik yang baik dapat membantu Anda memutuskan metode mana yang harus digunakan. Misalnya, sementara semua hewan mungkin tidur, dan tidur sama, saya tidak akan menggunakan kelas dasar untuk itu. Saya pertama kali menempatkan
Sleep()
metode diIAnimal
antarmuka sebagai penekanan (semantik) untuk apa yang diperlukan untuk menjadi hewan, kemudian saya menggunakan kelas abstrak dasar untuk kucing, anjing, dan buaya sebagai teknik pemrograman untuk tidak mengulangi diri saya.sumber
Saya tidak yakin ini adalah aturan praktis terbaik di luar sana. Anda bisa meletakkan
abstract
metode di kelas dasar dan meminta setiap kelas menerapkannya. Karena setiap orangmammal
harus makan, masuk akal untuk melakukannya. Kemudian masingmammal
- masing dapat menggunakan satueat
metodenya. Saya biasanya hanya menggunakan antarmuka untuk komunikasi antara objek, atau sebagai penanda untuk menandai kelas sebagai sesuatu. Saya pikir antarmuka menggunakan terlalu banyak untuk kode ceroboh.Saya juga setuju dengan @Panzercrisis bahwa aturan praktis seharusnya hanya menjadi pedoman umum. Bukan sesuatu yang harus ditulis dalam batu. Tidak diragukan lagi akan ada saat-saat di mana ia tidak berfungsi.
sumber
Antarmuka adalah tipe. Mereka tidak memberikan implementasi. Mereka mendefinisikan kontrak untuk menangani jenis itu. Kontrak ini harus dipenuhi oleh kelas yang mengimplementasikan antarmuka.
Penggunaan antarmuka dan kelas abstrak / dasar harus ditentukan oleh persyaratan desain.
Satu kelas tidak memerlukan antarmuka.
Jika dua kelas dipertukarkan dalam suatu fungsi, mereka harus mengimplementasikan antarmuka umum. Fungsionalitas apa pun yang diimplementasikan secara identik kemudian dapat direactored menjadi kelas abstrak yang mengimplementasikan antarmuka. Antarmuka dapat dianggap berlebihan pada saat itu jika tidak ada fungsi yang membedakan. Tapi apa bedanya kedua kelas itu?
Menggunakan kelas abstrak sebagai tipe umumnya berantakan karena lebih banyak fungsi ditambahkan dan sebagai hierarki diperluas.
Komposisi nikmat daripada warisan - semua ini hilang.
sumber