Warisan vs Komposisi Untuk Potongan Catur

9

Pencarian cepat dari stackexchange ini menunjukkan bahwa secara umum komposisi umumnya dianggap lebih fleksibel daripada warisan tetapi seperti biasanya tergantung pada proyek dll dan ada kalanya pewarisan adalah pilihan yang lebih baik. Saya ingin membuat permainan catur 3D di mana setiap bagian memiliki jala, animasi yang mungkin berbeda dan sebagainya. Dalam contoh konkret ini, sepertinya Anda bisa memperdebatkan kasus untuk kedua pendekatan, apakah saya salah?

Warisan akan terlihat seperti ini (dengan konstruktor yang tepat, dll)

class BasePiece
{
    virtual Squares GetValidMoveSquares() = 0;
    Mesh* mesh;
    // Other fields
}

class Pawn : public BasePiece
{
   Squares GetValidMoveSquares() override;
}

yang tentu saja mematuhi prinsip "is-a" sedangkan komposisi akan terlihat seperti ini

class MovementComponent
{
    virtual Squares GetValidMoveSquares() = 0;
}

class PawnMovementComponent
{
     Squares GetValidMoveSquares() override;
}

enum class Type
{
     PAWN,
     BISHOP, //etc
}


class Piece
{
    MovementComponent* movementComponent;
    MeshComponent* mesh;
    Type type;
    // Other fields
 }

Apakah ini lebih merupakan masalah pilihan pribadi atau apakah satu pendekatan jelas merupakan pilihan yang lebih cerdas daripada yang lain di sini?

EDIT: Saya pikir saya belajar sesuatu dari setiap jawaban jadi saya merasa tidak enak karena hanya memilih satu. Solusi terakhir saya akan mengambil inspirasi dari beberapa posting di sini (masih mengerjakannya). Terima kasih kepada semua orang yang meluangkan waktu untuk menjawab.

CanISleepYet
sumber
Saya percaya keduanya bisa ada berdampingan, keduanya untuk tujuan yang berbeda, tetapi ini tidak eksklusif satu sama lain. Catur terdiri dari potongan-potongan dan papan yang berbeda, dan potongan-potongan dari jenis yang berbeda. Potongan-potongan ini berbagi beberapa properti dan perilaku dasar, dan juga memiliki properti dan perilaku tertentu. Jadi menurut saya, komposisi harus diterapkan di atas papan dan potongan-potongan, sedangkan warisan harus diikuti lebih dari jenis potongan.
Hitesh Gaur
Meskipun Anda mungkin mendapatkan beberapa orang yang mengklaim bahwa semua warisan adalah buruk, saya pikir gagasan umum adalah bahwa warisan antarmuka adalah baik / baik dan bahwa warisan implementasi berguna tetapi dapat bermasalah. Apa pun di luar satu tingkat warisan dipertanyakan, menurut pengalaman saya. Tidak apa-apa selain itu tetapi tidak mudah untuk melakukannya tanpa membuat kekacauan yang berbelit-belit.
JimmyJames
Pola strategi umum dalam pemrograman game karena suatu alasan. var Pawn = new Piece(howIMove, howITake, whatILookLike)tampaknya jauh lebih sederhana, lebih mudah dikelola dan lebih bisa dikelola bagi saya daripada hierarki warisan.
Semut P
@ AntP Itu poin yang bagus, terima kasih!
CanISleepYet
@AntP Buatlah jawaban! ... Ada banyak manfaat untuk itu!
svidgen

Jawaban:

4

Pada pandangan pertama, jawaban Anda berpura-pura solusi "komposisi" tidak menggunakan warisan. Tapi saya kira Anda lupa menambahkan ini:

class PawnMovementComponent : MovementComponent
{
     Squares GetValidMoveSquares() override;
}

(dan lebih banyak dari derivasi ini, satu untuk masing-masing dari enam jenis potongan). Sekarang ini lebih mirip pola "Strategi" klasik, dan juga memanfaatkan warisan.

