Apakah itu bau kode untuk menyimpan objek generik dalam wadah dan kemudian mendapatkan objek dan menurunkan objek dari wadah?

34

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-elsedengan 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-elsepernyataan panjang . Apakah desain ini perlu "diperbaiki"? Jika demikian, apa yang dapat saya lakukan untuk memperbaikinya?

ggrr
sumber
4
Teknik OOP standar untuk menghapus tes untuk subkelas tertentu (dan downcast berikutnya) adalah untuk membuat, atau dalam hal ini mungkin dua, metode virtual di kelas dasar untuk digunakan daripada rantai jika dan gips. Ini dapat digunakan untuk menghapus if's all dan mendelegasikan operasi ke subclass untuk diimplementasikan. Anda juga tidak perlu mengedit pernyataan if setiap kali Anda menambahkan subkelas baru.
Erik Eidt
2
Juga pertimbangkan Pengiriman Ganda.
Boris the Spider
Mengapa tidak menambahkan properti ke kelas Alat Anda yang menyimpan kamus jenis atribut (yaitu serangan, pertahanan) dan nilai yang diberikan padanya. Serangannya, pertahanannya bisa dihitung nilainya. Kemudian Anda bisa memanggil nilai dari Alat itu sendiri dengan konstanta yang disebutkan.
user1740075
8
Saya akan meninggalkan ini di sini: ericlippert.com/2015/04/27/wizards-and-warriors-part-one
You
1
Lihat juga pola Pengunjung.
JDługosz

Jawaban:

63

Ya, ini adalah bau kode (dalam banyak kasus).

Saya pikir sulit untuk mengganti if-else dengan metode virtual pada alat

Dalam contoh Anda, cukup sederhana untuk mengganti if / else dengan metode virtual:

class Tool{
 public:
   virtual int GetAttack() const=0;
   virtual int GetDefense() const=0;
};

class Sword : public Tool{
    // ...
 public:
   virtual int GetAttack() const {return attack;}
   virtual int GetDefense() const{return 0;}
};

Sekarang tidak perlu lagi untuk ifblok Anda , pemanggil dapat menggunakannya seperti

       currentAttack+=tool->GetAttack();
       currentDefense+=tool->GetDefense();

Tentu 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).

Doc Brown
sumber
4
atau, dalam hal ini, di gamedev.stackexchange.com
Kromster mengatakan mendukung Monica
7
Anda bahkan tidak perlu konsep Swordcara ini di basis kode Anda. Anda bisa saja new Tool("sword", swordAttack, swordDefense)dari misalnya file JSON.
AmazingDreams
7
@AmazingDreams: itu benar (untuk bagian-bagian dari kode yang kita lihat di sini), tapi saya kira OP telah menyederhanakan kode aslinya agar pertanyaannya fokus pada aspek yang ingin dia diskusikan.
Doc Brown
3
Ini tidak jauh lebih baik daripada kode aslinya (well, ini sedikit). Alat apa pun yang memiliki properti tambahan tidak dapat dibuat tanpa menambahkan metode tambahan. Saya pikir dalam hal ini orang harus lebih menyukai komposisi daripada warisan. Ya, saat ini hanya ada serangan dan pertahanan, tetapi itu tidak perlu tetap seperti itu.
Polygnome
1
@DocBrown Ya itu benar, meskipun terlihat seperti RPG di mana karakter memiliki beberapa statistik yang dimodifikasi oleh alat atau lebih tepatnya item yang dilengkapi. Saya akan membuat generik Tooldengan semua pengubah yang mungkin, mengisi beberapa vector<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 a tool->modify(playerStats)adalah pilihan lain.
AmazingDreams
23

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 attackdan defenseke kelas dasar dan cukup inisialisasi mereka 0. 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?

    • Anda dapat membuat anggota kelas dasar seperti virtual void onEquip(Owner*) {}dan virtual void onUnequip(Owner*) {}.
    • Kelebihan mereka akan dipanggil dan memodifikasi statistik ketika (un-) melengkapi item, misalnya virtual void onEquip(Owner *o) { o->modifyStat("attack", attackValue); }dan virtual void onUnequip(Owner *o) { o->modifyStat("attack", -attackValue); }.
    • Statistik dapat diakses dengan cara yang dinamis, misalnya menggunakan string pendek atau konstanta sebagai kunci, sehingga Anda bahkan bisa memperkenalkan nilai atau bonus spesifik gigi baru yang tidak perlu Anda tangani di pemain atau "pemilik" Anda secara khusus.
    • 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.

