Cara membuat tipe data untuk sesuatu yang mewakili dirinya sendiri atau dua hal lainnya

16

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 Cardhanya akan menggunakan untuk merujuk pada sesuatu yang merupakan kartu biasa, atau setengah dari kartu dua bagian (dengan kata lain, sesuatu dengan hanya satu nama).

kartu-kartu

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). PartialCardmemiliki dua metode tambahan:PartialCard otherPart() dan boolean isFirstPart().

Perwakilan

Jika saya memiliki setumpuk, itu harus terdiri dari WholeCards, bukan Cards, sebagai a Cardbisa 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 Cardakan memiliki metode getRepresentative(). A Representativehampir 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 getRepresentativesebagai return this;.

Adapun PartialCards, mereka tidak mewakili diri mereka sendiri, tetapi mereka memiliki eksternal Representativeyang bukan Card, tetapi menyediakan metode untuk mengakses keduanya PartialCard.

Saya pikir jenis hierarki ini masuk akal, tetapi rumit. Jika kita menganggap Cards sebagai "kartu konseptual" dan Representatives 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 PartialCarddan WholeCardskeduanya 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 PartialCarduntuk mengakses metode tambahan mereka. Saat ini, saya menggunakan sistem yang dijelaskan di sini karena saya benar-benar tidak suka gips eksplisit. Dan seperti Card, Representativeperlu dilemparkan ke salah satu WholeCardatau 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.

pemecah kode
sumber
1
Apakah kartu PartialCard efektif selain dari itu ada dua kartu fisik? Apakah urutan mereka dimainkan? Bisakah Anda tidak hanya membuat "Deck Slot" dan "WholeCard" kelas dan memungkinkan DeckSlots memiliki beberapa WholeCards, dan kemudian hanya melakukan sesuatu seperti DeckSlot.WholeCards.Play dan jika ada 1 atau 2 mainkan keduanya seolah-olah mereka terpisah kartu-kartu? Tampaknya mengingat desain Anda yang jauh lebih rumit pasti ada sesuatu yang saya lewatkan :)
enderland
Saya benar-benar tidak merasa seperti saya dapat menggunakan polimorfisme antara seluruh kartu dan sebagian kartu untuk menyelesaikan masalah ini. Ya, jika saya ingin fungsi "play", maka saya bisa mengimplementasikannya dengan mudah, tetapi masalahnya adalah kartu parsial dan seluruh kartu memiliki set atribut yang berbeda. Saya benar-benar harus dapat melihat objek-objek ini sebagaimana adanya, bukan hanya sebagai kelas dasar mereka. Kecuali ada solusi yang lebih baik untuk masalah itu. Tetapi saya telah menemukan bahwa polimorfisme tidak bercampur dengan baik dengan tipe yang berbagi kelas dasar yang sama, tetapi memiliki metode yang berbeda di antara mereka, jika itu masuk akal.
codebreaker
1
Jujur, saya pikir ini sangat rumit. Anda tampaknya hanya memodelkan hubungan "adalah", yang mengarah ke desain yang sangat kaku dengan kopling ketat. Yang lebih menarik adalah apa yang sebenarnya dilakukan kartu-kartu itu?
Daniel Jour
2
Jika mereka "hanya objek data", maka insting saya adalah memiliki satu kelas yang berisi array objek dari kelas kedua, dan biarkan saja; tidak ada warisan atau komplikasi tidak berguna lainnya. Apakah kedua kelas itu harus PlayableCard dan DrawableCard atau WholeCard dan CardPart saya tidak tahu, karena saya tidak tahu cukup banyak tentang bagaimana permainan Anda bekerja, tetapi saya yakin Anda dapat memikirkan sesuatu untuk memanggil mereka.
Ixrec
1
Properti apa yang mereka miliki? Bisakah Anda memberikan contoh tindakan yang menggunakan properti ini?
Daniel Jour

Jawaban:

14

Sepertinya saya bahwa Anda harus memiliki kelas seperti

class PhysicalCard {
    List<LogicalCard> getLogicalCards();
}

Kode yang berkaitan dengan kartu fisik dapat menangani kelas kartu fisik, dan kode yang berkaitan dengan kartu logis dapat menangani itu.

Saya pikir Anda dapat membuat argumen bahwa kartu fisik memang mengandung kartu konseptual, dan bahwa mereka bukan hal yang sama, tetapi saya berpendapat bahwa itu adalah kartu yang sama.

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.