Sayangnya, solusi ini memiliki kekurangan: masing-masing Piecesekarang menyimpan informasi jenisnya secara berlebihan dua kali:

  • di dalam variabel anggota type

  • di dalam movementComponent(diwakili oleh subtipe)

Redundansi inilah yang benar-benar dapat membawa Anda ke masalah - kelas sederhana seperti Pieceseharusnya menyediakan "sumber kebenaran tunggal", bukan dua sumber.

Tentu saja, Anda dapat mencoba menyimpan informasi jenis hanya di type, dan tidak membuat kelas anak MovementComponentjuga. Tetapi desain ini kemungkinan besar akan mengarah pada switch(type)pernyataan " " yang sangat besar dalam implementasi GetValidMoveSquares. Dan itu jelas merupakan indikasi kuat untuk pewarisan menjadi pilihan yang lebih baik di sini .

Catatan dalam desain "pewarisan", cukup mudah untuk menyediakan bidang "tipe" dengan cara yang tidak berlebihan: menambahkan metode virtual GetType()ke BasePiecedan mengimplementasikannya di setiap kelas dasar.

Mengenai "promosi": Saya di sini dengan @viden, saya menemukan argumen yang disajikan dalam jawaban @ TheCatWhisperer masih bisa diperdebatkan.

Menafsirkan "promosi" sebagai pertukaran fisik potongan alih-alih menafsirkannya sebagai perubahan jenis karya yang sama terasa lebih alami bagi saya. Jadi menerapkan ini seperti dalam cara yang sama - dengan menukar satu bagian dengan yang lain dari jenis yang berbeda - kemungkinan besar tidak akan menyebabkan masalah besar - setidaknya tidak untuk kasus catur tertentu.

Doc Brown
sumber
Terima kasih telah memberi tahu saya nama polanya, saya tidak menyadari itu disebut Strategi. Anda juga benar saya merindukan warisan komponen gerakan dalam pertanyaan saya. Apakah tipe ini benar-benar berlebihan? Jika saya ingin menerangi semua ratu di papan tulis, akan terasa aneh untuk memeriksa komponen gerakan apa yang mereka miliki, ditambah lagi saya perlu melemparkan komponen gerakan untuk melihat tipe apa yang akan menjadi sangat jelek, bukan?
CanISleepYet
@CanISleepYet: masalah dengan bidang "tipe" yang Anda usulkan adalah, ia bisa menyimpang dari subtipe movementComponent. Lihat edit saya bagaimana menyediakan bidang jenis dengan cara yang aman dalam desain "warisan".
Doc Brown
4

Saya mungkin lebih suka opsi yang lebih sederhana kecuali jika Anda memiliki alasan yang jelas, diartikulasikan dengan baik, atau masalah ekstensiabilitas dan pemeliharaan yang kuat .

Opsi pewarisan adalah baris kode yang lebih sedikit dan kompleksitas yang lebih sedikit. Setiap jenis karya memiliki hubungan 1-ke-1 yang "tidak berubah" dengan karakteristik gerakannya. (Tidak seperti itu representasi, yang 1-ke-1, tetapi tidak selalu berubah - Anda mungkin ingin menawarkan berbagai "kulit".)

Jika Anda memecah hubungan 1-ke-1 yang tidak berubah antara bagian-bagian dan perilaku / gerakan mereka ke dalam beberapa kelas, Anda mungkin hanya menambahkan kompleksitas - dan tidak banyak lagi. Jadi, jika saya meninjau atau mewarisi kode itu, saya berharap dapat melihat alasan yang bagus dan terdokumentasi untuk kompleksitasnya.

Semua yang dikatakan, opsi yang bahkan lebih sederhana , IMO, adalah untuk membuat Piece antarmuka yang mengimplementasikan Potongan individu Anda . Dalam kebanyakan bahasa, ini sebenarnya sangat berbeda dari warisan , karena sebuah antarmuka tidak akan membatasi Anda untuk mengimplementasikan antarmuka lain . ... Anda hanya tidak mendapatkan perilaku kelas dasar secara gratis dalam hal ini: Anda ingin menempatkan perilaku bersama di tempat lain.