Mario
sumber
7

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:

class Tool {
    public:
        std::string name;
        int attack;
        int defense;
}

public void attack() {
    int attack = this->attack;
    int defense = this->defense;
    for (Tool* tool : tools){
        attack += tool->attack;
        defense += tool->defense;
    }
}

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 Toolkelas 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 attackmetode 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

class Component {
    public:
        // we need this, in Java we'd simply use getClass()
        virtual std::string type() const = 0;
};

Dan kemudian, kami membuat dua komponen pertama kami

class Attack : public Component {
    public:
        std::string type() const override { return std::string("mygame::components::Attack"); }
        int attackValue = 0;
};

class Defense : public Component {
    public:
      std::string type() const override { return std::string("mygame::components::Defense"); }
      int defenseValue = 0;
};

Setelah itu, kami membuat Alat memegang satu set properti, dan membuat properti-permintaan oleh orang lain.

class Tool {
private:
    std::map<std::string, Component*> components;

public:
    /** Adds a component to the tool */
    void addComponent(Component* component) { 
        components[component->type()] = component;
    };
    /** Removes a component from the tool */
    void removeComponent(Component* component) { components.erase(component->type()); };
    /** Return the component with the given type */
    Component* getComponentByType(std::string type) { 
        std::map<std::string, Component*>::iterator it = components.find(type);
        if (it != components.end()) { return it->second; }
        return nullptr;
    };
    /** Check wether a tol has a given component */
    bool hasComponent(std::string type) {
        std::map<std::string, Component*>::iterator it = components.find(type);
        return it != components.end();
    }
};

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: Toolsekarang ditutup untuk modifikasi - kita tidak akan pernah menyentuh sumber Toollagi - 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:

class Player {
    private:
        int attack = 0; 
        int defense = 0;
        int walkSpeed;
    public:
        std::vector<Tool*> tools;
        std::vector<Tool*> getToolsByComponentType(std::string type) {
            std::vector<Tool*> retVal;
            for (Tool* tool : tools) {
                if (tool->hasComponent(type)) { 
                    retVal.push_back(tool); 
                }
            }
            return retVal;
        }

        void doAttack() {
            int attackValue = this->attack;
            int defenseValue = this->defense;

            for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components::Attack"))) {
                Attack* component = (Attack*) tool->getComponentByType(std::string("mygame::components::Attack"));
                attackValue += component->attackValue;
            }
            for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components::Defense"))) {
                Defense* component = (Defense*)tool->getComponentByType(std::string("mygame::components::Defense"));
                defenseValue += component->defenseValue;
            }
            std::cout << "Attack with strength " << attackValue << "! Defend with strenght " << defenseValue << "!";
        }
};

Anda juga bisa merevisi ini ke dalam Inventorykelas 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 walkTometode, 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:

class WalkSpeed : public Component {
public:
    std::string type() const override { return std::string("mygame::components::WalkSpeed"); }
    int speedBonus;
};

Kemudian Anda cukup menambahkan instance komponen ini ke alat yang Anda ingin meningkatkan kecepatan bangun Anda, dan mengubah WalkTometode untuk memproses komponen yang baru saja Anda buat:

