Apakah kelas / metode abstrak sudah usang?

37

Saya biasa membuat banyak kelas / metode abstrak. Kemudian saya mulai menggunakan antarmuka.

Sekarang saya tidak yakin apakah antarmuka tidak membuat kelas abstrak menjadi usang.

Anda membutuhkan kelas yang sepenuhnya abstrak? Buat antarmuka sebagai gantinya. Anda memerlukan kelas abstrak dengan beberapa implementasi di dalamnya? Buat antarmuka, buat kelas. Mewarisi kelas, mengimplementasikan antarmuka. Manfaat tambahan adalah bahwa beberapa kelas mungkin tidak memerlukan kelas induk, tetapi hanya akan mengimplementasikan antarmuka.

Jadi, apakah kelas / metode abstrak sudah usang?

Boris Yankov
sumber
Bagaimana jika bahasa pemrograman pilihan Anda tidak mendukung antarmuka? Saya ingat ini kasus C ++.
Bernard
7
@Bernard: di C ++, kelas abstrak adalah antarmuka di semua kecuali nama. Bahwa mereka juga dapat melakukan lebih dari antarmuka 'murni' bukanlah suatu kerugian.
gbjbaanb
@ gbjbaanb: Saya kira. Saya tidak ingat menggunakannya sebagai antarmuka, melainkan untuk memberikan implementasi standar.
Bernard
Antarmuka adalah "mata uang" dari referensi objek. Secara umum mereka adalah dasar dari perilaku polimorfik. Kelas abstrak memiliki tujuan berbeda yang dijelaskan deadalnix dengan sempurna.
jiggy
4
Bukankah ini seperti mengatakan "apakah moda transportasi sudah usang sekarang kita memiliki mobil?" Ya, sebagian besar waktu, Anda menggunakan mobil. Tetapi apakah Anda membutuhkan sesuatu selain mobil atau tidak, itu tidak benar untuk mengatakan "Saya tidak perlu menggunakan moda transportasi". Antarmuka hampir sama dengan kelas abstrak tanpa implementasi apa pun, dan dengan nama khusus, bukan?
Jack V.

Jawaban:

111

Tidak.

Antarmuka tidak dapat memberikan implementasi standar, kelas abstrak dan metode bisa. Ini sangat berguna untuk menghindari duplikasi kode dalam banyak kasus.

Ini juga cara yang sangat bagus untuk mengurangi kopling berurutan. Tanpa metode / kelas abstrak, Anda tidak dapat menerapkan pola metode templat. Saya sarankan Anda melihat artikel wikipedia ini: http://en.wikipedia.org/wiki/Template_method_pattern

deadalnix
sumber
3
Pola metode template @deadalnix bisa sangat berbahaya. Anda dapat dengan mudah berakhir dengan kode yang sangat berpasangan, dan mulai meretas kode untuk memperluas kelas abstrak templated untuk menangani "hanya satu kasus lagi".
quant_dev
9
Ini tampaknya lebih seperti anti-pola. Anda bisa mendapatkan hal yang persis sama dengan komposisi terhadap antarmuka. Hal yang sama berlaku untuk kelas parsial dengan beberapa metode abstrak. Alih-alih memaksa kode klien untuk subkelas dan menimpanya, Anda harus menyuntikkan implementasinya . Lihat: en.wikipedia.org/wiki/Composition_over_inheritance#Manfaat
back2dos
4
Ini sama sekali tidak menjelaskan bagaimana mengurangi sekuensial kopling. Saya setuju bahwa ada beberapa cara untuk mencapai tujuan ini, tapi tolong, jawab masalah yang Anda bicarakan alih-alih secara membujuk memohon beberapa prinsip. Anda akhirnya akan melakukan pemrograman kultus kargo jika Anda tidak dapat menjelaskan mengapa ini terkait dengan masalah aktual.
deadalnix
3
@deadalnix: Mengatakan bahwa penggiliran sekuensial paling baik dikurangi dengan pola templat adalah pemrograman pemujaan kargo (menggunakan pola negara / strategi / pabrik mungkin juga berfungsi), seperti asumsi bahwa ia harus selalu dikurangi. Penggabungan berurutan seringkali merupakan konsekuensi belaka dari memperlihatkan kontrol yang sangat halus atas sesuatu, yang tidak berarti apa-apa selain pertukaran. Itu masih tidak berarti saya tidak bisa menulis pembungkus yang tidak memiliki kopling berurutan menggunakan komposisi. Bahkan, ini membuatnya jauh lebih mudah.
back2dos
4
Java sekarang memiliki implementasi standar untuk antarmuka. Apakah itu mengubah jawabannya?
raptortech97
15