svidgen
sumber
Saya tidak percaya bahwa solusi warisan adalah 'less lines of code'. Mungkin di kelas yang satu ini tetapi tidak secara keseluruhan.
JimmyJames
@ JimmyJames Benarkah? ... Hitung saja baris-baris kode di OP ... memang bukan seluruh solusi, tetapi bukan indikator yang buruk dari solusi mana yang lebih LOC dan kompleksitas untuk dijaga.
svidgen
Saya tidak ingin terlalu mempermasalahkan hal ini karena itu semua nosional pada titik ini tapi ya, saya benar-benar berpikir begitu. Pendapat saya adalah bahwa sedikit kode ini dapat diabaikan dibandingkan dengan keseluruhan aplikasi. Jika ini menyederhanakan implementasi aturan (dapat diperdebatkan), maka Anda dapat menyimpan puluhan atau bahkan ratusan baris kode.
JimmyJames
Terima kasih, saya tidak memikirkan antarmuka tetapi Anda mungkin benar bahwa ini adalah cara terbaik untuk pergi. Lebih sederhana hampir selalu lebih baik.
CanISleepYet
2

Masalahnya dengan catur adalah bahwa aturan permainan dan permainan ditentukan dan diperbaiki. Desain apa pun yang berfungsi baik - gunakan apa pun yang Anda suka! Eksperimen dan coba semuanya.

Dalam dunia bisnis, bagaimanapun, tidak ada yang begitu ketat ditentukan seperti ini - aturan & persyaratan bisnis berubah seiring waktu, dan program harus berubah dari waktu ke waktu untuk mengakomodasi. Di sinilah mana-a vs memiliki-a vs data membuat perbedaan. Di sini, kesederhanaan membuat solusi kompleks lebih mudah untuk dipertahankan seiring waktu dan perubahan persyaratan. Secara umum, dalam bisnis, kita juga harus berurusan dengan kegigihan, yang mungkin melibatkan basis data juga. Jadi, kami memiliki aturan seperti jangan gunakan beberapa kelas saat satu kelas akan melakukan pekerjaan, dan, jangan gunakan warisan ketika komposisinya mencukupi. Tetapi semua aturan ini diarahkan untuk membuat kode dapat dipertahankan dalam jangka panjang dalam menghadapi perubahan peraturan & persyaratan - yang tidak terjadi pada catur.

Dengan catur, jalur perawatan jangka panjang yang paling mungkin adalah bahwa program Anda perlu menjadi lebih pintar dan lebih cerdas, yang pada akhirnya berarti kecepatan dan optimalisasi penyimpanan akan mendominasi. Untuk itu, Anda umumnya harus melakukan trade off yang mengorbankan keterbacaan untuk kinerja, dan bahkan desain OO terbaik pun pada akhirnya akan berjalan di pinggir jalan.

Erik Eidt
sumber
Perhatikan bahwa ada banyak permainan sukses yang mengambil konsep dan kemudian melemparkan beberapa hal baru ke dalam campuran. Jika Anda mungkin ingin melakukan ini di masa depan dengan permainan catur Anda, Anda sebenarnya harus mempertimbangkan kemungkinan perubahan di masa depan.
Flater
2

Saya akan mulai dengan membuat beberapa pengamatan tentang domain masalah (yaitu aturan catur):

  • Seperangkat gerakan yang sah untuk sebuah keping tidak hanya bergantung pada jenis keping tersebut tetapi juga kondisi papan (pion dapat bergerak maju jika kotaknya kosong / dapat bergerak secara diagonal jika menangkap sebuah keping).
  • Tindakan tertentu melibatkan mengeluarkan bagian yang ada dari permainan (promosi / menangkap bagian) atau memindahkan banyak bagian (kastil).

