Antarmuka dan Warisan: Terbaik dari kedua dunia?

10

Saya 'menemukan' antarmuka dan saya mulai menyukainya. Keindahan antarmuka adalah bahwa itu adalah kontrak, dan objek apa pun yang memenuhi kontrak itu dapat digunakan di mana pun antarmuka itu diperlukan.

Masalah dengan antarmuka adalah bahwa itu tidak dapat memiliki implementasi standar, yang merupakan rasa sakit untuk properti biasa dan mengalahkan KERING. Ini juga bagus, karena menjaga implementasi dan sistem dipisahkan. Warisan, di tangan, mempertahankan kopling yang lebih ketat, dan memiliki potensi melanggar enkapsulasi.

Kasus 1 (Warisan dengan anggota pribadi, enkapsulasi yang baik, sangat erat)

class Employee
{
int money_earned;
string name;

public:
 void do_work(){money_earned++;};
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work. Oops, can't update money_earned. Unaware I have to call superclass' do_work()*/);

};

void HireNurse(Nurse *n)
{
   nurse->do_work();
)

Kasus 2 (hanya antarmuka)

class IEmployee
{
     virtual void do_work()=0;
     virtual string get_name()=0;
};

//class Nurse implements IEmployee.
//But now, for each employee, must repeat the get_name() implementation,
//and add a name member string, which breaks DRY.

Kasus 3: (terbaik dari kedua dunia?)

Mirip dengan Kasus 1 . Namun, bayangkan bahwa (secara hipotetis) C ++ tidak mengizinkan metode utama kecuali metode-metode yang murni virtual .

Jadi, dalam Kasus 1 , mengesampingkan do_work () akan menyebabkan kesalahan waktu kompilasi. Untuk memperbaiki ini, kami menetapkan do_work () sebagai virtual murni, dan menambahkan metode terpisah increment_money_earned (). Sebagai contoh:

class Employee
{
int money_earned;
string name;

public:
 virtual void do_work()=0;
 void increment_money_earned(money_earned++;);
 string get_name(return name;);
};


class Nurse : public Employee: 
{
   public:
   void do_work(/*do work*/ increment_money_earned(); ); .
};

Tetapi bahkan ini memiliki masalah. Bagaimana jika 3 bulan dari sekarang, Joe Coder menciptakan Karyawan Dokter, tetapi ia lupa menelepon increment_money_earned () di do_work ()?


Pertanyaan:

  • Apakah Case 3 lebih unggul dari Case 1 ? Apakah karena 'enkapsulasi yang lebih baik' atau 'lebih longgar digabungkan', atau karena alasan lain?

  • Apakah Case 3 lebih unggul dari Case 2 karena sesuai dengan KERING?

MustafaM
sumber
2
... Apakah Anda menciptakan kembali kelas abstrak atau apa?
ZJR

Jawaban:

10

Salah satu cara untuk menyelesaikan masalah superclass yang lupa menelepon adalah dengan mengembalikan kendali ke superclass! Saya telah jigger contoh pertama Anda untuk menunjukkan bagaimana (dan membuatnya mengkompilasi;)). Oh, saya juga menganggap bahwa do_work()dalam Employeeseharusnya virtualdalam contoh pertama Anda.

#include <string>

using namespace std;

class Employee
{
    int money_earned;
    string name;
    virtual void on_do_work() {}

    public:
        void do_work() { money_earned++; on_do_work(); }
        string get_name() { return name; }
};

class Nurse : public Employee
{
    void on_do_work() { /* do more work. Oh, and I don't have to call do_work()! */ }
};

void HireNurse(Nurse* nurse)
{
    nurse->do_work();
}

Sekarang do_work()tidak dapat ditimpa. Jika Anda ingin memperpanjang, Anda harus melakukannya melalui on_do_work()yang do_work()memiliki kendali atas.

Ini, tentu saja, dapat digunakan dengan antarmuka dari contoh kedua Anda juga jika Employeediperluas. Jadi, jika saya mengerti Anda dengan benar saya pikir itu membuat Kasus 3 ini tetapi tanpa harus menggunakan C ++ hipotetis! KERING dan memiliki enkapsulasi yang kuat.

