Topik ini membahas dua cara menggabungkan kelas, yaitu komposisi (composition) yang menempatkan objek lain sebagai member dan pewarisan (inheritance) yang mewariskan antarmuka beserta implementasinya. Materinya mencakup contoh keduanya, pertanyaan pembeda antara relasi memiliki dan relasi menjadi, serta pedoman memilih di antara keduanya. Setelah membacanya, keputusan desain antara komposisi dan pewarisan dapat diambil dengan alasan yang dapat dipertanggungjawabkan, bukan sekadar mengikuti kebiasaan.
Komposisi: relasi memiliki#
Komposisi menempatkan objek kelas lain sebagai member, sehingga kelas pemiliknya menggunakan kemampuan objek itu tanpa menjadi bentuk darinya. Relasi ini disebut has-a karena kalimatnya berbunyi demikian, misalnya mobil memiliki mesin, tetapi mobil bukanlah mesin. Member kelas lain dapat ditempatkan sebagai nilai, referensi, atau pointer, dan pilihan itu menentukan pihak yang bertanggung jawab atas masa hidupnya. Antarmuka kelas pemilik menjadi satu-satunya pintu keluar, sehingga detail di dalamnya bebas diganti tanpa memengaruhi pengguna kelas tersebut. Program berikut memperagakan pola itu.
#include <iostream>
class Mesin {
public:
void nyalakan() const {
std::cout << "Mesin 1500 cc menyala\n";
}
void matikan() const {
std::cout << "Mesin 1500 cc mati\n";
}
};
class Mobil {
public:
void berangkat() const {
std::cout << "Sabuk pengaman terpasang\n";
mesin.nyalakan();
}
void parkir() const {
mesin.matikan();
std::cout << "Rem tangan ditarik\n";
}
private:
Mesin mesin; // Mobil memiliki Mesin: relasi memiliki (has-a)
};
int main() {
Mobil mobil;
mobil.berangkat();
mobil.parkir();
return 0;
}Sabuk pengaman terpasang
Mesin 1500 cc menyala
Mesin 1500 cc mati
Rem tangan ditarikKelas Mobil menggunakan Mesin melalui member privatnya dan hanya mengekspos berangkat() serta parkir(). Mengganti Mesin bensin menjadi Mesin listrik, misalnya, cukup dilakukan pada member tersebut, sedangkan seluruh kode yang memanggil berangkat() tidak berubah sama sekali. Fleksibilitas inilah kekuatan utama komposisi, yaitu detail implementasi tersembunyi di balik antarmuka dan dapat ditukar tanpa menuliskan ulang kode penggunanya. Pola ini juga disebut delegasi, yaitu kelas pemilik meneruskan permintaan kepada member yang benar-benar memiliki kemampuannya. Dari sisi pengujian, member yang mudah diganti membuat kelas pemilik dapat diperiksa terpisah dari detail yang dipakainya.
Pewarisan: relasi menjadi#
Pewarisan beralasan ketika kelas turunan benar-benar merupakan bentuk khusus dari kelas dasarnya, sehingga objeknya dapat menggantikan kelas dasar di mana pun. Relasi ini disebut is-a karena kalimatnya berbunyi demikian. Contohnya, lingkaran adalah bentuk, sehingga kode yang bekerja pada Bentuk harus tetap benar ketika menerima Lingkaran. Syarat inilah yang membedakan pewarisan dari sekadar berbagi kode sekaligus membuat polimorfisme melalui fungsi virtual menjadi tepat digunakan.
Kelas turunan juga memperoleh akses ke member protected, yaitu wilayah yang terbuka bagi kelas turunan tetapi tertutup bagi pengguna biasa, sehingga kelas dasar dapat menyediakan titik penyambungan tanpa membocorkannya ke luar hierarki. Program berikut memperagakan substitusi tersebut.
#include <iostream>
#include <string>
#include <vector>
class Bentuk {
public:
virtual ~Bentuk() = default;
virtual std::string nama() const = 0;
virtual double luas() const = 0;
};
class Lingkaran : public Bentuk {
public:
explicit Lingkaran(int r) : jari{r} {}
std::string nama() const override {
return "Lingkaran jari-jari " + std::to_string(jari);
}
double luas() const override {
return 3.14 * jari * jari;
}
int jari;
};
class Kotak : public Bentuk {
public:
explicit Kotak(int s) : sisi{s} {}
std::string nama() const override {
return "Kotak sisi " + std::to_string(sisi);
}
double luas() const override {
return sisi * sisi;
}
int sisi;
};
int main() {
Lingkaran ling{7};
Kotak kotak{5};
std::vector<const Bentuk*> daftar{&ling, &kotak};
for (const Bentuk* b : daftar) {
std::cout << b->nama() << " luasnya " << b->luas() << "\n";
}
return 0;
}Lingkaran jari-jari 7 luasnya 153.86
Kotak sisi 5 luasnya 25Perulangan di atas tidak mengenal Lingkaran maupun Kotak secara langsung, karena keduanya berperilaku sebagai Bentuk melalui fungsi virtualnya. Menambahkan Segitiga baru kelak tidak menuntut perubahan satu baris pun pada perulangan itu, dan keuntungan itu hanya muncul bila relasi is-a memang benar. Apabila syarat substitusi itu tidak terpenuhi, keuntungan di atas hilang dan pewarisan hanya menyisakan ikatan yang erat antara dua kelas.
Panduan memilih#
Pertanyaan pembedanya menyatakan hubungan kedua kelas dalam satu kalimat, dan jawabannya langsung menunjukkan pilihannya.
| Pertanyaan tentang dua kelas | Pilihan yang cocok |
|---|---|
| Satu adalah jenis dari yang lain, dan dapat menggantikannya sepenuhnya | Pewarisan |
| Satu memiliki, menggunakan, atau tersusun dari yang lain | Komposisi |
| Butuh polimorfisme lewat antarmuka umum saat program berjalan | Pewarisan, umumnya antarmuka pure virtual |
| Hanya butuh fungsionalitas kelas lain untuk dipakai sendiri | Komposisi |
Pedoman di atas sejalan dengan FAQ resmi C++, yang menegaskan pewarisan public hanya untuk hubungan is-a yang sah secara substitusi. Dalam praktiknya, komposisi mendominasi karena mayoritas relasi antarkelas memang berbentuk memiliki dan menggunakan, sedangkan pewarisan yang dirancang dengan baik biasanya dangkal, yaitu satu atau dua tingkat dengan kelas dasar berupa antarmuka. Pewarisan private, yang mewarisi implementasi tanpa mewarisi antarmuka, tetap ada di bahasa ini tetapi jarang dibutuhkan karena hampir selalu dapat diganti komposisi.
Substitusi yang dimaksud adalah penggantian yang utuh, yaitu seluruh jaminan kelas dasar, mulai dari prasyarat hingga keluaran setiap fungsinya, tetap berlaku pada kelas turunannya. Apabila salah satu jaminan itu perlu dilemahkan, relasi keduanya bukan pewarisan, melainkan hubungan yang lebih tepat dinyatakan melalui komposisi. Keputusan yang lahir dari pertanyaan ini biasanya tahan lama, karena relasi yang dinyatakan benar sejak awal tidak perlu ditata ulang ketika fitur bertambah.
Kesalahan umum#
Kesalahan berikut paling sering muncul ketika kedua mekanisme ini mulai dipakai bersamaan.
- Mewarisi semata demi kode yang bisa dipakai ulang, misalnya
MobilmewarisiMesin. Ikatan yang terbentuk keliru secara makna karena memberMesinikut bocor ke antarmukaMobil, dan perbaikannya adalah menjadikanMesinmember biasa. - Memilih komposisi padahal substitusi polimorfik dibutuhkan. Objeknya tidak dapat diterima kode yang mengharapkan antarmuka umum, sehingga pewarisan dengan fungsi virtual menjadi pilihan yang tepat.
- Membangun hierarki dalam yang melewati tiga tingkat atau lebih. Semakin dalam hierarkinya, semakin sulit perilaku kelas turunan dilacak, dan perbaikannya adalah meratakan hierarki dengan mengganti tingkat pewarisan yang keliru menjadi komposisi.
- Menyalin seluruh antarmuka kelas lain ke kelas pemilik lalu menyebutnya komposisi. Delegasi yang sehat hanya meneruskan operasi yang benar-benar dipakai, sedangkan penyalinan antarmuka penuh hanya menambah ketergantungan yang samar.
Ringkasan#
- Komposisi menempatkan objek kelas lain sebagai member dan cocok untuk relasi memiliki, dengan antarmuka pemilik sebagai satu-satunya pintu keluar.
- Pewarisan beralasan hanya untuk relasi menjadi yang sah secara substitusi, dan nilainya terletak pada polimorfisme lewat fungsi virtual.
- Pedoman praktisnya memulai dari komposisi dan beralih ke pewarisan bila kebutuhan substitusinya nyata.
- Hierarki yang dangkal berbasis antarmuka lebih mudah dirawat daripada hierarki dalam yang mewarisi demi berbagi kode.
Referensi#
- Composition — LearnCpp — Penjelasan relasi has-a beserta contoh dan kelebihannya.
- Object relationships — LearnCpp — Kerangka relasi antarobjek sebagai latar pemilihan desain.
- Inheritance — proper inheritance and substitutability — C++ FAQ — FAQ resmi mengenai syarat pewarisan yang sah dan substitusinya.
- Derived classes — cppreference — Aturan pewarisan dalam standar bahasa C++.