Ini canggung untuk dimodelkan sebagai tanggung jawab atas karya itu sendiri, dan warisan maupun komposisi tidak terasa hebat di sini. Akan lebih alami untuk membuat model aturan ini sebagai warga negara kelas satu:

// pseudo-code, not pure C++

interface MovementRule {
  set<Square> getValidMoves(Board board, Square from); // what moves can I make from the given square?
  void makeMove(Board board, Square from, Square to); // update board state to reflect a specific move
}

class Game {
  Board board;
  map<PieceType, MovementRule> rules;
}

MovementRuleadalah antarmuka yang sederhana namun fleksibel yang memungkinkan implementasi untuk memperbarui status papan dengan cara apa pun yang diperlukan untuk mendukung gerakan kompleks seperti casting dan promosi. Untuk catur standar, Anda akan memiliki satu implementasi untuk setiap jenis permainan, tetapi Anda juga dapat memasukkan aturan yang berbeda dalam sebuah Gamecontoh untuk mendukung varian catur yang berbeda.

casablanca
sumber
Pendekatan yang menarik - Makanan yang enak untuk dipikirkan
CanISleepYet
1

Saya akan berpikir dalam hal ini pewarisan akan menjadi pilihan yang lebih bersih. Komposisi mungkin sedikit lebih elegan, tetapi bagi saya tampaknya sedikit lebih dipaksakan.

Jika Anda berencana mengembangkan gim lain yang menggunakan gim bergerak yang berperilaku berbeda, komposisi mungkin merupakan pilihan yang lebih baik, terutama jika Anda menggunakan pola pabrik untuk menghasilkan gim yang diperlukan untuk setiap gim.

Kurt Haldeman
sumber
2
Bagaimana Anda akan berurusan dengan promosi menggunakan warisan?
TheCatWhisperer
4
@TheCatWhisperer Bagaimana Anda menanganinya saat Anda bermain fisik? (Semoga Anda mengganti potongan - Anda tidak mengukir kembali bidak, kan !?)
svidgen
0

yang tentu saja mematuhi prinsip "is-a"

Benar, jadi Anda menggunakan warisan. Anda masih bisa mengarang dalam kelas Piece Anda, tetapi setidaknya Anda akan memiliki tulang punggung. Komposisi lebih fleksibel tetapi fleksibilitasnya berlebihan, secara umum, dan tentu saja dalam hal ini karena kemungkinan seseorang memperkenalkan jenis karya baru yang mengharuskan Anda untuk meninggalkan model kaku Anda adalah nol. Permainan catur tidak akan berubah dalam waktu dekat. Warisan memberi Anda model panduan, sesuatu untuk berhubungan juga, mengajarkan kode, arah. Komposisi tidak begitu banyak, entitas domain yang paling penting tidak menonjol, tidak berputar, Anda harus memahami permainan untuk memahami kode.

Martin Maat
sumber
terima kasih telah menambahkan poin bahwa dengan komposisi lebih sulit untuk alasan tentang kode
CanISleepYet
0

Tolong jangan gunakan warisan di sini. Sementara jawaban lain tentu saja memiliki banyak permata kebijaksanaan, menggunakan warisan di sini tentu akan menjadi kesalahan. Menggunakan komposisi tidak hanya membantu Anda menangani masalah yang tidak Anda antisipasi, tetapi saya sudah melihat masalah di sini bahwa warisan tidak dapat menangani dengan baik.

Promosi, ketika gadai dikonversi menjadi bagian yang lebih berharga, bisa jadi masalah pewarisan. Secara teknis, Anda bisa mengatasinya dengan mengganti satu bagian dengan yang lain, tetapi pendekatan ini memiliki keterbatasan. Salah satu batasan ini adalah pelacakan statistik per potong di mana Anda mungkin tidak ingin mengatur ulang informasi bagian pada promosi (atau menulis kode tambahan untuk menyalinnya).