Ada metode abstrak sehingga Anda bisa memanggilnya dari dalam kelas dasar Anda tetapi menerapkannya dalam kelas turunan. Jadi kelas dasar Anda tahu:

public void DoTask()
{
    doSetup();
    DoWork();
    doCleanup();
}

protected abstract void DoWork();

Itu cara yang cukup bagus untuk menerapkan lubang di pola tengah tanpa kelas turunan mengetahui tentang pengaturan dan kegiatan pembersihan. Tanpa metode abstrak, Anda harus bergantung pada kelas turunan yang mengimplementasikan DoTaskdan mengingat panggilan base.DoSetup()dan base.DoCleanup()sepanjang waktu.

Edit

Juga, terima kasih kepada deadalnix karena memposting tautan ke Pola Metode Templat , yang telah saya jelaskan di atas tanpa benar-benar mengetahui namanya. :)

Scott Whitlock
sumber
2
+1, saya lebih suka jawaban Anda dari deadalnix '. Perhatikan bahwa Anda dapat menerapkan metode templat dengan cara yang lebih mudah menggunakan delegasi (dalam C #):public void DoTask(Action doWork)
Joh
1
@ Jim - benar, tapi itu belum tentu bagus, tergantung API Anda. Jika kelas dasar Anda Fruitdan kelas turunan Anda adalah Apple, Anda ingin menelepon myApple.Eat(), bukan myApple.Eat((a) => howToEatApple(a)). Juga, Anda tidak ingin Appleharus menelepon base.Eat(() => this.howToEatMe()). Saya pikir lebih bersih hanya untuk mengganti metode abstrak.
Scott Whitlock
1
@Scott Whitlock: Contoh Anda menggunakan apel dan buah-buahan agak terlalu jauh dari kenyataan untuk menilai apakah pergi untuk warisan lebih baik daripada delegasi pada umumnya. Jelas, jawabannya adalah "itu tergantung", sehingga menyisakan banyak ruang untuk debat ... Lagi pula, saya menemukan bahwa metode template banyak terjadi selama refactoring, misalnya ketika menghapus kode duplikat. Dalam kasus seperti itu, saya biasanya tidak ingin dipusingkan dengan hierarki tipe, dan saya lebih memilih untuk menjauh dari warisan. Operasi semacam ini lebih mudah dilakukan dengan lambdas.
Yoh
Pertanyaan ini menggambarkan lebih dari sekedar aplikasi sederhana dari pola Metode Templat - aspek publik / non-publik dikenal di komunitas C ++ sebagai pola Non Virtual Interface . Dengan memiliki fungsi publik non-virtual membungkus yang virtual - bahkan jika awalnya tidak melakukan apa-apa selain memanggil yang terakhir - Anda meninggalkan titik penyesuaian untuk pengaturan / pembersihan, logging, profiling, pemeriksaan keamanan dll yang mungkin diperlukan nanti.
Tony
10

Tidak, Mereka tidak usang.

Bahkan, ada perbedaan yang tidak jelas tetapi mendasar antara Kelas / Metode Abstrak dan Antarmuka.