void walkTo() {
    int walkSpeed = this->walkSpeed;

    for (Tool* tool : this->getToolsByComponentType(std::string("mygame::components:WalkSpeed"))) {
        WalkSpeed* component = (WalkSpeed*)tool->getComponentByType(std::string("mygame::components::Defense"));
        walkSpeed += component->speedBonus;
        std::cout << "Walk with " << walkSpeed << std::endl;
    }
}

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 Combatkomponen yang menandai pemain agar dapat berpartisipasi dalam pertempuran, maka Anda dapat menyingkirkan attackmetode 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 Movablekomponen, sehingga Anda tidak perlu menerapkan walkTometode 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.

Polygnome
sumber
1

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 Toolmemiliki AttackBonusdan DefenseBonus- yang keduanya bisa 0berjaga-jaga jika tidak berguna untuk bertarung seperti bulu atau sesuatu seperti itu.

Untuk serangan, Anda mendapatkan baserate+ bonusdari senjata yang digunakan. Hal yang sama berlaku untuk pertahanan baserate+ bonus.

Karena itu Anda Toolharus memiliki virtualmetode untuk menghitung serangan / pertahanan boni.

tl; dr

Dengan desain yang lebih baik, Anda bisa menghindari peretasan if.

Thomas Junk
sumber
Kadang-kadang jika diperlukan, misalnya ketika membandingkan nilai skalar. Untuk beralih jenis objek, tidak terlalu banyak.
Andy
Haha, jika operator yang sangat penting dan Anda tidak bisa hanya mengatakan bahwa menggunakan kode bau.
tymtam
1
@Tymski untuk beberapa rasa hormat Anda benar. Saya membuat diri saya lebih jelas. Saya tidak membela ifkurang pemrograman. Sebagian besar dalam kombinasi seperti jika instanceofatau sesuatu seperti itu. Tetapi ada posisi, yang mengklaim ifs adalah kodemell dan ada cara untuk mengatasinya. Dan Anda benar, itu adalah operator penting yang memiliki haknya sendiri.
Thomas Junk
1

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, defensedan toolsvariabel. 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 toolsdaftar baru saya . Bagaimanapun, Toolini 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 bindtidak menerima sockaddr_in*, ia menerima yang lebih umum sockaddr*. Jika Anda melihat definisi untuk kelas-kelas itu, kami melihat bahwa sockaddrhanya memiliki satu anggota keluarga yang kami ditugaskan untuk sin_family*. Keluarga mengatakan subtipe yang harus Anda gunakan sockaddr. AF_INETmemberitahu 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 Toolcontoh 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 namebidang dan soket UNIX dengan sin_familybidangnya. 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 ifpernyataan besar atauswitchpernyataan, 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::stringbiasanya 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 switchpernyataan yang melibatkan enum, dengan satu-satunya bagian terpenting dari pola ini: defaultkasus 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 enumadalah 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 Toolkelas, 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 typebidang 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_INET6lakukan, 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, Shielddan MagicClothkelas, 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, sebelumboostsedikit!), 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, Toolmemiliki fungsi virtual accept,. Jika Anda memberikannya pengunjung, diharapkan untuk berbalik dan memanggil visitfungsi 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 visitakan 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::visitfungsi 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 ifatau switchpernyataan 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_familydan yang lain sepenuhnya dipertukarkan, menggunakan mana saja yang paling intuitif untuk prosa, percaya bahwa tipu C menangani perincian yang tidak penting.

