Ketika beberapa kelas perlu mengakses data yang sama, di mana seharusnya data dinyatakan?

39

Saya memiliki game dasar menara 2D di C ++.

Setiap peta adalah kelas terpisah yang mewarisi dari GameState. Peta mendelegasikan logika dan menggambar kode untuk setiap objek dalam permainan dan menetapkan data seperti jalur peta. Dalam pseudo-code, bagian logika mungkin terlihat seperti ini:

update():
  for each creep in creeps:
    creep.update()
  for each tower in towers:
    tower.update()
  for each missile in missiles:
    missile.update()

Benda-benda (merayap, menara dan rudal) disimpan dalam vektor-of-pointer. Menara harus memiliki akses ke vektor-creep dan vektor-rudal untuk membuat rudal baru dan mengidentifikasi target.

Pertanyaannya adalah: di mana saya mendeklarasikan vektor? Haruskah mereka menjadi anggota kelas Peta, dan diteruskan sebagai argumen ke fungsi tower.update ()? Atau dinyatakan secara global? Atau ada solusi lain yang saya lewatkan sepenuhnya?

Ketika beberapa kelas perlu mengakses data yang sama, di mana seharusnya data dinyatakan?

Berair
sumber
1
Anggota global dianggap 'jelek' tetapi cepat dan membuat pengembangan lebih mudah, jika ini adalah permainan kecil, itu tidak masalah (IMHO). Anda juga bisa membuat kelas exteral yang menangani logika ( mengapa menara membutuhkan vektor ini) dan memiliki akses ke semua vektor.
Jonathan Connell
-1 jika ini terkait dengan pemrograman game, maka makan pizza juga. Ambil sendiri beberapa buku desain perangkat lunak yang bagus
Maik Semder
9
@Maik: Bagaimana desain perangkat lunak tidak terkait dengan pemrograman game? Hanya karena itu juga berlaku untuk bidang pemrograman lain tidak menjadikannya di luar topik.
BlueRaja - Danny Pflughoeft
@BlueRaja daftar pola desain perangkat lunak lebih cocok pada SO, itulah yang ada di sana untuk semua. GD.SE adalah untuk pemrograman game, bukan desain perangkat lunak
Maik Semder

Jawaban:

53