jika himpunan kelas di mana salah satu dari ini harus digunakan memiliki perilaku umum yang mereka bagikan (kelas terkait, maksud saya), kemudian pergi untuk kelas / metode abstrak.

Contoh: juru tulis, Petugas, Direktur -semua kelas ini memiliki CalculateSalary () yang sama, menggunakan kelas basis abstrak. MenghitungSalary () tidak dapat diimplementasikan secara berbeda tetapi ada beberapa hal lain seperti GetAttendance () misalnya yang memiliki definisi umum di kelas dasar.

Jika kelas Anda tidak memiliki kesamaan (kelas yang tidak terkait, dalam konteks yang dipilih) di antara mereka tetapi memiliki tindakan yang sangat berbeda dalam implementasi, maka pergi untuk Antarmuka.

Contoh: sapi, bangku, mobil, kelas yang tidak terkait teleskop tetapi Isortable dapat berada di sana untuk mengurutkannya dalam sebuah array.

Perbedaan ini biasanya diabaikan ketika didekati dari perspektif polimorfik. Tetapi saya pribadi merasa bahwa ada situasi di mana seseorang lebih tepat daripada yang lain karena alasan yang dijelaskan di atas.

WinW
sumber
6

Selain jawaban yang baik lainnya, ada perbedaan mendasar antara antarmuka dan kelas abstrak yang tidak ada yang disebutkan secara khusus, yaitu bahwa antarmuka jauh lebih tidak dapat diandalkan dan karenanya membebani beban tes yang jauh lebih besar daripada kelas abstrak. Sebagai contoh, pertimbangkan kode C # ini:

public abstract class Frobber
{
    private Frobber() {}
    public abstract void Frob(Frotz frotz);
    private class GreenFrobber : Frobber
    { ... }
    private class RedFrobber : Frobber
    { ... }
    public static Frobber GetFrobber(bool b) { ... } // return a green or red frobber
}

