Misalnya, saya memiliki permainan, yang memiliki beberapa alat untuk meningkatkan kemampuan Pemain:
Tool.h
class Tool{
public:
std::string name;
};
Dan beberapa alat:
Pedang
class Sword : public Tool{
public:
Sword(){
this->name="Sword";
}
int attack;
};
Shield.h
class Shield : public Tool{
public:
Shield(){
this->name="Shield";
}
int defense;
};
MagicCloth.h
class MagicCloth : public Tool{
public:
MagicCloth(){
this->name="MagicCloth";
}
int attack;
int defense;
};
Dan kemudian pemain dapat memegang beberapa alat untuk menyerang:
class Player{
public:
int attack;
int defense;
vector<Tool*> tools;
void attack(){
//original attack and defense
int currentAttack=this->attack;
int currentDefense=this->defense;
//calculate attack and defense affected by tools
for(Tool* tool : tools){
if(tool->name=="Sword"){
Sword* sword=(Sword*)tool;
currentAttack+=sword->attack;
}else if(tool->name=="Shield"){
Shield* shield=(Shield*)tool;
currentDefense+=shield->defense;
}else if(tool->name=="MagicCloth"){
MagicCloth* magicCloth=(MagicCloth*)tool;
currentAttack+=magicCloth->attack;
currentDefense+=magicCloth->shield;
}
}
//some other functions to start attack
}
};
Saya pikir sulit untuk mengganti if-else
dengan metode virtual dalam alat, karena setiap alat memiliki sifat yang berbeda, dan masing-masing alat mempengaruhi serangan dan pertahanan pemain, yang mana pembaruan serangan pemain dan pertahanan perlu dilakukan di dalam objek Player.
Tapi saya tidak puas dengan desain ini, karena mengandung downcasting, dengan if-else
pernyataan panjang . Apakah desain ini perlu "diperbaiki"? Jika demikian, apa yang dapat saya lakukan untuk memperbaikinya?
design-patterns
c++
generics
ggrr
sumber
sumber
Jawaban:
Ya, ini adalah bau kode (dalam banyak kasus).
Dalam contoh Anda, cukup sederhana untuk mengganti if / else dengan metode virtual:
Sekarang tidak perlu lagi untuk
if
blok Anda , pemanggil dapat menggunakannya sepertiTentu saja, untuk situasi yang lebih rumit, solusi seperti itu tidak selalu begitu jelas (tetapi hampir kapan saja memungkinkan). Tetapi jika Anda datang ke situasi di mana Anda tidak tahu bagaimana menyelesaikan kasus dengan metode virtual, Anda dapat mengajukan pertanyaan baru lagi di sini di "Programmer" (atau, jika itu menjadi bahasa atau implementasi khusus, pada Stackoverflow).
sumber
Sword
cara ini di basis kode Anda. Anda bisa sajanew Tool("sword", swordAttack, swordDefense)
dari misalnya file JSON.Tool
dengan semua pengubah yang mungkin, mengisi beberapavector<Tool*>
dengan hal-hal yang dibaca dari file data, kemudian hanya mengulanginya dan memodifikasi statistik seperti yang Anda lakukan sekarang. Anda akan mendapat masalah ketika Anda ingin item memberi misalnya bonus 10% untuk serangan. Mungkin atool->modify(playerStats)
adalah pilihan lain.Masalah utama dengan kode Anda adalah, bahwa setiap kali Anda memperkenalkan item baru, Anda tidak hanya harus menulis dan memperbarui kode item, Anda juga harus memodifikasi pemain Anda (atau di mana pun item tersebut digunakan), yang membuat semuanya menjadi jauh lebih rumit.
Sebagai aturan umum, saya pikir itu selalu agak mencurigakan, ketika Anda tidak dapat bergantung pada subclassing normal / warisan dan harus melakukan upcasting sendiri.
Saya bisa memikirkan dua pendekatan yang mungkin membuat semuanya lebih fleksibel:
Seperti yang disebutkan lainnya, pindahkan anggota
attack
dandefense
ke kelas dasar dan cukup inisialisasi mereka0
. Ini juga bisa berfungsi ganda sebagai cek apakah Anda benar-benar dapat mengayunkan item untuk serangan atau menggunakannya untuk memblokir serangan.Buat semacam sistem callback / acara. Ada beberapa pendekatan yang mungkin berbeda untuk ini.
Bagaimana dengan menjaganya agar tetap sederhana?
virtual void onEquip(Owner*) {}
danvirtual void onUnequip(Owner*) {}
.virtual void onEquip(Owner *o) { o->modifyStat("attack", attackValue); }
danvirtual void onUnequip(Owner *o) { o->modifyStat("attack", -attackValue); }
.Dibandingkan dengan hanya meminta nilai serangan / pertahanan tepat pada waktunya, ini tidak hanya membuat semuanya menjadi lebih dinamis, tetapi juga menghemat panggilan yang tidak perlu dan bahkan memungkinkan Anda membuat item yang akan memengaruhi karakter Anda secara permanen.
Sebagai contoh, bayangkan sebuah cincin terkutuk yang hanya akan mengatur beberapa stat tersembunyi setelah dilengkapi, menandai karakter Anda sebagai dikutuk secara permanen.
sumber
Sementara @DocBrown telah memberikan jawaban yang bagus, itu tidak cukup jauh, imho. Sebelum Anda mulai mengevaluasi jawaban, Anda harus mengevaluasi kebutuhan Anda. Apa yang sebenarnya Anda butuhkan ?
Di bawah ini saya akan menunjukkan dua solusi yang mungkin, yang menawarkan keuntungan berbeda untuk kebutuhan yang berbeda.
Yang pertama sangat sederhana dan dirancang khusus untuk apa yang telah Anda perlihatkan:
Hal ini memungkinkan serialisasi / deserialisasi alat yang sangat mudah (misalnya untuk menyimpan atau jaringan), dan tidak perlu pengiriman virtual sama sekali. Jika hanya kode yang Anda tunjukkan, dan Anda tidak mengharapkannya berkembang jauh selain memiliki alat yang lebih berbeda dengan nama dan statistik yang berbeda, hanya dalam jumlah yang berbeda, maka inilah cara yang harus dilakukan.
@DocBrown telah menawarkan solusi yang masih mengandalkan pengiriman virtual, dan itu bisa menjadi keuntungan jika Anda entah bagaimana mengkhususkan alat untuk bagian-bagian kode Anda yang tidak ditampilkan. Namun, jika Anda benar-benar perlu atau ingin juga mengubah perilaku lain, maka saya akan menyarankan solusi berikut:
Komposisi atas warisan
Bagaimana jika nanti Anda menginginkan alat yang memodifikasi kelincahan ? Atau kecepatan lari ? Bagi saya, sepertinya Anda sedang membuat RPG. Satu hal yang penting bagi RPG adalah terbuka untuk ekstensi . Solusi yang ditampilkan sampai sekarang tidak menawarkan itu. Anda harus mengubah
Tool
kelas dan menambahkan metode virtual baru ke sana setiap kali Anda memerlukan atribut baru.Solusi kedua yang saya tunjukkan adalah yang saya tunjukkan sebelumnya dalam komentar - ia menggunakan komposisi alih-alih pewarisan dan mengikuti prinsip "tertutup untuk modifikasi, terbuka untuk ekstensi *. Jika Anda terbiasa dengan cara sistem entitas bekerja, beberapa hal akan terlihat akrab (saya suka menganggap komposisi sebagai adik lelaki ES).
Perhatikan bahwa apa yang saya perlihatkan di bawah ini jauh lebih elegan dalam bahasa yang memiliki informasi tipe runtime, seperti Java atau C #. Oleh karena itu, kode C ++ yang saya perlihatkan harus menyertakan beberapa "pembukuan" yang hanya diperlukan untuk membuat komposisi berfungsi di sini. Mungkin seseorang dengan lebih banyak pengalaman C ++ dapat menyarankan pendekatan yang lebih baik.
Pertama, kita melihat lagi di sisi penelepon . Dalam contoh Anda, Anda sebagai penelepon di dalam
attack
metode tidak peduli tentang alat sama sekali. Apa yang Anda pedulikan adalah dua properti - poin serangan dan pertahanan. Anda tidak benar - benar peduli dari mana asalnya, dan Anda tidak peduli dengan properti lain (misalnya kecepatan lari, kelincahan).Jadi pertama-tama, kami memperkenalkan kelas baru
Dan kemudian, kami membuat dua komponen pertama kami
Setelah itu, kami membuat Alat memegang satu set properti, dan membuat properti-permintaan oleh orang lain.
Perhatikan bahwa dalam contoh ini, saya hanya mendukung memiliki satu komponen dari setiap jenis - ini membuat segalanya lebih mudah. Anda bisa secara teori juga memungkinkan beberapa komponen dari jenis yang sama, tetapi itu menjadi sangat cepat. Salah satu aspek penting:
Tool
sekarang ditutup untuk modifikasi - kita tidak akan pernah menyentuh sumberTool
lagi - tetapi terbuka untuk ekstensi - kita dapat memperluas perilaku Alat dengan memodifikasi hal-hal lain, dan hanya dengan mengirimkan Komponen lain ke dalamnya.Sekarang kita membutuhkan cara untuk mengambil alat berdasarkan tipe komponen. Anda masih dapat menggunakan vektor untuk alat, seperti dalam contoh kode Anda:
Anda juga bisa merevisi ini ke dalam
Inventory
kelas Anda sendiri , dan menyimpan tabel pencarian yang sangat menyederhanakan alat pengambilan berdasarkan tipe komponen dan menghindari iterasi seluruh koleksi lagi dan lagi.Apa keuntungan dari pendekatan ini? Di
attack
, Anda memproses Alat yang memiliki dua komponen - Anda tidak peduli tentang hal lain.Mari kita bayangkan Anda memiliki
walkTo
metode, dan sekarang Anda memutuskan bahwa itu adalah ide yang baik jika beberapa alat akan mendapatkan kemampuan untuk memodifikasi kecepatan berjalan Anda. Tidak masalah!Pertama, buat yang baru
Component
:Kemudian Anda cukup menambahkan instance komponen ini ke alat yang Anda ingin meningkatkan kecepatan bangun Anda, dan mengubah
WalkTo
metode untuk memproses komponen yang baru saja Anda buat:Perhatikan bahwa kami menambahkan beberapa perilaku ke Alat kami tanpa mengubah kelas Alat sama sekali.
Anda bisa (dan harus) memindahkan string ke variabel const makro atau statis, jadi Anda tidak perlu mengetiknya lagi dan lagi.
Jika Anda mengambil pendekatan ini lebih jauh - misalnya membuat komponen yang dapat ditambahkan ke pemain, dan membuat
Combat
komponen yang menandai pemain agar dapat berpartisipasi dalam pertempuran, maka Anda dapat menyingkirkanattack
metode ini juga, dan membiarkannya ditangani oleh Komponen atau diproses di tempat lain.Keuntungan membuat pemain bisa mendapatkan Komponen juga adalah bahwa, Anda bahkan tidak perlu mengubah pemain untuk memberinya perilaku berbeda. Dalam contoh saya, Anda bisa membuat
Movable
komponen, sehingga Anda tidak perlu menerapkanwalkTo
metode pada pemain untuk membuatnya bergerak. Anda hanya akan membuat komponen, melampirkannya ke pemain dan biarkan orang lain memprosesnya.Anda dapat menemukan contoh lengkap di intisari ini: https://gist.github.com/NetzwergX/3a29e1b106c6bb9c7308e89dd715ee20
Solusi ini jelas sedikit lebih rumit daripada yang lain yang telah diposting. Tetapi tergantung pada seberapa fleksibel Anda ingin menjadi, seberapa jauh Anda ingin mengambilnya, ini bisa menjadi pendekatan yang sangat kuat.
Edit
Beberapa jawaban lain mengusulkan pewarisan langsung (Membuat pedang memperpanjang Alat, membuat Perisai memperpanjang Alat). Saya tidak berpikir ini adalah skenario di mana warisan bekerja dengan sangat baik. Bagaimana jika Anda memutuskan bahwa memblokir dengan perisai dengan cara tertentu juga dapat merusak penyerang? Dengan solusi saya, Anda cukup menambahkan komponen Serangan ke perisai dan menyadari bahwa tanpa ada perubahan pada kode Anda. Dengan warisan, Anda akan memiliki masalah. item / Alat dalam RPG adalah kandidat utama untuk komposisi atau bahkan langsung menggunakan sistem entitas sejak awal.
sumber
Secara umum, jika Anda memiliki kebutuhan untuk menggunakan
if
(dalam kombinasi dengan memerlukan jenis instance) dalam bahasa OOP, itu pertanda, bahwa sesuatu bau terjadi. Setidaknya, Anda harus melihat model Anda lebih dekat.Saya akan memodelkan Domain Anda secara berbeda.
Bagimu pengguna
Tool
memilikiAttackBonus
danDefenseBonus
- yang keduanya bisa0
berjaga-jaga jika tidak berguna untuk bertarung seperti bulu atau sesuatu seperti itu.Untuk serangan, Anda mendapatkan
baserate
+bonus
dari senjata yang digunakan. Hal yang sama berlaku untuk pertahananbaserate
+bonus
.Karena itu Anda
Tool
harus memilikivirtual
metode untuk menghitung serangan / pertahanan boni.tl; dr
Dengan desain yang lebih baik, Anda bisa menghindari peretasan
if
.sumber
if
kurang pemrograman. Sebagian besar dalam kombinasi seperti jikainstanceof
atau sesuatu seperti itu. Tetapi ada posisi, yang mengklaimif
s adalah kodemell dan ada cara untuk mengatasinya. Dan Anda benar, itu adalah operator penting yang memiliki haknya sendiri.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
dantools
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 ditools
daftar baru saya . Bagaimanapun,Tool
ini adalah kelas yang sangat sederhana dengan hanya satu anggota. Jadi saya membuat banyak salinan, termasuk salinanSword
, tetapi saya tidak tahu itu adalah pedang, jadi saya hanya menyalinnyaname
. 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:
Mengapa ini pola yang sama? Karena
bind
tidak menerimasockaddr_in*
, ia menerima yang lebih umumsockaddr*
. Jika Anda melihat definisi untuk kelas-kelas itu, kami melihat bahwasockaddr
hanya memiliki satu anggota keluarga yang kami ditugaskan untuksin_family
*. Keluarga mengatakan subtipe yang harus Anda gunakansockaddr
.AF_INET
memberitahu Anda bahwa struct alamat sebenarnya asockaddr_in
. Jika yaAF_INET6
, alamatnya adalah asockaddr_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 astd::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 dengansin_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 beberapaif
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: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.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:
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.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 subklasTool
, kami hanya menempatkan semua bidang dari setiap subtipe di atasnya.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: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 AndaAF_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 AndaSword
,Shield
danMagicCloth
kelas, kemudian membuat alat menjadiboost::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:
Jadi, apa pola dewa yang mengerikan ini? Yah,
Tool
memiliki fungsi virtualaccept
,. Jika Anda memberikannya pengunjung, diharapkan untuk berbalik dan memanggilvisit
fungsi yang benar pada pengunjung tersebut untuk jenisnya. Inilah yangvisitor.visit(*this);
dilakukan pada setiap subtipe. Rumit, tetapi kami dapat menunjukkan ini dengan contoh Anda di atas: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 panggilant->accept(v)
, fungsi virtual. Karena objek pertama adalah perisai, akhirnya memanggilvoid Shield::accept(ToolVisitor& visitor)
, yang memanggilvisitor.visit(*this);
. Sekarang, ketika kita mencari yangvisit
akan dipanggil, kita sudah tahu bahwa kita memiliki Perisai (karena fungsi ini dipanggil), jadi kita akhirnya akan memanggilvoid ToolVisitor::visit(Shield* shield)
kitaAttackVisitor
. 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, setiapToolVisitor
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 menggunakanif
atauswitch
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 menggunakansa_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.sumber
Secara umum saya menghindari menerapkan beberapa kelas / mewarisi jika itu hanya untuk mengkomunikasikan data. Anda dapat menempel pada satu kelas dan mengimplementasikan semuanya dari sana. Sebagai contoh, ini sudah cukup
Mungkin, Anda mengantisipasi bahwa gim Anda akan menerapkan beberapa jenis pedang, dll. Tetapi Anda akan memiliki cara lain untuk menerapkannya. Ledakan kelas jarang merupakan arsitektur terbaik. Tetap sederhana.
sumber
Seperti yang dinyatakan sebelumnya, ini adalah bau kode yang serius. Namun, orang dapat menganggap sumber masalah Anda menggunakan pewarisan alih-alih komposisi dalam desain Anda.
Misalnya, mengingat apa yang telah Anda tunjukkan kepada kami, Anda jelas memiliki 3 konsep:
Perhatikan bahwa kelas keempat Anda hanyalah kombinasi dari dua konsep terakhir. Jadi saya sarankan menggunakan komposisi untuk ini.
Anda memerlukan struktur data untuk mewakili informasi yang diperlukan untuk serangan. Dan Anda memerlukan struktur data yang mewakili informasi yang Anda butuhkan untuk pertahanan. Terakhir, Anda memerlukan struktur data untuk mewakili hal-hal yang mungkin atau mungkin tidak memiliki salah satu atau kedua properti ini:
sumber
Attack
danDefense
menjadi lebih rumit tanpa mengubah antarmukaTool
.Tool
tertutup sepenuhnya untuk modifikasi sambil tetap membiarkannya terbuka untuk ekstensi.Tool
tanpa mengubahnya . Dan jika saya memiliki hak untuk memodifikasinya, maka saya tidak melihat perlunya komponen yang berubah-ubah.Mengapa tidak membuat metode abstrak
modifyAttack
danmodifyDefense
diTool
kelas? Maka setiap anak akan memiliki implementasi mereka sendiri, dan Anda memanggil cara yang elegan ini:Melewati nilai sebagai referensi akan menghemat sumber daya jika Anda dapat:
sumber
Jika seseorang menggunakan polimorfisme, selalu yang terbaik jika semua kode yang peduli tentang kelas mana yang digunakan berada di dalam kelas itu sendiri. Ini adalah bagaimana saya akan mengkodekannya:
Ini memiliki keuntungan sebagai berikut:
sumber
Saya pikir salah satu cara untuk mengenali kekurangan dalam pendekatan ini adalah mengembangkan ide Anda sampai pada kesimpulan logisnya.
Ini terlihat seperti permainan, jadi pada tahap tertentu Anda mungkin akan mulai khawatir tentang kinerja dan menukar perbandingan string tersebut dengan
int
atauenum
. Karena daftar item menjadi lebih panjang, ituif-else
mulai menjadi sangat sulit, jadi Anda dapat mempertimbangkan refactoring menjadiswitch-case
. Anda juga punya cukup dinding teks pada titik ini sehingga Anda dapat memisahkan tindakan dalam masingcase
- masing ke dalam fungsi yang terpisah.Setelah Anda mencapai titik ini, struktur kode Anda mulai terlihat familier - permulaannya terlihat seperti homebrew, vtable linting tangan * - struktur dasar di mana metode virtual biasanya diterapkan. Kecuali, ini adalah tabel yang harus Anda perbarui dan pelihara sendiri secara manual, setiap kali Anda menambah atau memodifikasi jenis barang.
Dengan berpegang pada fungsi virtual "nyata" Anda dapat menjaga implementasi perilaku setiap item dalam item itu sendiri. Anda dapat menambahkan item tambahan dengan cara yang lebih mandiri dan konsisten. Dan saat Anda melakukan semua ini, itu adalah kompiler yang akan mengurus implementasi pengiriman dinamis Anda, bukan Anda.
Untuk menyelesaikan masalah spesifik Anda: Anda berjuang untuk menulis sepasang fungsi virtual sederhana untuk memperbarui serangan dan pertahanan karena beberapa item hanya mempengaruhi serangan dan beberapa item hanya mempengaruhi pertahanan. Caranya dalam kasus sederhana seperti ini untuk menerapkan kedua perilaku itu, tetapi tanpa efek dalam kasus-kasus tertentu.
GetDefenseBonus()
mungkin kembali0
atauApplyDefenseBonus(int& defence)
mungkindefence
tidak berubah. Bagaimana Anda melakukannya akan tergantung pada bagaimana Anda ingin menangani tindakan lain yang memiliki efek. Dalam kasus yang lebih kompleks, di mana ada perilaku yang lebih bervariasi, Anda dapat dengan mudah menggabungkan aktivitas menjadi satu metode.* (Meskipun, dialihkan relatif terhadap implementasi tipikal)
sumber
Memiliki blok kode yang mengetahui semua "alat" yang memungkinkan bukanlah desain yang bagus (terutama karena Anda akan berakhir dengan banyak blok seperti itu dalam kode Anda); tetapi tidak ada yang memiliki dasar
Tool
dengan bertopik untuk semua properti alat yang mungkin: sekarangTool
kelas harus tahu tentang semua kemungkinan penggunaan.Apa yang diketahui setiap alat adalah apa yang dapat dikontribusikannya pada karakter yang menggunakannya. Jadi sediakan satu metode untuk semua alat
giveto(*Character owner)
,. Ini akan menyesuaikan statistik pemain sebagaimana mestinya tanpa mengetahui alat apa yang bisa dilakukan, dan apa yang terbaik, itu tidak perlu diketahui tentang sifat karakter yang tidak relevan juga. Misalnya, perisai bahkan tidak perlu tahu tentang atributattack
,invisibility
,health
dll Semua yang dibutuhkan untuk menerapkan alat ini untuk karakter untuk mendukung atribut bahwa objek membutuhkan. Jika Anda mencoba memberikan pedang kepada keledai, dan keledai itu tidak memilikiattack
statistik, Anda akan mendapatkan kesalahan.Alat juga harus memiliki
remove()
metode, yang membalikkan efeknya pada pemilik. Ini sedikit rumit (mungkin saja berakhir dengan alat yang meninggalkan efek tidak nol ketika diberikan dan kemudian dibawa pergi), tetapi setidaknya itu dilokalkan ke masing-masing alat.sumber
Tidak ada jawaban yang mengatakan bahwa itu tidak berbau jadi saya akan menjadi orang yang mendukung pendapat itu; kode ini benar-benar baik-baik saja! Pendapat saya didasarkan pada fakta bahwa kadang-kadang lebih mudah untuk bergerak dan memungkinkan keterampilan Anda meningkat secara bertahap saat Anda membuat lebih banyak barang baru. Anda dapat terjebak selama berhari-hari untuk membuat arsitektur yang sempurna, tetapi mungkin tidak ada yang akan melihatnya dalam tindakan, karena Anda tidak pernah menyelesaikan proyek. Tepuk tangan!
sumber