Winston Ewert
sumber
2
Terpilih karena ini adalah jawaban terbaik, meskipun bukan karena alasan yang diberikan. Ini adalah jawaban terbaik bukan karena itu sederhana, tetapi karena kartu fisik dan kartu konseptual bukan hal yang sama, dan solusi ini memodelkan hubungan mereka dengan benar. Memang benar bahwa model OOP yang baik tidak selalu mencerminkan realitas fisik, tetapi harus selalu mencerminkan realitas konseptual. Kesederhanaan itu baik, tentu saja, tetapi dibutuhkan kursi belakang untuk kebenaran konseptual.
Kevin Krumwiede
2
@KevinKrumwiede, seperti yang saya lihat tidak ada realitas konseptual tunggal. Orang yang berbeda memikirkan hal yang sama dengan cara yang berbeda, dan orang dapat mengubah cara mereka memikirkannya. Anda dapat menganggap kartu fisik dan logis sebagai entitas yang terpisah. Atau Anda dapat menganggap kartu terbagi sebagai semacam pengecualian terhadap gagasan umum kartu. Baik secara intrinsik tidak benar, tetapi yang cocok untuk pemodelan sederhana.
Winston Ewert
8

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 Cardentitas Anda untuk mengandung N dari setiap atribut: nama yang dapat dimainkan, biaya mana, jenis, kelangkaan, teks, efek, dll.

interface Card {
  List<String> Names();
  List<ManaCost> Costs();
  List<CardTypes> Types();
  /* etc. */
}

Opsi 2. Tidak jauh berbeda dari Opsi 1, modelkan setelah realitas fisik. Anda memiliki Cardentitas yang mewakili kartu fisik . Dan, tujuannya adalah untuk memegang N Playable hal. Mereka Playablemasing-masing dapat memiliki nama yang berbeda, biaya mana, daftar efek, daftar kemampuan, dll. Dan "fisik" Anda Carddapat memiliki pengidentifikasi sendiri (atau nama) yang merupakan gabungan dari Playablenama masing-masing , seperti database MTG tampaknya dilakukan.

interface Card {
  String Name();
  List<Playable> Playables();
}

interface Playable {
  String Name();
  ManaCost Cost();
  CardType Type();
  /* etc. */
}

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

svidgen
sumber
5

Tujuan dari objek-objek ini adalah benar-benar hanya untuk menyimpan properti kartu. Mereka tidak benar-benar melakukan apa pun sendiri.

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.

Bagaimanapun, mereka adalah kartu yang mewakili diri mereka sendiri!

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:

  1. Buat Cardantarmuka, seperti yang sudah Anda lakukan.
  2. Buat ConcreteCard, yang mengimplementasikan Carddan mendefinisikan kartu wajah sederhana. Jangan ragu untuk menunjukkan perilaku kartu normal di kelas ini.
  3. Buat CompositeCard, yang mengimplementasikan Carddan memiliki dua tambahan (dan priori pribadi) Card. Mari kita memanggil mereka leftCarddan rightCard.

Keanggunan pendekatannya adalah bahwa a CompositeCardberisi dua kartu, yang dapat berupa Kartu Beton atau Kartu Komposit. Dalam gim Anda, leftCarddan rightCardmungkin secara sistematis akan ada ConcreteCard, 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.

CompositeCardharus menerapkan metode yang ditentukan dalam Card, tentu saja, dan akan melakukannya dengan memperhitungkan fakta bahwa kartu tersebut terbuat dari 2 kartu (ditambah, jika Anda ingin, sesuatu yang spesifik untuk CompositeCardkartu itu sendiri. Misalnya, Anda mungkin menginginkan implementasi berikut:

public class CompositeCard implements Card
{ 
   private final Card leftCard, rightCard;
   private final double factor;

   @Override // Defined in Card
   public double attack(Player p){
      return factor * (leftCard.attack(p) + rightCard.attack(p));
   }

   @Override // idem
   public String name()
   {
       return leftCard.name() + " combined with " + rightCard.name();
   }

   ...
}

Dengan melakukan itu, Anda dapat menggunakan CompositeCardpersis seperti yang Anda lakukan untuk apa pun Card, dan perilaku spesifik disembunyikan berkat polimorfisme.

Jika Anda yakin a CompositeCardakan selalu mengandung dua Cards normal , Anda dapat menyimpan idenya, dan cukup gunakan ConcreateCardsebagai tipe untuk leftCarddan rightCard.

mgoeminne
sumber
Anda berbicara tentang pola komposit , tetapi sebenarnya karena Anda memegang dua referensi Carddi CompositeCardAnda menerapkan pola dekorator . Saya juga merekomendasikan kepada OP untuk menggunakan solusi ini, dekorator adalah cara untuk pergi!
Terlihat
Saya tidak mengerti mengapa memiliki dua instance Kartu menjadikan kelas saya sebagai dekorator. Menurut tautan Anda sendiri, dekorator menambahkan fitur ke objek tunggal, dan itu sendiri merupakan instance dari kelas / antarmuka yang sama dari objek ini. Sementara, menurut tautan Anda yang lain, komposit memungkinkan memiliki beberapa objek dari kelas / antarmuka yang sama. Tetapi pada akhirnya kata-kata itu tidak penting, dan hanya idenya yang hebat.
mgoeminne
@ Spotted Ini jelas bukan pola dekorator seperti istilah yang digunakan di Jawa. Pola dekorator diimplementasikan dengan mengganti metode pada objek individual, membuat kelas anonim yang unik untuk objek itu.
Kevin Krumwiede
@KevinKrumwiede selama CompositeCardtidak mengekspos metode tambahan, CompositeCardhanya dekorator.
Terlihat
"... Pola Desain memungkinkan Anda untuk merancang komposisi tingkat yang lebih tinggi secara gratis" - tidak, itu tidak datang secara gratis , justru sebaliknya - ia datang dengan harga memiliki solusi yang lebih rumit daripada yang dibutuhkan untuk persyaratan.
Doc Brown
3

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.

Davislor
sumber
Sayangnya, saya tidak memainkan kartu-kartu ini. Mereka benar-benar hanya objek data yang dapat ditanyakan. Jika Anda ingin struktur data dek, Anda ingin Daftar <WholeCard> atau sesuatu. Jika Anda ingin melakukan pencarian untuk kartu hijau Instan, Anda ingin Daftar <Card>.
codebreaker
Ah, baiklah. Tidak terlalu relevan dengan masalah Anda, kalau begitu. Haruskah saya hapus?
Davislor
3

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 Cardantarmuka:

public interface Card {
    public void accept(CardVisitor visitor);
}

Mungkin ada perilaku sedikit lebih pada Cardantarmuka, tetapi sebagian besar getter properti pindah ke kelas baru, CardProperties:

public class CardProperties {
    // property methods, constructors, etc.

    String name();
    String text();
    // ...
}

Sekarang kita dapat memiliki kartu yang SimpleCardmewakili seluruh dengan satu set properti:

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

    @Override
    public void accept(CardVisitor visitor) {
        visitor.visit(properties);
    }
}