Cort Ammon - Reinstate Monica
sumber
Menyerang secara berurutan akan membuat pemain menjadi sangat kuat dalam contoh Anda. Dan Anda tidak dapat memperluas pendekatan Anda tanpa memodifikasi sumber ToolVisitor. Ini solusi yang bagus.
Polygnome
@ Polygnome Anda benar tentang contoh ini. Saya pikir kode itu terlihat aneh, tetapi bergulir melewati semua halaman teks yang saya lewatkan kesalahan. Perbaiki sekarang. Adapun persyaratan memodifikasi sumber ToolVisitor, itu adalah karakteristik desain dari pola Pengunjung. Itu adalah berkah (seperti yang saya tulis) dan kutukan (seperti yang Anda tulis). Menangani kasus di mana Anda menginginkan versi yang dapat diperluas secara sewenang-wenang ini jauh lebih sulit, dan mulai menggali makna variabel, bukan hanya nilainya, dan membuka pola lain, seperti variabel yang diketik dengan lemah, kamus dan JSON.
Cort Ammon - Reinstate Monica
1
Ya, sayangnya kami tidak cukup tahu tentang preferensi dan tujuan OP untuk membuat keputusan yang benar-benar terinformasi. Dan ya, solusi yang sepenuhnya fleksibel lebih sulit untuk diterapkan, saya bekerja pada jawaban saya selama hampir 3 jam, karena C ++ saya cukup berkarat :(
Polygnome
0

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

class Tool{
    public:
    //constructor, name etc.
    int GetAttack() { return attack }; //Endpoints for your Player
    int GetDefense() { return defense };
    protected:
         int attack;
         int defense;
};

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.

Arthur Havlicek
sumber
0

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:

  • Barang
  • Item yang bisa diserang.
  • Barang yang bisa memiliki pertahanan.

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:

class Attack
{
private:
  int attack_;

public:
  int AttackValue() const;
};

class Defense
{
private:
  int defense_

public:
  int DefenseValue() const;
};

class Tool
{
private:
  std::optional<Attack> atk_;
  std::optional<Defense> def_;

public:
  const std::optional<Attack> &GetAttack() const {return atk_;}
  const std::optional<Defense> &GetDefense() const {return def_;}
};
Nicol Bolas
sumber
Juga: jangan gunakan pendekatan selalu-buat :)! Mengapa menggunakan komposisi dalam hal ini? Saya setuju bahwa ini adalah solusi alternatif, tetapi membuat kelas untuk "merangkum" bidang (perhatikan "") tampaknya aneh dalam kasus ini ...
AilurusFulgens
@AilurusFulgens: Ini "lapangan" hari ini. Apa yang akan terjadi besok? Desain ini memungkinkan Attackdan Defensemenjadi lebih rumit tanpa mengubah antarmuka Tool.
Nicol Bolas
1
Anda masih tidak dapat memperpanjang Alat dengan sangat baik dengan ini - tentu saja, serangan dan pertahanan mungkin menjadi lebih kompleks, tapi hanya itu. Jika Anda menggunakan komposisi dengan kekuatan penuh, Anda bisa membuatnya Tooltertutup sepenuhnya untuk modifikasi sambil tetap membiarkannya terbuka untuk ekstensi.
Polygnome
@ Polygnome: Jika Anda ingin melalui masalah membuat seluruh sistem komponen arbitrer untuk kasus sepele seperti ini, itu terserah Anda. Saya pribadi tidak melihat alasan mengapa saya ingin memperpanjang Tooltanpa mengubahnya . Dan jika saya memiliki hak untuk memodifikasinya, maka saya tidak melihat perlunya komponen yang berubah-ubah.
Nicol Bolas
Selama Alat berada di bawah kendali Anda sendiri, Anda dapat memodifikasinya. Tetapi prinsip "ditutup untuk modifikasi, terbuka untuk perluasan" ada karena alasan yang baik (terlalu panjang untuk diuraikan di sini). Saya tidak berpikir itu sepele. Jika Anda menghabiskan waktu yang tepat merencanakan sistem komponen fleksibel untuk RPG, Anda akan mendapatkan hadiah yang luar biasa dalam jangka panjang. Saya tidak melihat manfaat tambahan dalam ini jenis komposisi lebih dari hanya menggunakan bidang polos. Mampu mengkhususkan serangan dan pertahanan lebih jauh tampaknya merupakan skenario yang sangat teoretis. Tetapi seperti yang saya tulis, itu tergantung pada persyaratan persis OP.
Polygnome
0