public sealed class Frotz
{
    public void Frobbit(Frobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Saya dijamin hanya ada dua jalur kode yang perlu saya uji. Penulis Frobbit dapat mengandalkan fakta bahwa frobber berwarna merah atau hijau.

Jika sebaliknya kita katakan:

public interface IFrobber
{
    void Frob(Frotz frotz);
}
public class GreenFrobber : IFrobber
{ ... }
public class RedFrobber : Frobber
{ ... }

public sealed class Frotz
{
    public void Frobbit(IFrobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Saya sekarang benar - benar tidak tahu apa - apa tentang efek panggilan itu kepada Frob di sana. Saya perlu memastikan bahwa semua kode dalam Frobbit kuat terhadap kemungkinan implementasi IFrobber , bahkan implementasi oleh orang-orang yang tidak kompeten (buruk) atau secara aktif memusuhi saya atau pengguna saya (jauh lebih buruk).

Kelas abstrak memungkinkan Anda untuk menghindari semua masalah ini; gunakan mereka!

Eric Lippert
sumber
1
Masalah yang Anda bicarakan harus diselesaikan dengan menggunakan tipe data aljabar, daripada membengkokkan kelas ke titik di mana mereka melanggar prinsip buka / tutup.
back2dos
1
Pertama, saya tidak pernah mengatakan itu baik atau bermoral . Kau taruh itu di mulutku. Kedua (menjadi poin saya yang sebenarnya): Ini hanyalah alasan yang buruk untuk fitur bahasa yang hilang. Terakhir, ada solusi tanpa kelas abstrak: pastebin.com/DxEh8Qfz . Berbeda dengan itu, pendekatan Anda menggunakan kelas bersarang dan abstrak, membuang semua kecuali kode yang membutuhkan keamanan bersama ke dalam bola lumpur yang besar. Tidak ada alasan bagus mengapa RedFrobber atau GreenFrobber harus dikaitkan dengan kendala yang ingin Anda tegakkan. Ini meningkatkan sambungan dan mengunci dalam banyak keputusan tanpa manfaat apa pun.
back2dos
1
Mengatakan bahwa metode abstrak sudah usang adalah salah tanpa keraguan, tetapi mengklaim bahwa mereka harus lebih disukai daripada antarmuka adalah salah arah. Mereka hanyalah alat untuk memecahkan masalah yang berbeda dari antarmuka.
Groo
2
"Ada perbedaan mendasar [...] antarmuka [...] jauh lebih tidak dapat diandalkan daripada kelas abstrak". Saya tidak setuju, perbedaan yang Anda gambarkan dalam kode Anda bergantung pada pembatasan akses, yang menurut saya tidak memiliki alasan untuk berbeda secara signifikan antara antarmuka dan kelas abstrak. Mungkin demikian dalam C #, tetapi pertanyaannya adalah bahasa-agnostik.
Yoh
1
@ back2dos: Saya hanya ingin menunjukkan bahwa solusi Eric, pada kenyataannya, menggunakan tipe data aljabar: kelas abstrak adalah tipe penjumlahan. Memanggil metode abstrak sama dengan pencocokan pola pada serangkaian varian.
Rodrick Chapman
4

Anda mengatakannya sendiri:

Anda memerlukan kelas abstrak dengan beberapa implementasi di dalamnya? Buat antarmuka, buat kelas. Mewarisi kelas, mengimplementasikan antarmuka

itu terdengar banyak pekerjaan dibandingkan dengan 'mewarisi kelas abstrak'. Anda dapat bekerja untuk diri sendiri dengan mendekati kode dari pandangan 'purist', tetapi saya merasa sudah cukup untuk melakukannya tanpa mencoba menambah beban kerja saya tanpa manfaat praktis.

gbjbaanb
sumber
Belum lagi jika kelas tidak mewarisi antarmuka (yang tidak disebutkan seharusnya), Anda harus menulis fungsi penerusan dalam beberapa bahasa.
Sjoerd
Umm, "Lot of work" tambahan yang Anda sebutkan adalah bahwa Anda membuat antarmuka dan membuat kelas mengimplementasikannya - apakah itu benar-benar tampak seperti banyak pekerjaan? Selain itu, tentu saja, antarmuka memberi Anda kemampuan untuk mengimplementasikan antarmuka dari hierarki baru yang tidak akan berfungsi dengan kelas dasar abstrak.
Bill K
4

Seperti yang saya komentari pada postingan @deadnix: Implementasi parsial adalah anti-pola, meskipun pada kenyataannya pola template memformalkannya.

Solusi bersih untuk contoh wikipedia ini dari pola templat :

interface Game {
    void initialize(int playersCount);
    void makePlay(int player);
    boolean done();
    void finished();
    void printWinner();
}
class GameRunner {
    public void playOneGame(int playersCount, Game game) {
        game.initialize(playersCount);
        int j = 0;
        for (int i = 0; !game.finished(); i++)
             game.makePlay(i % playersCount);
        game.printWinner();
    }
} 
class Monopoly implements Game {
     //... implementation
}

Solusi ini lebih baik, karena menggunakan komposisi daripada pewarisan . Pola templat memperkenalkan ketergantungan antara penerapan aturan Monopoli dan penerapan cara permainan dijalankan. Namun ini adalah dua tanggung jawab yang sama sekali berbeda dan tidak ada alasan yang baik untuk memasangkannya.

back2dos
sumber
+1. Sebagai catatan tambahan: "Implementasi sebagian adalah anti-pola, meskipun pola templat memformalkannya." Deskripsi pada wikipedia mendefinisikan polanya dengan bersih, hanya contoh kode yang "salah" (dalam arti bahwa ia menggunakan warisan ketika tidak diperlukan dan ada alternatif yang lebih sederhana, seperti yang diilustrasikan di atas). Dengan kata lain, saya tidak berpikir pola itu sendiri yang harus disalahkan, hanya cara orang cenderung menerapkannya.
Yoh
2

Tidak. Bahkan alternatif yang Anda usulkan termasuk penggunaan kelas abstrak. Selain itu, karena Anda tidak menentukan bahasa, maka saya akan langsung saja dan mengatakan bahwa kode generik adalah pilihan yang lebih baik daripada warisan rapuh. Kelas abstrak memiliki keunggulan signifikan dibandingkan antarmuka.

DeadMG
sumber
-1. Saya tidak mengerti apa yang harus dilakukan dengan "kode generik vs warisan" dengan pertanyaan itu. Anda harus mengilustrasikan atau membenarkan mengapa "kelas abstrak memiliki keunggulan signifikan dibandingkan antarmuka".
Yoh
1

Kelas abstrak bukan interface. Mereka adalah kelas yang tidak bisa dipakai.

Anda membutuhkan kelas yang sepenuhnya abstrak? Buat antarmuka sebagai gantinya. Anda memerlukan kelas abstrak dengan beberapa implementasi di dalamnya? Buat antarmuka, buat kelas. Mewarisi kelas, mengimplementasikan antarmuka. Manfaat tambahan adalah bahwa beberapa kelas mungkin tidak memerlukan kelas induk, tetapi hanya akan mengimplementasikan antarmuka.

Tapi kemudian Anda akan memiliki kelas tidak berguna yang tidak abstrak. Metode abstrak diperlukan untuk mengisi lubang fungsionalitas di kelas dasar.

Misalnya diberikan kelas ini

public abstract class Frobber {
    public abstract void Frob();

    public abstract boolean IsFrobbingNeeded { get; }

    public void FrobUntilFinished() {
        while (IsFrobbingNeeded) {
            Frob();
        }
    }
}

Bagaimana Anda menerapkan fungsionalitas dasar ini di kelas yang tidak memiliki Frob()atau tidak IsFrobbingNeeded?

konfigurator
sumber
1
Antarmuka juga tidak bisa dipakai.
Sjoerd
@ Soerd: Tapi Anda membutuhkan kelas dasar dengan implementasi bersama; itu tidak bisa berupa antarmuka.
konfigurator
1

Saya adalah pencipta kerangka servlet di mana kelas abstrak memainkan peran penting. Saya akan mengatakan lebih banyak, saya perlu metode semi abstrak, ketika sebuah metode perlu diganti dalam 50% kasus dan saya ingin melihat peringatan dari kompiler tentang metode yang tidak diganti. Saya mengatasi masalah menambahkan anotasi. Kembali ke pertanyaan Anda, ada dua perbedaan penggunaan kelas abstrak dan antarmuka, dan sejauh ini tidak ada yang usang.

Dmitriy R
sumber
0

Saya tidak berpikir mereka usang oleh antarmuka, tetapi mereka mungkin usang oleh pola strategi.

Penggunaan utama dari kelas abstrak adalah untuk menunda bagian dari implementasi; untuk mengatakan, "ini bagian dari implementasi kelas dapat dibuat berbeda".

Sayangnya, kelas abstrak memaksa klien untuk melakukan ini melalui warisan . Sedangkan pola strategi akan memungkinkan Anda untuk mencapai hasil yang sama tanpa warisan apa pun. Klien dapat membuat instance kelas Anda daripada selalu mendefinisikan sendiri, dan "implementasi" kelas (perilaku) kelas dapat bervariasi secara dinamis. Pola strategi memiliki tambahan plus bahwa perilaku dapat diubah pada saat run-time, tidak hanya pada waktu desain, dan juga memiliki kopling yang lebih lemah secara signifikan antara jenis yang terlibat.

Dave Cousineau
sumber
0

Saya secara umum menghadapi jauh, jauh, lebih banyak masalah pemeliharaan yang terkait dengan antarmuka murni daripada ABC, bahkan ABC digunakan dengan banyak pewarisan. YMMV - tidak tahu, mungkin tim kami hanya menggunakannya secara tidak memadai.

Yang mengatakan, jika kita menggunakan analogi dunia nyata, berapa banyak penggunaan yang ada untuk antarmuka murni sepenuhnya tanpa fungsi dan keadaan? Jika saya menggunakan USB sebagai contoh, itu adalah antarmuka yang cukup stabil (saya pikir kita berada di USB 3.2 sekarang, tetapi juga mempertahankan kompatibilitas ke belakang).

Namun itu bukan antarmuka stateless. Ini bukan tanpa fungsi. Ini lebih seperti kelas dasar abstrak daripada antarmuka murni. Ini sebenarnya lebih dekat ke kelas beton dengan persyaratan fungsional dan negara yang sangat spesifik, dengan satu-satunya abstraksi adalah apa yang dihubungkan ke port menjadi satu-satunya bagian yang dapat diganti.

Kalau tidak, itu hanya akan menjadi "lubang" di komputer Anda dengan faktor bentuk standar dan persyaratan fungsional yang jauh lebih longgar yang tidak akan melakukan apa pun sampai setiap produsen membuat perangkat keras sendiri untuk membuat lubang itu melakukan sesuatu, pada titik mana itu menjadi standar yang jauh lebih lemah dan tidak lebih dari "lubang" dan spesifikasi apa yang harus dilakukan, tetapi tidak ada ketentuan utama tentang bagaimana melakukannya. Sementara itu kita mungkin berakhir dengan 200 cara berbeda untuk melakukannya setelah semua produsen perangkat keras mencoba menemukan cara mereka sendiri untuk melampirkan fungsionalitas dan menyatakan "lubang" itu.

Dan pada saat itu kami mungkin memiliki produsen tertentu yang memperkenalkan masalah berbeda dari yang lain. Jika kita perlu memperbarui spesifikasi kita mungkin memiliki 200 implementasi port USB konkret yang berbeda dengan cara yang sama sekali berbeda untuk menangani spesifikasi yang harus diperbarui dan diuji. Beberapa produsen mungkin mengembangkan implementasi standar de facto yang mereka bagikan di antara mereka sendiri (kelas dasar analog Anda yang mengimplementasikan antarmuka itu), tetapi tidak semua. Beberapa versi mungkin lebih lambat dari yang lain. Beberapa mungkin memiliki throughput yang lebih baik tetapi latensi yang lebih buruk atau sebaliknya. Beberapa mungkin menggunakan daya baterai lebih dari yang lain. Beberapa mungkin terkelupas dan tidak berfungsi dengan semua perangkat keras yang seharusnya berfungsi dengan port USB. Beberapa mungkin memerlukan reaktor nuklir untuk dipasang untuk beroperasi yang memiliki kecenderungan untuk memberikan keracunan radiasi kepada penggunanya.

Dan itulah yang saya temukan, secara pribadi, dengan antarmuka murni. Mungkin ada beberapa kasus di mana mereka masuk akal, seperti hanya untuk memodelkan faktor bentuk motherboard terhadap casing CPU. Analogi faktor bentuk, memang, cukup banyak tanpa kewarganegaraan dan tanpa fungsi, seperti "lubang" analogis. Tetapi saya sering menganggap itu adalah kesalahan besar bagi tim untuk menganggap itu entah bagaimana unggul dalam semua kasus, bahkan tidak menutup.

Sebaliknya, saya pikir jauh lebih banyak kasus akan diselesaikan lebih baik oleh ABC daripada antarmuka jika itu adalah dua pilihan kecuali tim Anda begitu besar sehingga sebenarnya diinginkan untuk memiliki analog yang setara dengan 200 implementasi USB yang bersaing daripada satu standar pusat untuk. mempertahankan. Dalam tim mantan saya di, saya benar-benar harus berjuang keras hanya untuk melonggarkan standar pengkodean untuk memungkinkan ABC dan pewarisan berganda, dan terutama dalam menanggapi masalah pemeliharaan yang dijelaskan di atas.


sumber