Kopling rendah dan kohesi yang ketat


11

Tentu itu tergantung situasi. Tetapi ketika objek atau sistem tuas yang lebih rendah berkomunikasi dengan sistem tingkat yang lebih tinggi, haruskah panggilan balik atau peristiwa lebih disukai daripada mempertahankan pointer ke objek tingkat yang lebih tinggi?

Sebagai contoh, kami memiliki worldkelas yang memiliki variabel anggota vector<monster> monsters. Ketika monsterkelas berkomunikasi dengan world, haruskah saya lebih suka menggunakan fungsi panggilan balik atau haruskah saya memiliki pointer ke worldkelas di dalam monsterkelas?


Selain dari pengejaan dalam judul pertanyaan, itu sebenarnya tidak diutarakan dalam bentuk pertanyaan. Saya pikir mengulanginya mungkin membantu memperkuat apa yang Anda minta di sini, karena saya pikir Anda tidak bisa mendapatkan jawaban yang berguna untuk pertanyaan ini dalam bentuk saat ini. Dan ini juga bukan pertanyaan desain game, ini pertanyaan tentang struktur pemrograman (tidak yakin apakah itu berarti tag desain sesuai atau tidak, tidak dapat mengingat di mana kita menemukan 'desain perangkat lunak' dan tag)
MrCranky

Jawaban:


10

Ada tiga cara utama agar satu kelas dapat berbicara dengan yang lain tanpa terikat erat dengannya:

  1. Melalui fungsi panggilan balik.
  2. Melalui sistem acara.
  3. Melalui antarmuka.

Ketiganya terkait erat satu sama lain. Sistem acara dalam banyak hal hanyalah daftar panggilan balik. Panggilan balik lebih atau kurang antarmuka dengan metode tunggal.

Di C ++, saya jarang menggunakan callback:

  1. C ++ tidak memiliki dukungan yang baik untuk panggilan balik yang mempertahankan thispenunjuk mereka , jadi sulit untuk menggunakan panggilan balik dalam kode berorientasi objek.

  2. Callback pada dasarnya adalah antarmuka satu metode yang tidak dapat diperluas. Seiring waktu, saya menemukan bahwa saya hampir selalu akhirnya membutuhkan lebih dari satu metode untuk mendefinisikan antarmuka itu dan panggilan balik tunggal jarang cukup.

Dalam hal ini, saya mungkin akan melakukan antarmuka. Dalam pertanyaan Anda, Anda sebenarnya tidak menguraikan apa yang monstersebenarnya perlu dikomunikasikan world. Mengambil tebakan, saya akan melakukan sesuatu seperti:

class IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) = 0;
  virtual Item*    getItemAt(const Position & position) = 0;
};

class Monster {
public:
  void update(IWorld * world) {
    // Do stuff...
  }
}

class World : public IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) {
    // ...
  }

  virtual Item*    getItemAt(const Position & position) {
    // ...
  }

  // Lots of other stuff that Monster should not have access to...
}

Idenya di sini adalah bahwa Anda hanya memasukkan IWorld(yang merupakan nama jelek) minimum yang Monsterperlu diakses. Pandangannya tentang dunia harus sesempit mungkin.


1
+1 Delegasi (panggilan balik) biasanya menjadi lebih banyak seiring berjalannya waktu. Memberikan antarmuka kepada monster sehingga mereka bisa mendapatkan barang adalah cara yang baik untuk menurut saya.
Michael Coleman

12

Jangan gunakan fungsi panggilan balik untuk menyembunyikan fungsi apa yang Anda panggil. Jika Anda dimasukkan ke dalam fungsi panggilan balik, dan fungsi panggilan balik itu akan memiliki satu dan hanya satu fungsi yang ditetapkan untuk itu, maka Anda tidak benar-benar merusak kopling sama sekali. Anda hanya menutupinya dengan lapisan abstraksi lain. Anda tidak mendapatkan apa pun (selain dari waktu kompilasi), tetapi Anda kehilangan kejelasan.

Saya tidak akan menyebutnya sebagai praktik terbaik, tetapi merupakan pola umum untuk memiliki entitas yang berisi sesuatu untuk memiliki pointer ke orang tua mereka.

Yang sedang berkata, mungkin bernilai saat Anda menggunakan pola antarmuka untuk memberikan monster Anda sejumlah fungsi yang dapat mereka panggil di dunia.


+1 Memberi monster itu cara terbatas untuk memanggil orang tua adalah jalan tengah yang bagus menurut saya.
Michael Coleman

7

Secara umum saya mencoba dan menghindari tautan dua arah, tetapi jika saya harus memilikinya, saya benar-benar yakin bahwa ada satu metode untuk membuatnya dan satu untuk memutusnya, sehingga Anda tidak pernah mendapatkan inkonsistensi.