Sekarang, sejauh desain komposisi Anda dalam pertanyaan Anda, saya pikir itu tidak perlu rumit. Saya tidak curiga Anda perlu memiliki komponen terpisah untuk setiap tindakan dan dapat menempel satu kelas per jenis per satuan. Jenis enum mungkin juga tidak dibutuhkan.

Saya akan mendefinisikan Piecekelas (seperti yang sudah Anda miliki), serta PieceTypekelas. The PieceTypekelas mendefinisikan semua metode yang pion, queen, dll harus menentukan, seperti, CanMoveTo, GetPieceTypeName, dll. Kemudian kelas Pawndan Queen, dll akan benar-benar mewarisi dari PieceTypekelas.

TheCatWhisperer
sumber
3
Masalah yang Anda lihat dengan promosi dan penggantian sepele, IMO. Pemisahan keprihatinan cukup banyak mandat bahwa seperti "per piece pelacakan" (atau apa pun) ditangani secara independen pula . ... Setiap potensi keuntungan yang ditawarkan solusi Anda dikalahkan oleh kekhawatiran imajiner yang Anda coba atasi, IMO.
svidgen
5
Untuk mengklarifikasi: Tidak diragukan lagi ada manfaat untuk solusi yang Anda usulkan. Tetapi, mereka sulit ditemukan dalam jawaban Anda, yang berfokus terutama pada masalah yang benar-benar sangat mudah diselesaikan dalam "solusi warisan." ... Itu semacam mengurangi (untuk saya pula) dari apa pun jasa yang Anda coba komunikasikan tentang pendapat Anda.
svidgen
1
Jika Piecetidak virtual, saya setuju dengan solusi ini. Sementara desain kelas mungkin tampak sedikit lebih kompleks di permukaan, saya pikir implementasinya akan lebih sederhana secara keseluruhan. Hal-hal utama yang akan dikaitkan dengan Piecedan bukan PieceTypeidentitas itu dan berpotensi sejarah perpindahannya.
JimmyJames
1
@viden Suatu implementasi yang menggunakan Sepotong terdiri dan implementasi yang menggunakan sepotong berbasis warisan akan terlihat pada dasarnya identik selain dari rincian yang dijelaskan dalam solusi ini. Solusi komposisi ini menambahkan satu tingkat tipuan. Saya tidak melihat itu sebagai sesuatu yang layak dihindari.
JimmyJames
2
@ JimmyJames Benar. Tapi, sejarah perpindahan beberapa bagian perlu dilacak dan dikorelasikan - bukan sesuatu yang harus bertanggung jawab atas satu hal, IMO. Ini adalah "masalah tersendiri" jika Anda suka hal-hal SOLID.
svidgen
0

Dari sudut pandang saya, dengan informasi yang diberikan, tidaklah bijaksana untuk menjawab apakah warisan atau komposisi harus menjadi jalan yang harus ditempuh.

  • Warisan dan komposisi hanyalah alat di dalam sabuk alat desainer berorientasi objek.
  • Anda dapat menggunakan alat-alat ini untuk merancang berbagai model untuk domain masalah.
  • Karena ada banyak model yang dapat mewakili domain, tergantung pada sudut pandang perancang dari persyaratan, Anda dapat menggabungkan warisan dan komposisi dalam berbagai cara untuk menghasilkan model yang setara secara fungsional.

Model Anda benar-benar berbeda, yang pertama Anda modelkan di sekitar konsep bidak catur, di yang kedua di sekitar konsep pergerakan bidak. Mereka mungkin secara fungsional setara, dan saya akan memilih yang mewakili domain dengan lebih baik dan membantu saya bernalar tentang hal itu dengan lebih mudah.

Juga, dalam desain kedua Anda, fakta bahwa Piecekelas Anda memiliki typebidang, dengan jelas mengungkapkan ada masalah desain di sini karena karya itu sendiri dapat terdiri dari beberapa jenis. Bukankah itu terdengar seperti ini mungkin harus menggunakan semacam warisan, di mana karya itu sendiri adalah tipenya?

