Warisan dalam C ++: pemula, menengah, lanjutan

Artikel ini menjelaskan warisan pada tiga tingkat: pemula, menengah, dan lanjutan. Pakar no. Dan tidak sepatah kata pun tentang SOLID. Jujur.


Pemula


Apa itu warisan?


Warisan adalah salah satu prinsip dasar OOP. Menurutnya, sebuah kelas dapat menggunakan variabel dan metode dari kelas lain sebagai miliknya.


Kelas yang mewarisi data disebut subclass, kelas turunan, atau kelas anak. Kelas dari mana data atau metode diwarisi disebut kelas super, kelas dasar, atau kelas induk. Istilah "orang tua" dan "anak" sangat berguna untuk memahami warisan. Ketika seorang anak menerima karakteristik orang tuanya, kelas turunannya menerima metode dan variabel dari kelas dasar.


Warisan berguna karena memungkinkan Anda untuk menyusun dan menggunakan kembali kode, yang pada gilirannya secara signifikan dapat mempercepat proses pengembangan. Meskipun demikian, pewarisan harus digunakan dengan hati-hati, karena sebagian besar perubahan dalam superclass akan mempengaruhi semua subclass, yang dapat menyebabkan konsekuensi yang tidak terduga.


Dalam contoh ini, metode turn_on() dan variabel serial_number tidak dideklarasikan atau didefinisikan dalam subkelas Computer . Namun, mereka dapat digunakan karena mereka diwarisi dari kelas dasar.


Catatan Penting : Variabel dan metode pribadi tidak dapat diwariskan.


 #include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } private: int pincode = 87654321; }; class Computer: public Device {}; int main() { Computer Computer_instance; Computer_instance.turn_on(); cout << "Serial number is: " << Computer_instance.serial_number << endl; // cout << "Pin code is: " << Computer_instance.pincode << endl; // will cause compile time error return 0; } 

Jenis-Jenis Warisan


Ada beberapa jenis warisan di C ++:


  • publik - publik ( public ) dan data yang dilindungi ( protected ) diwariskan tanpa mengubah tingkat aksesnya;
  • dilindungi ( protected ) - semua data yang diwariskan menjadi terlindungi;
  • pribadi - semua data yang diwariskan menjadi pribadi.

Untuk kelas dasar Device , tingkat akses data tidak berubah, tetapi karena kelas yang diturunkan Computer mewarisi data sebagai pribadi, data menjadi pribadi untuk kelas Computer .


 #include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } }; class Computer: private Device { public: void say_hello() { turn_on(); cout << "Welcome to Windows 95!" << endl; } }; int main() { Device Device_instance; Computer Computer_instance; cout << "\t Device" << endl; cout << "Serial number is: "<< Device_instance.serial_number << endl; Device_instance.turn_on(); // cout << "Serial number is: " << Computer_instance.serial_number << endl; // Computer_instance.turn_on(); // will cause compile time error cout << "\t Computer" << endl; Computer_instance.say_hello(); return 0; } 

Kelas Computer sekarang menggunakan metode turn_on() seperti metode pribadi: turn_on() dapat dipanggil dari dalam kelas, tetapi mencoba memanggilnya langsung dari main akan menghasilkan kesalahan pada waktu kompilasi. Untuk Device kelas dasar, metode turn_on() tetap bersifat publik, dan dapat dipanggil dari main .


Konstruktor dan destruktor


Dalam C ++, konstruktor dan destruktor tidak diwarisi. Namun, mereka dipanggil ketika kelas anak menginisialisasi objeknya. Konstruktor disebut secara hierarkis satu demi satu, dimulai dengan kelas dasar dan diakhiri dengan kelas turunan terakhir. Destructors disebut dalam urutan terbalik.


Catatan Penting: Artikel ini tidak mencakup destruktor virtual. Materi tambahan tentang topik ini dapat ditemukan, misalnya, dalam artikel ini tentang Habr .


 #include <iostream> using namespace std; class Device { public: // constructor Device() { cout << "Device constructor called" << endl; } // destructor ~Device() { cout << "Device destructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } ~Computer() { cout << "Computer destructor called" << endl; } }; class Laptop: public Computer { public: Laptop() { cout << "Laptop constructor called" << endl; } ~Laptop() { cout << "Laptop destructor called" << endl; } }; int main() { cout << "\tConstructors" << endl; Laptop Laptop_instance; cout << "\tDestructors" << endl; return 0; } 

Konstruktor: Device -> Computer -> Laptop .
Destructors: Laptop -> Computer -> Device .


Warisan berganda


