Latar Belakang
Inilah masalah aktual yang sedang saya kerjakan: Saya ingin cara untuk mewakili kartu dalam permainan kartu Magic: The Gathering . Sebagian besar kartu dalam permainan adalah kartu yang tampak normal, tetapi beberapa di antaranya dibagi menjadi dua bagian, masing-masing dengan namanya sendiri. Setiap setengah dari kartu dua bagian ini diperlakukan sebagai kartu itu sendiri. Jadi untuk kejelasan, saya Card
hanya akan menggunakan untuk merujuk pada sesuatu yang merupakan kartu biasa, atau setengah dari kartu dua bagian (dengan kata lain, sesuatu dengan hanya satu nama).
Jadi kami memiliki tipe dasar, Kartu. Tujuan dari objek-objek ini adalah benar-benar hanya untuk menyimpan properti kartu. Mereka tidak benar-benar melakukan apa pun sendiri.
interface Card {
String name();
String text();
// etc
}
Ada dua subclass Card
, yang saya panggil PartialCard
(setengah dari kartu dua bagian) dan WholeCard
(kartu biasa). PartialCard
memiliki dua metode tambahan:PartialCard otherPart()
dan boolean isFirstPart()
.
Perwakilan
Jika saya memiliki setumpuk, itu harus terdiri dari WholeCard
s, bukan Card
s, sebagai a Card
bisa menjadi PartialCard
, dan itu tidak masuk akal. Jadi saya ingin objek yang mewakili "kartu fisik," yaitu, sesuatu yang dapat mewakili satu WholeCard
, atau dua PartialCard
. Untuk sementara saya memanggil tipe ini Representative
, dan Card
akan memiliki metode getRepresentative()
. A Representative
hampir tidak akan memberikan informasi langsung pada kartu yang diwakilinya, itu hanya akan menunjukkannya kepada mereka. Sekarang, ide brilian / gila / bodoh saya (Anda memutuskan) adalah WholeCard mewarisi dari keduanya Card
dan Representative
. Bagaimanapun, mereka adalah kartu yang mewakili diri mereka sendiri! WholeCards dapat diimplementasikan getRepresentative
sebagai return this;
.
Adapun PartialCards
, mereka tidak mewakili diri mereka sendiri, tetapi mereka memiliki eksternal Representative
yang bukan Card
, tetapi menyediakan metode untuk mengakses keduanya PartialCard
.
Saya pikir jenis hierarki ini masuk akal, tetapi rumit. Jika kita menganggap Card
s sebagai "kartu konseptual" dan Representative
s sebagai "kartu fisik," well, kebanyakan kartu keduanya! Saya pikir Anda dapat membuat argumen bahwa kartu fisik memang mengandung kartu konseptual, dan bahwa itu bukan hal yang sama , tetapi saya berpendapat bahwa itu memang kartu yang sama .
Kebutuhan Jenis-casting
Karena keduanya PartialCard
dan WholeCards
keduanya Card
, dan biasanya tidak ada alasan yang baik untuk memisahkan mereka, saya biasanya hanya bekerja dengan Collection<Card>
. Jadi kadang-kadang saya perlu melakukan casting PartialCard
untuk mengakses metode tambahan mereka. Saat ini, saya menggunakan sistem yang dijelaskan di sini karena saya benar-benar tidak suka gips eksplisit. Dan seperti Card
, Representative
perlu dilemparkan ke salah satu WholeCard
atau Composite
, untuk mengakses yang sebenarnyaCard
mereka wakili.
Jadi hanya untuk ringkasan:
- Jenis dasar
Representative
- Jenis dasar
Card
- Tipe
WholeCard extends Card, Representative
(tidak perlu akses, itu mewakili dirinya sendiri) - Tipe
PartialCard extends Card
(memberikan akses ke bagian lain) - Ketik
Composite extends Representative
(memberikan akses ke kedua bagian)
Apakah ini gila? Saya pikir itu benar-benar masuk akal, tapi sejujurnya saya tidak yakin.
sumber
Jawaban:
Sepertinya saya bahwa Anda harus memiliki kelas seperti
Kode yang berkaitan dengan kartu fisik dapat menangani kelas kartu fisik, dan kode yang berkaitan dengan kartu logis dapat menangani itu.
Tidak masalah apakah Anda berpikir kartu fisik dan logika itu sama. Jangan berasumsi bahwa hanya karena mereka adalah objek fisik yang sama, mereka harus menjadi objek yang sama dalam kode Anda. Yang penting adalah apakah mengadopsi model itu membuat pembuat kode lebih mudah untuk membaca dan menulis. Faktanya adalah, mengadopsi model yang lebih sederhana di mana setiap kartu fisik diperlakukan sebagai kumpulan kartu logis secara konsisten, 100% dari waktu, akan menghasilkan kode yang lebih sederhana.
sumber
Terus terang, saya pikir solusi yang diusulkan terlalu ketat dan terlalu berkerut dan terputus-putus dari realitas fisik adalah model, dengan sedikit keuntungan.
Saya menyarankan satu dari dua alternatif:
Opsi 1. Perlakukan sebagai kartu tunggal, diidentifikasi sebagai Half A // Half B , seperti situs MTG mencantumkan Wear // Tear . Tetapi, izinkan
Card
entitas Anda untuk mengandung N dari setiap atribut: nama yang dapat dimainkan, biaya mana, jenis, kelangkaan, teks, efek, dll.Opsi 2. Tidak jauh berbeda dari Opsi 1, modelkan setelah realitas fisik. Anda memiliki
Card
entitas yang mewakili kartu fisik . Dan, tujuannya adalah untuk memegang NPlayable
hal. MerekaPlayable
masing-masing dapat memiliki nama yang berbeda, biaya mana, daftar efek, daftar kemampuan, dll. Dan "fisik" AndaCard
dapat memiliki pengidentifikasi sendiri (atau nama) yang merupakan gabungan dariPlayable
nama masing-masing , seperti database MTG tampaknya dilakukan.Saya pikir salah satu dari opsi ini cukup dekat dengan realitas fisik. Dan, saya pikir itu akan bermanfaat bagi siapa saja yang melihat kode Anda. (Seperti dirimu sendiri dalam 6 bulan.)
sumber
Kalimat ini adalah tanda bahwa ada sesuatu yang salah dalam desain Anda: dalam OOP, setiap kelas harus memiliki tepat satu peran, dan kurangnya perilaku mengungkapkan Kelas Data potensial , yang merupakan bau busuk dalam kode.
IMHO, kedengarannya agak aneh, dan bahkan agak aneh. Objek jenis "Kartu" harus mewakili kartu. Titik.
Saya tidak tahu apa-apa tentang Magic: Pertemuan itu , tetapi saya kira Anda ingin menggunakan kartu Anda dengan cara yang sama, apa pun struktur aktualnya: Anda ingin menampilkan representasi string, Anda ingin menghitung nilai serangan, dll.
Untuk masalah yang Anda jelaskan, saya akan merekomendasikan Pola Desain Komposit , meskipun DP ini biasanya disajikan untuk menyelesaikan masalah yang lebih umum:
Card
antarmuka, seperti yang sudah Anda lakukan.ConcreteCard
, yang mengimplementasikanCard
dan mendefinisikan kartu wajah sederhana. Jangan ragu untuk menunjukkan perilaku kartu normal di kelas ini.CompositeCard
, yang mengimplementasikanCard
dan memiliki dua tambahan (dan priori pribadi)Card
. Mari kita memanggil merekaleftCard
danrightCard
.Keanggunan pendekatannya adalah bahwa a
CompositeCard
berisi dua kartu, yang dapat berupa Kartu Beton atau Kartu Komposit. Dalam gim Anda,leftCard
danrightCard
mungkin secara sistematis akan adaConcreteCard
, tetapi Pola Desain memungkinkan Anda untuk merancang komposisi tingkat yang lebih tinggi secara gratis jika Anda mau. Manipulasi kartu Anda tidak akan mempertimbangkan jenis kartu Anda yang sebenarnya, dan karena itu Anda tidak perlu hal-hal seperti casting ke subclass.CompositeCard
harus menerapkan metode yang ditentukan dalamCard
, tentu saja, dan akan melakukannya dengan memperhitungkan fakta bahwa kartu tersebut terbuat dari 2 kartu (ditambah, jika Anda ingin, sesuatu yang spesifik untukCompositeCard
kartu itu sendiri. Misalnya, Anda mungkin menginginkan implementasi berikut:Dengan melakukan itu, Anda dapat menggunakan
CompositeCard
persis seperti yang Anda lakukan untuk apa punCard
, dan perilaku spesifik disembunyikan berkat polimorfisme.Jika Anda yakin a
CompositeCard
akan selalu mengandung duaCard
s normal , Anda dapat menyimpan idenya, dan cukup gunakanConcreateCard
sebagai tipe untukleftCard
danrightCard
.sumber
Card
diCompositeCard
Anda menerapkan pola dekorator . Saya juga merekomendasikan kepada OP untuk menggunakan solusi ini, dekorator adalah cara untuk pergi!CompositeCard
tidak mengekspos metode tambahan,CompositeCard
hanya dekorator.Mungkin semuanya adalah Kartu saat berada di geladak atau kuburan, dan ketika Anda memainkannya, Anda membangun Makhluk, Tanah, Pesona, dll. Dari satu atau lebih objek Kartu, yang semuanya menerapkan atau memperluas Playable. Kemudian, komposit menjadi Playable tunggal yang konstruktornya mengambil dua Kartu parsial, dan kartu dengan kicker menjadi Playable yang konstruktornya mengambil argumen mana. Jenis ini mencerminkan apa yang dapat Anda lakukan dengannya (menggambar, memblokir, menghilangkan, mengetuk) dan apa yang dapat mempengaruhinya. Atau Playable hanyalah Kartu yang harus dikembalikan dengan hati-hati (kehilangan bonus dan penghitung, terbelah) ketika dikeluarkan dari permainan, jika itu benar-benar berguna untuk menggunakan antarmuka yang sama untuk memanggil kartu dan memprediksi apa yang dilakukannya.
Mungkin Card dan Playable memiliki efek.
sumber
The pola pengunjung adalah teknik klasik untuk memulihkan informasi jenis tersembunyi. Kita dapat menggunakannya (sedikit variasi di sini) di sini untuk membedakan antara kedua jenis bahkan ketika mereka disimpan dalam variabel abstraksi yang lebih tinggi.
Mari kita mulai dengan abstraksi yang lebih tinggi, sebuah
Card
antarmuka:Mungkin ada perilaku sedikit lebih pada
Card
antarmuka, tetapi sebagian besar getter properti pindah ke kelas baru,CardProperties
:Sekarang kita dapat memiliki kartu yang
SimpleCard
mewakili seluruh dengan satu set properti:Kita melihat bagaimana
CardProperties
dan yang belum ditulisCardVisitor
mulai cocok. Mari kita lakukanCompoundCard
untuk mewakili kartu dengan dua wajah:The
CardVisitor
mulai muncul. Mari kita coba menulis antarmuka itu sekarang:(Ini adalah versi antarmuka pertama untuk saat ini. Kita dapat melakukan peningkatan, yang akan dibahas nanti.)
Kami sekarang telah menyempurnakan semua bagian. Kita sekarang hanya perlu menyatukannya:
Runtime akan menangani pengiriman ke versi
#visit
metode yang benar melalui polimorfisme alih-alih mencoba memecahkannya.Daripada menggunakan kelas anonim, Anda bahkan dapat mempromosikan
CardVisitor
ke kelas dalam atau bahkan kelas penuh jika perilaku dapat digunakan kembali atau jika Anda ingin kemampuan untuk bertukar perilaku saat runtime.Kita dapat menggunakan kelas seperti sekarang, tetapi ada beberapa perbaikan yang bisa kita lakukan untuk
CardVisitor
antarmuka. Sebagai contoh, mungkin ada saatnya ketikaCard
s dapat memiliki tiga atau empat atau lima wajah. Daripada menambahkan metode baru untuk diimplementasikan, kita bisa meminta metode kedua mengambil dan mengatur bukan dua parameter. Ini masuk akal jika kartu multi-wajah diperlakukan berbeda, tetapi jumlah wajah di atas semua diperlakukan sama.Kami juga dapat mengonversi
CardVisitor
ke kelas abstrak alih-alih antarmuka, dan memiliki implementasi kosong untuk semua metode. Ini memungkinkan kita untuk menerapkan hanya perilaku yang kita minati (mungkin kita hanya tertarik pada wajah-tunggalCard
). Kami juga dapat menambahkan metode baru tanpa memaksa setiap kelas yang ada untuk mengimplementasikan metode tersebut atau gagal untuk mengkompilasi.sumber