Seringkali Anda dapat menghindari tautan dua arah sepenuhnya dengan mengirimkan data yang diperlukan. Refactoring yang sepele adalah membuatnya agar alih-alih memiliki monster yang menjaga tautan ke dunia, Anda meneruskan dunia dengan merujuk pada metode monster yang membutuhkannya. Lebih baik lagi adalah dengan hanya memberikan antarmuka untuk bit-bit dunia yang benar-benar dibutuhkan monster, artinya monster itu tidak bergantung pada implementasi konkret dunia. Ini sesuai dengan Prinsip Segregasi Antarmuka dan Prinsip Pembalikan Ketergantungan , tetapi tidak mulai memperkenalkan abstraksi berlebih yang kadang-kadang Anda dapatkan dengan peristiwa, sinyal + slot, dll.

Di satu sisi, Anda bisa berpendapat bahwa menggunakan panggilan balik adalah antarmuka mini yang sangat khusus, dan itu bagus. Anda harus memutuskan apakah Anda dapat mencapai tujuan dengan lebih bermakna melalui satu kumpulan metode dalam objek antarmuka atau beberapa yang berbeda dalam panggilan balik yang berbeda.


3

Saya mencoba untuk menghindari benda-benda berisi memanggil wadah mereka karena saya menemukan hal itu menyebabkan kebingungan, itu menjadi terlalu mudah untuk dibenarkan, itu akan menjadi terlalu sering digunakan dan itu menciptakan ketergantungan yang tidak dapat dikelola.

Menurut pendapat saya, solusi yang ideal adalah kelas yang lebih tinggi cukup pintar untuk mengelola kelas yang lebih rendah. Sebagai contoh, dunia yang mengetahui untuk menentukan apakah tabrakan antara monster dan ksatria terjadi tanpa mengetahui tentang yang lain lebih baik bagiku daripada monster yang bertanya kepada dunia apakah itu bertabrakan dengan seorang ksatria.

Opsi lain dalam kasus Anda, saya mungkin akan mencari tahu mengapa kelas monster perlu tahu tentang kelas dunia dan Anda kemungkinan besar akan menemukan bahwa ada sesuatu di kelas dunia yang dapat dibagi menjadi kelas tersendiri yang dibuatnya. merasakan kelas monster untuk tahu tentang.


2

Anda tidak akan pergi jauh tanpa peristiwa, jelas, tetapi bahkan sebelum mulai menulis (dan mendesain) sistem acara, Anda harus mengajukan pertanyaan sesungguhnya: mengapa monster itu berkomunikasi dengan kelas dunia? Haruskah itu benar?

Mari kita ambil situasi "klasik", monster menyerang pemain.

Monster sedang menyerang: dunia bisa mengidentifikasi situasi di mana seorang pahlawan berada di sebelah monster, dan memberitahu monster itu untuk menyerang. Jadi fungsi dalam monster adalah:

void Monster::attack(LivingCreature l)
{
  // Call to combat system
}

Tetapi dunia (yang sudah tahu monster itu) tidak perlu diketahui oleh monster itu. Sebenarnya, monster itu bisa mengabaikan keberadaan kelas Dunia, yang mungkin lebih baik.

Hal yang sama ketika monster bergerak (saya membiarkan sub-sistem mendapatkan makhluk dan menangani perhitungan bergerak / niat untuk itu, monster itu hanya sekumpulan data, tetapi banyak orang akan mengatakan ini bukan OOP asli).

Maksud saya adalah: peristiwa (atau panggilan balik) bagus, tentu saja, tetapi itu bukan satu-satunya jawaban untuk setiap masalah yang akan Anda hadapi.


1

Kapan pun saya bisa, saya mencoba membatasi komunikasi antara objek ke model permintaan dan respons. Ada urutan parsial tersirat pada objek dalam program saya sehingga antara dua objek A dan B, mungkin ada cara bagi A untuk secara langsung atau tidak langsung memanggil metode B atau untuk B untuk secara langsung atau tidak langsung memanggil metode A , tetapi tidak pernah mungkin bagi A dan B untuk saling memanggil metode satu sama lain. Terkadang, tentu saja, Anda ingin memiliki komunikasi mundur dengan penelepon suatu metode. Ada beberapa cara yang saya suka lakukan ini, dan tidak satu pun dari mereka adalah panggilan balik.

Salah satu caranya adalah dengan memasukkan lebih banyak informasi dalam nilai balik dari pemanggilan metode, yang berarti kode klien dapat memutuskan apa yang harus dilakukan dengannya setelah prosedur mengembalikan kontrol ke sana.

Cara lain adalah dengan memanggil objek anak bersama. Yaitu, jika A memanggil metode pada B, dan B perlu mengkomunikasikan beberapa informasi ke A, B memanggil metode pada C, di mana A dan B keduanya dapat memanggil C, tetapi C tidak dapat memanggil A atau B. Objek A akan menjadi bertanggung jawab untuk mendapatkan informasi dari C setelah B mengembalikan kontrol ke A. Perhatikan bahwa ini sebenarnya tidak jauh berbeda dari cara pertama yang saya usulkan. Objek A masih dapat hanya mengambil informasi dari nilai kembali; tidak ada metode objek A yang dipanggil oleh B atau C. Variasi trik ini adalah untuk lulus C sebagai parameter untuk metode, tetapi pembatasan pada hubungan C ke A dan B masih berlaku.

