Seperti yang tertulis, "baunya", tetapi itu mungkin saja contoh yang Anda berikan. Menyimpan data dalam wadah objek generik, lalu menuangnya untuk mendapatkan akses ke data tersebut tidak secara otomatis menyandi kode. Anda akan melihatnya digunakan dalam banyak situasi. Namun, ketika Anda menggunakannya, Anda harus menyadari apa yang Anda lakukan, bagaimana Anda melakukannya, dan mengapa. Ketika saya melihat contohnya, penggunaan perbandingan berbasis string untuk memberi tahu saya objek apa yang merupakan benda yang mengukur meteran bau pribadi saya. Ini menunjukkan bahwa Anda tidak sepenuhnya yakin apa yang Anda lakukan di sini (yang baik-baik saja, karena Anda memiliki kebijaksanaan untuk datang ke sini untuk programmer. SE dan berkata "hei, saya tidak berpikir saya suka apa yang saya lakukan, tolong saya keluar! ").
Masalah mendasar dengan pola casting data dari wadah generik seperti ini adalah bahwa produsen data dan konsumen data harus bekerja sama, tetapi mungkin tidak jelas bahwa mereka melakukannya pada pandangan pertama. Dalam setiap contoh pola ini, bau atau tidak, ini adalah masalah mendasar. Hal ini sangat mungkin bagi pengembang berikutnya harus benar-benar menyadari bahwa Anda melakukan pola ini dan istirahat dengan kecelakaan, jadi jika Anda menggunakan pola ini Anda harus berhati-hati untuk membantu pengembang keluar berikutnya. Anda harus membuatnya lebih mudah baginya untuk tidak memecahkan kode secara tidak sengaja karena beberapa detail dia mungkin tidak tahu ada.
Misalnya, bagaimana jika saya ingin menyalin pemain? Jika saya hanya melihat isi objek pemain, tampilannya cukup mudah. Aku hanya perlu menyalin attack
, defense
dan tools
variabel. Mudah seperti pai! Yah, saya akan segera mengetahui bahwa penggunaan pointer Anda membuatnya sedikit lebih sulit (pada titik tertentu, ada baiknya melihat pointer pintar, tapi itu topik lain). Itu mudah diselesaikan. Saya hanya akan membuat salinan baru dari setiap alat, dan meletakkannya di tools
daftar baru saya . Bagaimanapun, Tool
ini adalah kelas yang sangat sederhana dengan hanya satu anggota. Jadi saya membuat banyak salinan, termasuk salinan Sword
, tetapi saya tidak tahu itu adalah pedang, jadi saya hanya menyalinnya name
. Kemudian, attack()
fungsi tersebut melihat nama, melihat bahwa itu adalah "pedang", melemparkannya, dan hal-hal buruk terjadi!
Kita dapat membandingkan kasing ini dengan kasing lain dalam pemrograman soket, yang menggunakan pola yang sama. Saya dapat mengatur fungsi soket UNIX seperti ini:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(portno);
serv_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
Mengapa ini pola yang sama? Karena bind
tidak menerima sockaddr_in*
, ia menerima yang lebih umum sockaddr*
. Jika Anda melihat definisi untuk kelas-kelas itu, kami melihat bahwa sockaddr
hanya memiliki satu anggota keluarga yang kami ditugaskan untuk sin_family
*. Keluarga mengatakan subtipe yang harus Anda gunakan sockaddr
. AF_INET
memberitahu Anda bahwa struct alamat sebenarnya a sockaddr_in
. Jika ya AF_INET6
, alamatnya adalah a sockaddr_in6
, yang memiliki bidang yang lebih besar untuk mendukung alamat IPv6 yang lebih besar.
Ini identik dengan Tool
contoh Anda , kecuali ia menggunakan bilangan bulat untuk menentukan keluarga mana daripada a std::string
. Namun, saya akan mengklaim itu tidak berbau, dan mencoba melakukannya untuk alasan selain "itu cara standar untuk melakukan soket, jadi tidak boleh 'berbau.'" Jelas itu pola yang sama, yaitu mengapa saya mengklaim bahwa menyimpan data dalam objek umum dan casting itu tidak secara otomatis kode bau, tetapi ada beberapa perbedaan dalam cara mereka melakukannya yang membuatnya lebih aman.
Saat menggunakan pola ini, informasi terpenting adalah menangkap penyampaian informasi tentang subkelas dari produsen ke konsumen. Inilah yang Anda lakukan dengan name
bidang dan soket UNIX dengan sin_family
bidangnya. Bidang itu adalah informasi yang dibutuhkan konsumen untuk memahami apa yang sebenarnya diciptakan oleh produsen. Dalam semua kasus pola ini, itu harus enumerasi (atau paling tidak, bilangan bulat bertindak seperti enumerasi). Mengapa? Pikirkan tentang apa yang akan dilakukan konsumen Anda dengan informasi tersebut. Mereka harus menulis beberapa if
pernyataan besar atauswitch
pernyataan, seperti yang Anda lakukan, di mana mereka menentukan subtipe yang benar, melemparkannya, dan menggunakan data. Menurut definisi, hanya ada sejumlah kecil dari tipe-tipe ini. Anda dapat menyimpannya dalam sebuah string, seperti yang Anda lakukan, tetapi itu memiliki banyak kelemahan:
- Lambat -
std::string
biasanya harus melakukan beberapa memori dinamis untuk menjaga string. Anda juga harus melakukan perbandingan teks lengkap untuk mencocokkan nama setiap kali Anda ingin mengetahui subclass apa yang Anda miliki.
- Terlalu serbaguna - Ada sesuatu yang bisa dikatakan untuk menempatkan kendala pada diri Anda ketika Anda melakukan sesuatu yang sangat berbahaya. Saya sudah memiliki sistem seperti ini yang mencari substring untuk mengatakan jenis objek apa yang dilihatnya. Ini berfungsi dengan baik sampai nama objek secara tidak sengaja berisi substring itu, dan menciptakan kesalahan samar yang sangat. Karena, seperti yang kami nyatakan di atas, kami hanya membutuhkan sejumlah kecil kasus, tidak ada alasan untuk menggunakan alat yang dikuasai secara masif seperti string. Ini mengarah ke ...
- Rawan kesalahan - Anggap saja Anda ingin mengamuk dengan kejam mencoba men-debug mengapa hal-hal tidak berfungsi ketika salah satu konsumen secara tidak sengaja menetapkan nama kain ajaib
MagicC1oth
. Serius, bug seperti itu bisa memakan waktu berhari - hari sebelum Anda menyadari apa yang terjadi.
Pencacahan bekerja lebih baik. Cepat, murah, dan jauh lebih sedikit kesalahan:
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
std::string typeName() const {
switch(type) {
case kSword: return "Sword";
case kSheild: return "Sheild";
case kMagicCloth: return "Magic Cloth";
default:
throw std::runtime_error("Invalid enum!");
}
}
};
Contoh ini juga memamerkan switch
pernyataan yang melibatkan enum, dengan satu-satunya bagian terpenting dari pola ini: default
kasus yang melempar. Anda seharusnya tidak pernah berada dalam situasi itu jika Anda melakukan sesuatu dengan sempurna. Namun, jika seseorang menambahkan jenis alat baru, dan Anda lupa memperbarui kode Anda untuk mendukungnya, Anda akan menginginkan sesuatu untuk menangkap kesalahan. Bahkan, saya sangat merekomendasikan mereka sehingga Anda harus menambahkannya bahkan jika Anda tidak membutuhkannya.
Keuntungan besar lainnya enum
adalah memberikan pengembang berikutnya daftar lengkap jenis alat yang valid, tepat di depan. Tidak perlu menelusuri kode untuk menemukan kelas Flute khusus Bob yang ia gunakan dalam pertempuran bos epiknya.
void damageWargear(Tool* tool)
{
switch(tool->type)
{
case Tool::kSword:
static_cast<Sword*>(tool)->damageSword();
break;
case Tool::kShield:
static_cast<Sword*>(tool)->damageShield();
break;
default:
break; // Ignore all other objects
}
}
Ya, saya memasukkan pernyataan default "kosong", hanya untuk membuatnya eksplisit kepada pengembang berikutnya tentang apa yang saya harapkan terjadi jika beberapa tipe baru yang tidak terduga menghampiri saya.
Jika Anda melakukan ini, polanya akan lebih sedikit berbau. Namun, untuk bebas dari bau, hal terakhir yang perlu Anda lakukan adalah mempertimbangkan pilihan lain. Gips ini adalah beberapa alat yang lebih kuat dan berbahaya yang Anda miliki dalam repertoar C ++. Anda tidak boleh menggunakannya kecuali Anda memiliki alasan yang bagus.
Salah satu alternatif yang sangat populer adalah apa yang saya sebut "serikat buruh" atau "kelas serikat pekerja." Sebagai contoh Anda, ini sebenarnya sangat cocok. Untuk membuatnya, Anda membuat Tool
kelas, dengan enumerasi seperti sebelumnya, tetapi alih-alih subklas Tool
, kami hanya menempatkan semua bidang dari setiap subtipe di atasnya.
class Tool {
public:
enum TypeE {
kSword,
kShield,
kMagicCloth
};
TypeE type;
int attack;
int defense;
};
Sekarang Anda tidak perlu subclass sama sekali. Anda hanya perlu melihat type
bidang untuk melihat bidang mana yang benar-benar valid. Ini jauh lebih aman dan lebih mudah dipahami. Namun, ada kekurangannya. Ada saatnya Anda tidak ingin menggunakan ini:
- Saat objek terlalu berbeda - Anda bisa berakhir dengan daftar bidang cucian, dan bisa jadi tidak jelas yang mana yang berlaku untuk setiap jenis objek.
- Saat beroperasi dalam situasi kritis-memori - Jika Anda perlu membuat 10 alat, Anda bisa malas dengan memori. Ketika Anda perlu membuat 500 juta alat, Anda akan mulai peduli dengan bit dan byte. Serikat pekerja selalu lebih besar dari yang seharusnya.
Solusi ini tidak digunakan oleh soket UNIX karena masalah ketidaksamaan yang diperparah oleh berakhirnya API. Tujuan dengan soket UNIX adalah untuk menciptakan sesuatu yang dapat digunakan oleh setiap rasa UNIX. Setiap rasa dapat menentukan daftar keluarga yang mereka dukung AF_INET
, dan akan ada daftar pendek untuk masing-masing. Namun, jika protokol baru datang, seperti yang Anda AF_INET6
lakukan, Anda mungkin perlu menambahkan bidang baru. Jika Anda melakukan ini dengan struct serikat, Anda akhirnya akan secara efektif membuat versi baru struct dengan nama yang sama, menciptakan masalah ketidakcocokan yang tak ada habisnya. Inilah sebabnya mengapa soket UNIX memilih untuk menggunakan pola casting daripada struct serikat. Saya yakin mereka mempertimbangkannya, dan fakta bahwa mereka memikirkannya adalah bagian dari mengapa itu tidak berbau ketika mereka menggunakannya.
Anda juga bisa menggunakan penyatuan nyata. Serikat menyimpan memori, dengan hanya sebesar anggota terbesar, tetapi mereka datang dengan masalah mereka sendiri. Ini mungkin bukan opsi untuk kode Anda, tetapi selalu merupakan opsi yang harus Anda pertimbangkan.
Solusi menarik lainnya adalah boost::variant
. Boost adalah perpustakaan hebat yang penuh dengan solusi lintas platform yang dapat digunakan kembali. Mungkin beberapa kode C ++ terbaik yang pernah ditulis. Boost.Variant pada dasarnya adalah versi C ++ dari serikat pekerja. Ini adalah wadah yang dapat berisi berbagai jenis, tetapi hanya satu per satu. Anda bisa membuat Anda Sword
, Shield
dan MagicCloth
kelas, kemudian membuat alat menjadi boost::variant<Sword, Shield, MagicCloth>
, yang berarti mengandung salah satu dari tiga jenis. Ini masih mengalami masalah yang sama dengan kompatibilitas di masa depan yang mencegah soket UNIX menggunakannya (belum lagi soket UNIX adalah C, sebelumboost
sedikit!), tetapi pola ini bisa sangat berguna. Varian sering digunakan, misalnya, di pohon parse, yang mengambil string teks dan memecahnya menggunakan tata bahasa untuk aturan.
Solusi terakhir yang saya sarankan lihat sebelum terjun dan menggunakan pendekatan casting objek generik adalah pola desain Pengunjung . Pengunjung adalah pola desain yang kuat yang mengambil keuntungan dari pengamatan yang memanggil fungsi virtual secara efektif melakukan casting yang Anda butuhkan, dan melakukannya untuk Anda. Karena kompiler melakukannya, itu tidak akan pernah salah. Jadi, alih-alih menyimpan enum, Pengunjung menggunakan kelas dasar abstrak, yang memiliki vtable yang tahu jenis objeknya. Kami kemudian membuat panggilan dua arah yang rapi yang berfungsi:
class Tool;
class Sword;
class Shield;
class MagicCloth;
class ToolVisitor {
public:
virtual void visit(Sword* sword) = 0;
virtual void visit(Shield* shield) = 0;
virtual void visit(MagicCloth* cloth) = 0;
};
class Tool {
public:
virtual void accept(ToolVisitor& visitor) = 0;
};
lass Sword : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
};
class Shield : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int defense;
};
class MagicCloth : public Tool{
public:
virtual void accept(ToolVisitor& visitor) { visitor.visit(*this); }
int attack;
int defense;
};
Jadi, apa pola dewa yang mengerikan ini? Yah, Tool
memiliki fungsi virtual accept
,. Jika Anda memberikannya pengunjung, diharapkan untuk berbalik dan memanggil visit
fungsi yang benar pada pengunjung tersebut untuk jenisnya. Inilah yang visitor.visit(*this);
dilakukan pada setiap subtipe. Rumit, tetapi kami dapat menunjukkan ini dengan contoh Anda di atas:
class AttackVisitor : public ToolVisitor
{
public:
int& currentAttack;
int& currentDefense;
AttackVisitor(int& currentAttack_, int& currentDefense_)
: currentAttack(currentAttack_)
, currentDefense(currentDefense_)
{ }
virtual void visit(Sword* sword)
{
currentAttack += sword->attack;
}
virtual void visit(Shield* shield)
{
currentDefense += shield->defense;
}
virtual void visit(MagicCloth* cloth)
{
currentAttack += cloth->attack;
currentDefense += cloth->defense;
}
};
void Player::attack()
{
int currentAttack = this->attack;
int currentDefense = this->defense;
AttackVisitor v(currentAttack, currentDefense);
for (Tool* t: tools) {
t->accept(v);
}
//some other functions to start attack
}
Jadi apa yang terjadi di sini? Kami membuat pengunjung yang akan melakukan beberapa pekerjaan untuk kami, setelah mengetahui jenis objek yang dikunjungi. Kami kemudian beralih ke daftar alat. Demi argumen, katakanlah objek pertama adalah Shield
, tetapi kode kami belum tahu itu. Itu panggilan t->accept(v)
, fungsi virtual. Karena objek pertama adalah perisai, akhirnya memanggilvoid Shield::accept(ToolVisitor& visitor)
, yang memanggil visitor.visit(*this);
. Sekarang, ketika kita mencari yang visit
akan dipanggil, kita sudah tahu bahwa kita memiliki Perisai (karena fungsi ini dipanggil), jadi kita akhirnya akan memanggil void ToolVisitor::visit(Shield* shield)
kita AttackVisitor
. Ini sekarang menjalankan kode yang benar untuk memperbarui pertahanan kita.
Pengunjung besar. Ini sangat kikuk sehingga saya hampir berpikir itu memiliki bau sendiri. Sangat mudah untuk menulis pola pengunjung yang buruk. Namun, ia memiliki satu keuntungan besar yang tidak dimiliki oleh yang lain. Jika kita menambahkan jenis alat baru, kita harus menambahkan ToolVisitor::visit
fungsi baru untuk itu. Begitu kita melakukan ini, setiap ToolVisitor
program akan menolak untuk dikompilasi karena tidak ada fungsi virtual. Ini membuatnya sangat mudah untuk menangkap semua kasus di mana kami melewatkan sesuatu. Jauh lebih sulit untuk menjamin bahwa jika Anda menggunakan if
atau switch
pernyataan untuk melakukan pekerjaan. Keunggulan ini cukup baik sehingga Pengunjung telah menemukan ceruk kecil yang bagus dalam generator adegan grafis 3d. Mereka kebetulan benar-benar membutuhkan jenis perilaku yang ditawarkan Pengunjung sehingga berfungsi dengan baik!
Secara keseluruhan, ingat bahwa pola-pola ini menyulitkan pengembang berikutnya. Luangkan waktu untuk membuatnya lebih mudah, dan kode tidak akan berbau!
* Secara teknis, jika Anda melihat spec, sockaddr memiliki satu anggota bernama sa_family
. Ada beberapa hal rumit yang dilakukan di sini di level C yang tidak masalah bagi kita. Anda dipersilakan untuk melihat implementasi yang sebenarnya , tetapi untuk jawaban ini saya akan menggunakan sa_family
sin_family
dan yang lain sepenuhnya dipertukarkan, menggunakan mana saja yang paling intuitif untuk prosa, percaya bahwa tipu C menangani perincian yang tidak penting.