Saat Anda membutuhkan satu instance kelas di seluruh program Anda, kami menyebut kelas itu layanan . Ada beberapa metode standar untuk mengimplementasikan layanan dalam program:

  • Variabel global . Ini adalah yang paling mudah diimplementasikan, tetapi desain yang terburuk. Jika Anda menggunakan terlalu banyak variabel global, Anda akan dengan cepat mendapati diri Anda menulis modul yang terlalu banyak mengandalkan satu sama lain ( penggandaan kuat ), membuat alur logika sangat sulit untuk diikuti. Variabel global tidak ramah multithreading. Variabel global membuat pelacakan masa pakai objek lebih sulit, dan mengacaukan namespace. Namun, mereka adalah opsi yang paling berkinerja, jadi ada kalanya mereka dapat dan harus digunakan, tetapi menggunakannya dengan tenang.
  • Lajang . Sekitar 10-15 tahun yang lalu, lajang yang satu desain-pola besar untuk tahu tentang. Namun, saat ini mereka dipandang rendah. Mereka jauh lebih mudah untuk multi-utas, tetapi Anda harus membatasi penggunaannya pada satu utas pada satu waktu, yang tidak selalu seperti yang Anda inginkan. Melacak masa hidup sama sulitnya dengan variabel global.
    Kelas singleton tipikal akan terlihat seperti ini:

    class MyClass
    {
    private:
        static MyClass* _instance;
        MyClass() {} //private constructor
    
    public:
        static MyClass* getInstance();
        void method();
    };
    
    ...
    
    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
        if(_instance == NULL)
            _instance = new MyClass(); //Not thread-safe version
        return _instance;
    
        //Note that _instance is *never* deleted - 
        //it exists for the entire lifetime of the program!
    }
  • Ketergantungan Injeksi (DI) . Ini hanya berarti melewatkan layanan sebagai parameter konstruktor. Suatu layanan harus sudah ada untuk lulus ke dalam kelas, jadi tidak ada cara bagi dua layanan untuk saling bergantung; dalam 98% kasus, inilah yang Anda inginkan (dan untuk 2% lainnya, Anda selalu dapat membuat setWhatever()metode dan meneruskan layanan nanti) . Karena itu, DI tidak memiliki masalah kopling yang sama dengan opsi lainnya. Ini dapat digunakan dengan multithreading, karena setiap utas hanya dapat memiliki contoh sendiri dari setiap layanan (dan hanya berbagi yang benar-benar perlu). Itu juga membuat kode unit dapat diuji, jika Anda peduli tentang itu.

    Masalah dengan injeksi ketergantungan adalah ia membutuhkan lebih banyak memori; sekarang setiap instance kelas membutuhkan referensi ke setiap layanan yang akan digunakan. Juga, itu akan mengganggu untuk digunakan ketika Anda memiliki terlalu banyak layanan; ada kerangka kerja yang mengurangi masalah ini dalam bahasa lain, tetapi karena kurangnya refleksi C ++, kerangka kerja DI di C ++ cenderung lebih berfungsi daripada hanya melakukannya secara manual.

    //Example of dependency injection
    class Tower
    {
    private:
        MissileCreationService* _missileCreator;
        CreepLocatorService* _creepLocator;
    public:
        Tower(MissileCreationService*, CreepLocatorService*);
    }
    
    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to 
    // add a new service to the Tower constructor, we must add it to the
    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.

    Lihat halaman ini (dari dokumentasi untuk Ninject, kerangka kerja C # DI) untuk contoh lain.

    Injeksi ketergantungan adalah solusi yang biasa untuk masalah ini, dan merupakan jawaban yang akan Anda lihat paling terunggulkan untuk pertanyaan seperti ini di StackOverflow.com. DI adalah jenis Pembalikan Kontrol (IoC).

  • Pencari Lokasi Layanan . Pada dasarnya, hanya kelas yang menyimpan instance dari setiap layanan. Anda dapat melakukannya menggunakan refleksi , atau Anda bisa menambahkan contoh baru ke dalamnya setiap kali Anda ingin membuat layanan baru. Anda masih memiliki masalah yang sama seperti sebelumnya - Bagaimana kelas mengakses locator ini? - yang dapat diselesaikan dengan salah satu cara di atas, tetapi sekarang Anda hanya perlu melakukannya untuk ServiceLocatorkelas Anda , bukan untuk puluhan layanan. Metode ini juga dapat diuji unit, jika Anda peduli tentang hal semacam itu.

    Service Locators adalah bentuk lain dari Inversion of Control (IoC). Biasanya, kerangka kerja yang melakukan injeksi dependensi otomatis juga akan memiliki pelacak layanan.

    XNA (kerangka kerja pemrograman game C # Microsoft) mencakup pelacak layanan; untuk mempelajari lebih lanjut, lihat jawaban ini .


Ngomong-ngomong, IMHO menara tidak harus tahu tentang merinding. Kecuali jika Anda berencana hanya mengulang daftar creep untuk setiap menara, Anda mungkin ingin menerapkan beberapa partisi ruang nontrivial ; dan logika semacam itu tidak termasuk dalam kelas menara.

BlueRaja - Danny Pflughoeft
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Josh
Salah satu jawaban terbaik dan paling jelas yang pernah saya baca. Sudah selesai dilakukan dengan baik. Saya pikir layanan selalu seharusnya dibagikan.
Nikos
5

Saya pribadi akan menggunakan polimorfisme di sini. Mengapa memiliki missilevektor, towervektor, dan vektor creep.. ketika mereka semua memanggil fungsi yang sama; update? Mengapa tidak memiliki vektor pointer ke beberapa kelas dasar Entityatau GameObject?

Saya menemukan cara yang baik untuk merancang adalah berpikir 'apakah ini masuk akal dalam hal kepemilikan'? Jelas sebuah menara memiliki cara untuk memperbarui dirinya sendiri, tetapi apakah peta memiliki semua objek di dalamnya? Jika Anda ingin global, apakah Anda mengatakan bahwa tidak ada yang memiliki menara dan merinding? Global biasanya merupakan solusi buruk - mempromosikan pola desain yang buruk, namun jauh lebih mudah untuk dikerjakan. Pertimbangkan menimbang 'apakah saya ingin menyelesaikan ini?' dan 'apakah saya menginginkan sesuatu yang dapat saya gunakan kembali'?

Salah satu cara mengatasi hal ini adalah beberapa bentuk sistem pengiriman pesan. Yang towerdapat mengirim pesan ke map(yang memiliki akses ke, mungkin referensi ke pemiliknya?) Yang ditabrak creep, dan mapkemudian memberitahucreep itu telah dipukul. Ini sangat bersih dan memisahkan data.

Cara lain adalah dengan hanya mencari peta itu sendiri untuk apa yang diinginkannya. Namun, mungkin ada masalah dengan urutan pembaruan di sini.

Bebek Komunis
sumber
1
Saran Anda tentang polimorfisme tidak benar-benar relevan. Saya minta mereka disimpan dalam vektor yang terpisah sehingga saya bisa mengulangi setiap jenis secara individual, seperti dalam kode gambar (di mana saya ingin benda-benda tertentu digambar terlebih dahulu) atau dalam kode tumbukan.
Juicy
Untuk tujuan saya, peta memang memiliki entitas, karena peta di sini analog dengan 'level'. Saya akan mempertimbangkan ide Anda tentang pesan, terima kasih.
Juicy
1
Dalam permainan, masalah kinerja. Jadi vektor dari waktu objek yang sama memiliki lokalitas referensi yang lebih baik. Juga, objek polimorfik dengan pointer virtual memiliki kinerja yang mengerikan karena mereka tidak dapat dimasukkan ke dalam loop pembaruan.
Zan Lynx
0

Ini adalah kasus di mana pemrograman berorientasi objek ketat (OOP) rusak.

Menurut prinsip-prinsip OOP, Anda harus mengelompokkan data dengan perilaku terkait menggunakan kelas. Tetapi Anda memiliki perilaku (penargetan) yang membutuhkan data yang tidak terkait satu sama lain (menara dan merayap). Dalam situasi ini, banyak programmer akan mencoba mengaitkan perilaku dengan bagian dari data yang dibutuhkan (misalnya menara menangani penargetan, tetapi tidak tahu tentang creep), tetapi ada opsi lain: jangan kelompok perilaku dengan data.

Alih-alih menjadikan perilaku penargetan sebagai metode kelas menara, menjadikannya fungsi bebas yang menerima menara dan merayap sebagai argumen. Ini mungkin memerlukan membuat lebih banyak anggota yang tersisa di menara dan kelas merayap publik, dan itu tidak masalah. Menyembunyikan data berguna, tetapi itu sarana, bukan tujuan itu sendiri, dan Anda tidak harus menjadi budaknya. Selain itu, anggota pribadi bukan satu-satunya cara untuk mengontrol akses ke data - jika data tidak dialihkan ke fungsi dan bukan global, itu secara efektif disembunyikan dari fungsi itu. Jika menggunakan teknik ini memungkinkan Anda menghindari data global, Anda mungkin sebenarnya meningkatkan enkapsulasi.

Contoh ekstrem dari pendekatan ini adalah arsitektur sistem entitas .

Steve S
sumber