Sekarang, pertanyaan penting adalah mengapa saya bersikeras melakukan hal-hal seperti ini. Ada tiga alasan utama:

  • Itu membuat benda-benda saya lebih longgar digabungkan. Objek saya mungkin merangkum objek lain, tetapi mereka tidak akan pernah bergantung pada konteks pemanggil, dan konteks tidak akan pernah bergantung pada objek yang dienkapsulasi.
  • Itu membuat aliran kendali saya mudah dipikirkan. Sangat menyenangkan untuk dapat mengasumsikan bahwa satu-satunya kode yang dapat mengubah keadaan internal selfsementara metode mengeksekusi adalah satu metode dan tidak ada yang lain. Ini adalah jenis pemikiran yang sama yang mungkin menyebabkan seseorang untuk menempatkan mutex pada objek bersamaan.
  • Ini melindungi invarian pada data enkapsulasi objek saya. Metode publik diizinkan untuk bergantung pada invarian, dan invarian tersebut dapat dilanggar jika satu metode dapat dipanggil secara eksternal sementara metode lain sudah dijalankan.

Saya tidak menentang semua penggunaan callback. Sesuai dengan kebijakan saya tentang tidak pernah "memanggil penelepon," jika suatu objek A memanggil metode pada B dan meneruskan panggilan balik ke sana, panggilan balik tidak dapat mengubah keadaan internal A, dan itu termasuk objek yang dienkapsulasi oleh A dan objek dalam konteks A. Dengan kata lain, callback hanya dapat memanggil metode pada objek yang diberikan kepadanya oleh B. Callback, pada dasarnya, berada di bawah batasan yang sama dengan B.

Salah satu jalan keluar terakhir untuk mengikat adalah bahwa saya akan mengijinkan doa dari setiap fungsi murni, terlepas dari pemesanan parsial ini yang telah saya bicarakan. Fungsi murni sedikit berbeda dari metode yang tidak dapat diubah atau bergantung pada efek atau efek samping yang dapat berubah, sehingga tidak ada kekhawatiran tentang hal-hal yang membingungkan.


0

Sendiri? Saya hanya menggunakan singleton.

Ya, oke, desain buruk, tidak berorientasi objek, dll. Anda tahu? Saya tidak peduli . Saya sedang menulis game, bukan showcase teknologi. Tidak ada yang akan memberi saya kode. Tujuannya adalah membuat game yang menyenangkan, dan semua yang menghalangi saya akan menghasilkan game yang kurang menyenangkan.

Apakah Anda pernah menjalankan dua dunia sekaligus? Mungkin! Mungkin kamu akan. Tetapi kecuali jika Anda dapat memikirkan situasi itu sekarang, Anda mungkin tidak akan.

Jadi, solusi saya: bikin World singleton. Panggil fungsi di atasnya. Selesaikan dengan seluruh kekacauan. Anda bisa memberikan parameter tambahan untuk setiap fungsi tunggal - dan jangan salah, di situlah ini mengarah. Atau Anda bisa menulis kode yang berfungsi.

Melakukannya dengan cara ini membutuhkan sedikit disiplin untuk membersihkan segala sesuatunya ketika menjadi berantakan (itu "ketika", bukan "jika") tetapi tidak ada cara Anda dapat mencegah kode menjadi berantakan - baik Anda memiliki masalah spageti, atau ribuan -Pemain-of-abstraksi. Setidaknya dengan cara ini Anda tidak sedang menulis kode yang tidak perlu dalam jumlah besar.

Dan jika Anda memutuskan untuk tidak lagi menginginkan singleton, biasanya cukup mudah untuk menyingkirkannya. Membutuhkan beberapa pekerjaan, membutuhkan melewati satu miliar parameter di sekitar, tetapi itu adalah parameter yang Anda harus melewati sekitar


2
Saya mungkin akan mengucapkannya sedikit lebih ringkas: "Jangan takut untuk menolak".
Tetrad

Lajang itu jahat! Global jauh lebih baik. Semua kebajikan tunggal yang Anda daftarkan adalah sama untuk global. Saya menggunakan pointer global (sebenarnya fungsi global mengembalikan referensi) ke berbagai subsistem saya dan menginisialisasi / menghancurkan mereka di fungsi utama saya. Pointer global menghindari masalah urutan inisialisasi, menggantung lajang selama penghancuran, konstruksi lajang yang tidak sepele, dll. Saya ulangi lajang adalah kejahatan.
deft_code

@Tetrad, sangat setuju. Ini adalah salah satu keterampilan terbaik yang dapat Anda miliki.
ZorbaTHut
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.