Mengapa tidak membuat metode abstrak modifyAttackdan modifyDefensedi Toolkelas? Maka setiap anak akan memiliki implementasi mereka sendiri, dan Anda memanggil cara yang elegan ini:

for(Tool* tool : tools){
    currentAttack = tool->recalculateAttack(currentAttack);
    currentDefense = tool->recalculateDefense(currentDefense);
}
// proceed with new values for currentAttack and currentDefense

Melewati nilai sebagai referensi akan menghemat sumber daya jika Anda dapat:

for(Tool* tool : tools){
    tool->recalculateAttack(&currentAttack);
    tool->recalculateDefense(&currentDefense);
}
// proceed with new values for currentAttack and currentDefense
Paulo Amaral
sumber
0

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:

class Tool{
 public:
   virtual void equipTo(Player* player) =0;
   virtual void unequipFrom(Player* player) =0;
};

class Sword : public Tool{
  public:
    int attack;
    virtual void equipTo(Player* player) {
      player->attackBonus+=this->attack;
    };
    //unequipFrom = reverse equip
};
class Shield : public Tool{
  public:
    int defense;
    virtual void equipTo(Player* player) {
      player->defenseBonus+=this->defense;
    };
    //unequipFrom = reverse equip
};
//other tools
class Player{
  public:
    int baseAttack;
    int baseDefense;
    int attackBonus;
    int defenseBonus;

    virtual void equip(Tool* tool) {
      tool->equipTo(this);
      this->tools.push_back(tool)
    };

    //unequip = reverse equip

    void attack(){
      //modified attack and defense
      int modifiedAttack = baseAttack + this->attackBonus;
      int modifiedDefense = baseDefense+ this->defenseBonus;
      //some other functions to start attack
    }
  private:
    vector<Tool*> tools;
};

Ini memiliki keuntungan sebagai berikut:

  • lebih mudah untuk menambahkan kelas baru: Anda hanya perlu menerapkan semua metode abstrak dan sisanya hanya berfungsi
  • lebih mudah untuk menghapus kelas
  • lebih mudah untuk menambahkan statistik baru (kelas yang tidak peduli dengan stat abaikan saja)
Siphor
sumber
Anda setidaknya harus memasukkan metode unequip () yang menghapus bonus dari pemain.
Polygnome
0

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 intatau enum. Karena daftar item menjadi lebih panjang, itu if-elsemulai menjadi sangat sulit, jadi Anda dapat mempertimbangkan refactoring menjadi switch-case. Anda juga punya cukup dinding teks pada titik ini sehingga Anda dapat memisahkan tindakan dalam masing case- 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 kembali 0atau ApplyDefenseBonus(int& defence)mungkin defencetidak 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)

Pengembang dalam Pengembangan
sumber
0

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 Tooldengan bertopik untuk semua properti alat yang mungkin: sekarang Toolkelas 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 atribut attack, invisibility, healthdll 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 memiliki attackstatistik, 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.

alexis
sumber
-4

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!

Ostmeistro
sumber
4
Meningkatkan keterampilan dengan pengalaman pribadi itu baik, tentu saja. Tetapi meningkatkan keterampilan dengan bertanya kepada orang-orang yang sudah memiliki pengalaman pribadi itu, jadi Anda tidak perlu jatuh ke dalam lubang itu sendiri, lebih pintar. Tentunya itulah alasan mengapa orang mengajukan pertanyaan di sini, bukan?
Graham
Saya tidak setuju. Tapi saya mengerti bahwa situs ini adalah tentang kedalaman. Terkadang itu berarti terlalu berlebihan. Inilah mengapa saya ingin memposting pendapat ini, karena itu berlabuh dalam kenyataan dan jika Anda mencari tips untuk menjadi lebih baik dan membantu pemula, Anda melewatkan seluruh bab ini tentang "cukup baik" yang cukup berguna untuk diketahui oleh pemula.
Ostmeistro