Kita melihat bagaimana CardPropertiesdan yang belum ditulis CardVisitormulai cocok. Mari kita lakukan CompoundCarduntuk mewakili kartu dengan dua wajah:

public class CompoundCard implements Card {
    private CardProperties firstFaceProperties;
    private CardProperties secondFaceProperties;

    // Constructors, ...

    public void accept(CardVisitor visitor) {
        visitor.visit(firstFaceProperties, secondFaceProperties);
    }
}

The CardVisitormulai muncul. Mari kita coba menulis antarmuka itu sekarang:

public interface CardVisitor {
    public void visit(CardProperties properties);
    public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties);
}

(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:

List<Card> cards = new LinkedList<>();
cards.add(new SimpleCard(new CardProperties(/* ... */)));
cards.add(new CompoundCard(new CardProperties(/* ... */), new CardProperties(/* ... */)));

 for(Card card : cards) {
     card.accept(new CardVisitor() {
         @Override
         public void visit(CardProperties properties) {
             // Do something for simple cards with a single face
         }

         public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties) {
             // Do something else for compound cards with two faces
         }
     });
 }

Runtime akan menangani pengiriman ke versi #visitmetode yang benar melalui polimorfisme alih-alih mencoba memecahkannya.

Daripada menggunakan kelas anonim, Anda bahkan dapat mempromosikan CardVisitorke 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 CardVisitorantarmuka. Sebagai contoh, mungkin ada saatnya ketika Cards 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 CardVisitorke 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-tunggal Card). Kami juga dapat menambahkan metode baru tanpa memaksa setiap kelas yang ada untuk mengimplementasikan metode tersebut atau gagal untuk mengkompilasi.

cbojar
sumber
1
Saya pikir ini dapat dengan mudah diperluas ke jenis lain dari dua kartu yang dihadapi (depan & belakang, bukan berdampingan). ++
RubberDuck
Untuk eksplorasi lebih lanjut, penggunaan Pengunjung untuk mengeksploitasi metode khusus untuk subkelas kadang-kadang disebut Pengiriman Ganda. Double Dispatch bisa menjadi solusi yang menarik untuk masalah ini.
mgoeminne
1
Saya memilih turun karena saya pikir pola pengunjung menambah kerumitan yang tidak perlu dan menghubungkan ke kode. Karena alternatif tersedia (lihat jawaban mgoeminne), saya tidak akan menggunakannya.
Terlihat
@Spotted Pengunjung adalah pola yang kompleks, dan saya juga mempertimbangkan pola gabungan saat menulis jawabannya. Alasan saya pergi bersama pengunjung adalah karena OP ingin memperlakukan hal yang sama secara berbeda, bukan hal yang sama. Jika kartu memiliki lebih banyak perilaku, dan digunakan untuk bermain game, maka pola gabungan memungkinkan untuk menggabungkan data untuk menghasilkan stat terpadu. Mereka hanya sekumpulan data, meskipun, mungkin digunakan membuat tampilan kartu, dalam hal mendapatkan informasi yang terpisah tampak lebih berguna daripada agregator komposit sederhana.
cbojar