Warisan berganda terjadi ketika subkelas memiliki dua atau lebih superclasses. Dalam contoh ini, kelas Laptop mewarisi Monitor dan Computer bersamaan.


 #include <iostream> using namespace std; class Computer { public: void turn_on() { cout << "Welcome to Windows 95" << endl; } }; class Monitor { public: void show_image() { cout << "Imagine image here" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.show_image(); return 0; } 

Berbagai Masalah Warisan


Warisan berganda membutuhkan desain yang cermat, karena dapat mengakibatkan konsekuensi yang tidak terduga. Sebagian besar konsekuensi ini disebabkan oleh ambiguitas dalam warisan. Dalam contoh ini, Laptop mewarisi metode turn_on() dari kedua orang tua dan tidak jelas metode mana yang harus dipanggil.


 #include <iostream> using namespace std; class Computer { private: void turn_on() { cout << "Computer is on." << endl; } }; class Monitor { public: void turn_on() { cout << "Monitor is on." << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; } 

Terlepas dari kenyataan bahwa data pribadi tidak diwariskan, tidak mungkin untuk menyelesaikan warisan yang ambigu dengan mengubah tingkat akses ke data menjadi pribadi. Ketika mengkompilasi, pertama pencarian metode atau variabel terjadi, dan setelah itu, memeriksa tingkat akses ke mereka.


Menengah


Masalah belah ketupat



Masalah berlian adalah masalah klasik dalam bahasa yang mendukung banyak pewarisan. Masalah ini terjadi ketika kelas B dan C mewarisi A , dan kelas D mewarisi B dan C


Misalnya, kelas A , B dan C menentukan metode print_letter() . Jika print_letter() akan dipanggil oleh kelas D , tidak jelas metode mana yang harus disebut - metode kelas A , B atau C Bahasa yang berbeda memiliki pendekatan yang berbeda untuk menyelesaikan masalah berbentuk berlian. Dalam C ++, solusi untuk masalah diserahkan kepada kebijaksanaan programmer.

Masalah berbentuk berlian terutama adalah masalah desain, dan harus disediakan pada tahap desain. Pada tahap pengembangan, dapat diselesaikan sebagai berikut:


  • sebut metode superclass tertentu;
  • merujuk ke objek subclass sebagai objek dari superclass tertentu;
  • menimpa metode bermasalah di kelas anak terakhir (dalam kode, turn_on() di subkelas Laptop ).

 #include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } }; class Computer: public Device {}; class Monitor: public Device {}; class Laptop: public Computer, public Monitor { /* public: void turn_on() { cout << "Laptop is on." << endl; } // uncommenting this function will resolve diamond problem */ }; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast<Monitor&>( Laptop_instance ).turn_on(); return 0; } 

Jika metode turn_on() belum diganti di Laptop, memanggil Laptop_instance.turn_on() akan menghasilkan kesalahan kompilasi. Objek Laptop dapat mengakses dua turn_on() metode turn_on() secara bersamaan: Device:Computer:Laptop.turn_on() dan Device:Monitor:Laptop.turn_on() .


Masalah Intan: Konstruktor dan Destruktor


Karena dalam C ++, ketika objek kelas anak diinisialisasi, konstruktor dari semua kelas induk dipanggil, masalah lain muncul: konstruktor Device kelas dasar akan dipanggil dua kali.


 #include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; return 0; } 

Warisan virtual


Warisan virtual mencegah beberapa objek kelas dasar agar tidak muncul dalam hierarki pewarisan. Jadi, konstruktor Device kelas dasar akan dipanggil hanya sekali, dan mengakses metode turn_on() tanpa turn_on() di kelas anak tidak akan menyebabkan kesalahan kompilasi.


 #include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } void turn_on() { cout << "Device is on." << endl; } }; class Computer: virtual public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: virtual public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); return 0; } 

Catatan : warisan virtual di kelas Computer dan Monitor tidak akan memungkinkan warisan rhomboid jika Laptop kelas anak tidak mewarisi kelas Device virtual ( class Laptop: public Computer, public Monitor, public Device {}; ).


Kelas abstrak


Dalam C ++, kelas di mana setidaknya ada satu metode virtual murni dianggap abstrak. Jika metode virtual tidak diganti di kelas anak, kode tidak akan dikompilasi. Juga, di C ++ tidak mungkin untuk membuat objek dari kelas abstrak - upaya juga akan menyebabkan kesalahan selama kompilasi.


 #include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } virtual void say_hello() = 0; }; class Laptop: public Device { public: void say_hello() { cout << "Hello world!" << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; } 

Antarmuka