Gyan alias Gary Buyn
sumber
3
Dan itu adalah pola desain yang dikenal sebagai "metode templat" ( en.wikipedia.org/wiki/Template_method_pattern ).
Joris Timmermans
Ya, ini sesuai dengan Kasus 3. Ini terlihat menjanjikan. Akan memeriksa secara detail. Juga, ini adalah semacam sistem acara. Apakah ada nama untuk 'pola' ini?
MustafaM
@ MadKeithV apakah Anda yakin ini adalah 'metode templat'?
MustafaM
@illmath - ya, ini adalah metode publik nonvirtual yang mendelegasikan bagian detail penerapannya ke metode yang dilindungi virtual / privat.
Joris Timmermans
@illmath Saya tidak menganggapnya sebagai metode templat sebelumnya tapi saya percaya itu adalah contoh dasar dari satu. Saya baru saja menemukan artikel ini yang mungkin ingin Anda baca di mana penulis yakin itu pantas namanya sendiri: Non-Virtual Interface Idiom
Gyan aka Gary Buyn
1

Masalah dengan antarmuka adalah bahwa itu tidak dapat memiliki implementasi standar, yang merupakan rasa sakit untuk properti biasa dan mengalahkan KERING.

Menurut pendapat saya sendiri, antarmuka seharusnya hanya memiliki metode murni - tanpa implementasi standar. Itu tidak melanggar prinsip KERING dengan cara apa pun, karena antarmuka menunjukkan cara mengakses beberapa entitas. Hanya untuk referensi, saya melihat penjelasan KERING di sini :
"Setiap bagian dari pengetahuan harus memiliki representasi otoritatif tunggal, tidak ambigu, dalam suatu sistem."

Di sisi lain, SOLID memberi tahu Anda bahwa setiap kelas harus memiliki antarmuka.

Apakah Case 3 lebih unggul dari Case 1? Apakah karena 'enkapsulasi yang lebih baik' atau 'lebih longgar digabungkan', atau karena alasan lain?

Tidak, kasus 3 tidak lebih unggul daripada kasus 1. Anda harus mengambil keputusan. Jika Anda ingin memiliki implementasi standar maka lakukanlah. Jika Anda ingin metode murni maka ikutilah.

Bagaimana jika 3 bulan dari sekarang, Joe Coder menciptakan Karyawan Dokter, tetapi ia lupa menelepon increment_money_earned () di do_work ()?

Kemudian Joe Coder harus mendapatkan apa yang pantas diterimanya karena mengabaikan tes unit yang gagal. Dia memang menguji kelas ini, bukan? :)

Case mana yang terbaik untuk proyek perangkat lunak yang mungkin memiliki 40.000 baris kode?

Satu ukuran tidak cocok untuk semua. Tidak mungkin mengatakan mana yang lebih baik. Ada beberapa kasus di mana yang satu lebih cocok daripada yang lain.

Mungkin Anda harus mempelajari beberapa pola desain daripada mencoba menciptakan pola Anda sendiri.


Saya baru menyadari bahwa Anda mencari pola desain antarmuka non-virtual , karena seperti itulah tampilan case 3 class Anda.

BЈовић
sumber
Terima kasih atas komentarnya. Saya telah memperbarui Kasus 3 untuk membuat maksud saya lebih jelas.
MustafaM
1
Saya harus -1 Anda di sini. Tidak ada alasan sama sekali untuk mengatakan bahwa semua antarmuka harus murni, atau bahwa semua kelas harus diwarisi dari antarmuka.
DeadMG
@DeadMG ISP
BЈовић
@VJovic: Ada perbedaan besar antara SOLID dan "Semuanya harus diwarisi dari antarmuka".
DeadMG
"Satu ukuran tidak cocok untuk semua" dan "pelajari beberapa pola desain" benar - sisa jawaban Anda melanggar saran Anda sendiri bahwa satu ukuran tidak cocok untuk semua.
Joris Timmermans
0

Antarmuka dapat memiliki implementasi default di C ++. Tidak ada yang mengatakan bahwa implementasi default fungsi tidak hanya tergantung pada anggota virtual lainnya (dan argumen), jadi tidak meningkatkan segala jenis kopling.

Untuk kasus 2, KERING menggantikan di sini. Enkapsulasi ada untuk melindungi program Anda dari perubahan, dari implementasi yang berbeda, tetapi dalam hal ini, Anda tidak memiliki implementasi yang berbeda. Jadi enkapsulasi YAGNI.

Bahkan, antarmuka run-time biasanya dianggap lebih rendah daripada setara dengan waktu kompilasi mereka. Dalam kasus saat kompilasi, Anda dapat memiliki baik kasus 1 dan kasus 2 di bundle- yang sama tidak menyebutkan itu banyak keuntungan lainnya. Atau bahkan pada saat run-time, Anda dapat melakukan Employee : public IEmployeekeuntungan yang sama secara efektif. Ada banyak cara untuk menangani hal-hal seperti itu.