Jadi, Anda tahu, sangat sulit untuk berdebat tentang solusi Anda. Yang penting bukanlah apakah Anda menggunakan warisan atau komposisi, tetapi apakah model Anda mencerminkan secara akurat domain masalah dan apakah itu berguna untuk bernalar tentang hal itu dan memberikan implementasi dari itu.

Butuh beberapa pengalaman untuk menggunakan pewarisan dan komposisi dengan benar, tetapi mereka adalah alat yang sama sekali berbeda dan saya tidak percaya seseorang dapat "menggantikan" yang satu dengan yang lain, meskipun saya dapat setuju bahwa suatu sistem dapat dirancang sepenuhnya menggunakan komposisi yang adil.

edalorzo
sumber
Anda membuat titik yang baik bahwa itu adalah model yang penting dan bukan alat. Dalam hal jenis yang berlebihan, apakah pewarisan benar-benar membantu? Misalnya jika saya ingin menyorot semua pion atau memeriksa apakah sepotong itu raja, bukankah saya perlu casting?
CanISleepYet
-1

Yah, saya tidak menyarankan keduanya.

Sementara orientasi objek adalah alat yang bagus, dan sering membantu menyederhanakan banyak hal, itu bukan satu-satunya alat di gudang alat.

Pertimbangkan mengimplementasikan kelas papan sebagai gantinya, berisi bytearray sederhana.
Menafsirkannya, dan menghitung hal-hal di dalamnya dilakukan dalam fungsi anggota menggunakan bit-masking dan switching.

Gunakan MSB untuk pemiliknya, sisakan 7 bit untuk bagiannya, meskipun sisakan nol untuk yang kosong.
Atau, nol untuk kosong, tanda untuk pemilik, nilai absolut untuk potongan.

Jika Anda mau, Anda bisa menggunakan bitfields untuk menghindari penyembunyian manual, meskipun itu tidak dapat diport

class field {
    signed char data = 0;
public:
    constexpr field() = default;
    constexpr field(bool black, piece x) noexcept
    : data(x < piece::pawn || x > piece::king ? 0 : (black << 7) | x))
    {}
    constexpr bool is_black() noexcept { return data < 0; }
    constexpr bool is_white() noexcept { return data > 0; }
    constexpr bool empty() noexcept { return data == 0; }
    constexpr piece piece() noexcept { return piece(data & 0x7f); }
};
Deduplicator
sumber
Menarik ... dan tepat mengingat ini adalah pertanyaan C ++. Tapi, bukan menjadi programmer C ++, ini bukan insting pertama (atau kedua atau ketiga ) saya! ... Ini mungkin jawaban C ++ terbanyak di sini!
svidgen
7
Ini adalah optimasi mikro yang tidak boleh diikuti oleh aplikasi nyata.
Lie Ryan
@ LieRyan Jika semua yang Anda miliki adalah OOP, semuanya berbau seperti objek. Dan apakah itu benar-benar "mikro" juga merupakan pertanyaan yang menarik. Tergantung pada apa yang Anda lakukan dengannya, itu mungkin cukup signifikan.
Deduplicator
Heh ... yang mengatakan , C ++ adalah berorientasi objek. Saya menganggap ada alasan OP menggunakan C ++ bukan hanya C .
svidgen
3
Di sini saya kurang khawatir tentang kurangnya OOP, tetapi agak membingungkan kode dengan sedikit memutar-mutar. Kecuali jika Anda menulis kode untuk Nintendo 8-bit atau Anda sedang menulis algoritma yang mengharuskan berurusan dengan jutaan salinan status board, ini adalah optimasi mikro prematur, dan tidak disarankan. Dalam skenario normal, bagaimanapun juga mungkin tidak akan lebih cepat karena akses bit yang tidak selaras membutuhkan operasi bit tambahan.
Lie Ryan