C ++, tidak seperti beberapa bahasa OOP, tidak menyediakan kata kunci terpisah untuk menunjukkan antarmuka. Namun, implementasi antarmuka dimungkinkan dengan membuat kelas abstrak murni - kelas di mana hanya ada deklarasi metode. Kelas semacam itu juga sering disebut sebagai Kelas Dasar Abstrak (ABC).


 #include <iostream> using namespace std; class Device { public: virtual void turn_on() = 0; }; class Laptop: public Device { public: void turn_on() { cout << "Device is on." << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; } 

Mahir


Meskipun warisan adalah prinsip dasar OOP, itu harus digunakan dengan hati-hati. Penting untuk berpikir bahwa kode apa pun yang akan digunakan kemungkinan akan diubah dan dapat digunakan dengan cara yang tidak jelas bagi pengembang.


Warisan dari kelas yang diimplementasikan atau diimplementasikan sebagian


Jika warisan tidak datang dari antarmuka (kelas abstrak murni dalam konteks C ++), tetapi dari kelas di mana ada implementasi, perlu dipertimbangkan bahwa ahli waris terhubung ke kelas induk dengan koneksi terdekat yang memungkinkan. Sebagian besar perubahan pada kelas induk dapat mempengaruhi ahli waris, yang dapat menyebabkan perilaku yang tidak terduga. Perubahan dalam perilaku ahli waris tidak selalu jelas - kesalahan dapat terjadi dalam kode yang sudah diuji dan bekerja. Situasi ini diperburuk oleh kehadiran hierarki kelas yang kompleks. Perlu selalu diingat bahwa kode dapat diubah tidak hanya oleh orang yang menulisnya, dan jalur pewarisan yang jelas bagi penulis mungkin tidak diperhitungkan oleh rekan-rekannya.


Sebaliknya, perlu dicatat bahwa warisan dari kelas yang diimplementasikan sebagian memiliki keuntungan yang tidak dapat disangkal. Perpustakaan dan kerangka kerja sering berfungsi sebagai berikut: mereka menyediakan kelas abstrak kepada pengguna dengan beberapa metode virtual dan banyak diimplementasikan. Dengan demikian, jumlah pekerjaan terbesar telah dilakukan - logika kompleks telah ditulis, dan pengguna hanya dapat menyesuaikan solusi yang sudah jadi agar sesuai dengan kebutuhannya.


Antarmuka


Warisan dari antarmuka (kelas abstrak murni) menghadirkan warisan sebagai peluang untuk menyusun kode dan melindungi pengguna. Karena antarmuka menggambarkan pekerjaan yang akan dilakukan oleh kelas implementasi, tetapi tidak menjelaskan caranya, pengguna antarmuka mana pun dilindungi dari perubahan di kelas yang mengimplementasikan antarmuka ini.


Antarmuka: Contoh Penggunaan


Pertama-tama, perlu dicatat bahwa contoh tersebut terkait erat dengan konsep polimorfisme, tetapi akan dipertimbangkan dalam konteks pewarisan dari kelas abstrak murni.


Aplikasi yang menjalankan logika bisnis abstrak harus dikonfigurasikan dari file konfigurasi terpisah. Pada tahap awal pengembangan, format file konfigurasi ini tidak sepenuhnya terbentuk. Melewati file parsing di belakang antarmuka memberikan beberapa keuntungan.


Kurangnya kejelasan tentang format file konfigurasi tidak memperlambat proses pengembangan program utama. Dua pengembang dapat bekerja secara paralel - satu di logika bisnis, dan yang lain di parser. Karena mereka berinteraksi melalui antarmuka ini, masing-masing dapat bekerja secara independen. Pendekatan ini membuatnya lebih mudah untuk menguji unit test dengan kode, karena tes yang diperlukan dapat ditulis menggunakan tiruan untuk antarmuka ini.


Juga, ketika mengubah format file konfigurasi, logika bisnis aplikasi tidak terpengaruh. Satu-satunya hal yang memerlukan transisi lengkap dari satu format ke format lainnya adalah menulis implementasi baru dari kelas abstrak yang sudah ada (kelas parser). Lebih jauh, kembali ke format file asli membutuhkan kerja minimal - mengganti satu parser yang ada dengan yang lain.


Kesimpulan


Warisan memberikan banyak manfaat, tetapi harus dirancang dengan cermat untuk menghindari masalah yang dihadirkannya kesempatan. Dalam konteks pewarisan, C ++ menyediakan berbagai alat yang membuka banyak kemungkinan bagi programmer.


Dan SOLID bagus.

Source: https://habr.com/ru/post/id445948/


All Articles