Case 3: (best of both worlds?)

Similar to Case 1. However, imagine that (hypothetically)

Saya berhenti membaca. YAGNI. C ++ adalah apa itu C ++, dan komite Standar tidak pernah, akan mengimplementasikan perubahan seperti itu, untuk alasan yang sangat baik.

DeadMG
sumber
Anda mengatakan "Anda tidak memiliki implementasi yang berbeda". Tapi saya lakukan. Saya memiliki Perawat implementasi Karyawan, dan saya mungkin memiliki implementasi lain nanti (Dokter, Petugas Kebersihan, dll). Saya telah memperbarui Kasus 3 untuk membuatnya lebih jelas apa yang saya maksud.
MustafaM
@illmath: Tetapi Anda tidak memiliki implementasi dari get_name. Semua implementasi yang Anda usulkan akan berbagi implementasi yang sama dari get_name. Selain itu, seperti yang saya katakan, tidak ada alasan untuk memilih, Anda dapat memiliki keduanya. Juga, Kasus 3 sama sekali tidak berharga. Anda dapat mengganti virtual yang tidak murni, jadi lupakan desain yang tidak bisa Anda gunakan.
DeadMG
Tidak hanya antarmuka memiliki implementasi standar dalam C ++, antarmuka juga dapat memiliki implementasi standar dan masih abstrak! yaitu virtual void IMethod () = 0 {std :: cout << "Ni!" << std :: endl; }
Joris Timmermans
@ MadKeithV: Saya tidak percaya bahwa Anda dapat mendefinisikan mereka sebaris, tetapi intinya masih sama.
DeadMG
@ MadKeith: Seolah Visual Studio pernah menjadi representasi akurat Standar C ++.
DeadMG
0

Apakah Case 3 lebih unggul dari Case 1? Apakah karena 'enkapsulasi yang lebih baik' atau 'lebih longgar digabungkan', atau karena alasan lain?

Dari apa yang saya lihat dalam implementasi Anda, implementasi Case 3 Anda membutuhkan kelas abstrak yang dapat mengimplementasikan metode virtual murni yang kemudian dapat diubah di kelas turunan. Kasus 3 akan lebih baik karena kelas turunan dapat mengubah implementasi do_work ketika dan ketika diperlukan dan semua turunan turunan pada dasarnya akan menjadi milik tipe abstrak dasar.

Case mana yang terbaik untuk proyek perangkat lunak yang mungkin memiliki 40.000 baris kode.

Saya akan mengatakan itu murni tergantung pada desain implementasi Anda dan tujuan yang ingin Anda capai. Kelas abstrak dan Antarmuka diimplementasikan berdasarkan masalah yang harus dipecahkan.

Edit pertanyaan

Bagaimana jika 3 bulan dari sekarang, Joe Coder menciptakan Karyawan Dokter, tetapi ia lupa menelepon increment_money_earned () di do_work ()?

Tes unit dapat dilakukan untuk memeriksa apakah setiap kelas mengkonfirmasi perilaku yang diharapkan. Jadi, jika unit test yang tepat diterapkan, bug dapat dicegah ketika Joe Coder mengimplementasikan kelas baru.

Karthik Sreenivasan
sumber
0

Menggunakan antarmuka hanya memecah KERING jika setiap implementasi adalah duplikat dari yang lain. Anda dapat menyelesaikan dilema ini dengan menerapkan antarmuka dan warisan, namun ada beberapa kasus di mana Anda mungkin ingin mengimplementasikan antarmuka yang sama pada sejumlah kelas, tetapi memvariasikan perilaku di setiap kelas, dan ini akan tetap berpegang pada prinsip dari KERING. Apakah Anda memilih untuk menggunakan salah satu dari 3 pendekatan yang telah Anda uraikan, turun ke pilihan yang Anda butuhkan untuk menerapkan teknik terbaik untuk mencocokkan situasi tertentu. Di sisi lain, Anda mungkin akan menemukan bahwa seiring waktu, Anda lebih banyak menggunakan Antarmuka, dan menerapkan pewarisan hanya jika Anda ingin menghapus pengulangan. Itu bukan untuk mengatakan bahwa ini adalah satu - satunya alasan pewarisan, tetapi lebih baik meminimalkan penggunaan warisan untuk memungkinkan Anda menjaga opsi tetap terbuka jika Anda merasa desain Anda perlu diubah nanti, dan jika Anda ingin meminimalkan dampak pada kelas turunan dari efek yang diubah. akan memperkenalkan di kelas induk.

S.Robins
sumber