Apa cara yang tepat untuk memodelkan aktivitas dunia nyata ini yang tampaknya membutuhkan referensi melingkar dalam OOP?

24

Saya telah bergulat dengan masalah dalam proyek Java tentang referensi melingkar. Saya mencoba memodelkan situasi dunia nyata di mana benda-benda tersebut saling terkait dan perlu saling mengetahui.

Proyek ini adalah model generik dari bermain permainan papan. Kelas dasar tidak spesifik, tetapi diperluas untuk menangani spesifik catur, backgammon dan permainan lainnya. Saya mengkodekan ini sebagai applet 11 tahun yang lalu dengan setengah lusin game yang berbeda, tetapi masalahnya adalah itu penuh dengan referensi melingkar. Saya menerapkannya saat itu dengan menjejalkan semua kelas yang saling terkait dalam satu file sumber, tapi saya mendapatkan ide bahwa itu bentuk yang buruk di Jawa. Sekarang saya ingin menerapkan hal serupa sebagai aplikasi Android, dan saya ingin melakukan hal-hal dengan benar.

Kelasnya adalah:

  • RuleBook: objek yang dapat diinterogasi untuk hal-hal seperti tata letak awal Dewan, informasi awal permainan Negara lainnya seperti siapa yang bergerak pertama, Bergerak yang tersedia, apa yang terjadi pada keadaan permainan setelah Pindah yang diusulkan, dan evaluasi dari posisi dewan saat ini atau yang diusulkan.

  • Papan: representasi sederhana papan permainan, yang dapat diinstruksikan untuk mencerminkan Gerakan.

  • MoveList: daftar Bergerak. Ini adalah tujuan ganda: pilihan langkah yang tersedia pada titik tertentu, atau daftar langkah yang telah dibuat dalam permainan. Ini dapat dibagi menjadi dua kelas yang hampir identik, tetapi itu tidak relevan dengan pertanyaan yang saya ajukan dan dapat memperumitnya.

  • Pindah: satu gerakan tunggal. Ini mencakup segala sesuatu tentang gerakan sebagai daftar atom: ambil sepotong dari sini, letakkan di sana, lepaskan potongan yang diambil dari sana.

  • Status: informasi status lengkap dari game yang sedang berlangsung. Tidak hanya posisi Dewan, tetapi MoveList, dan informasi status lainnya seperti siapa yang akan pindah sekarang. Dalam catur kita akan mencatat apakah raja dan benteng masing-masing pemain telah dipindahkan.

Referensi sirkular ada banyak, misalnya: RuleBook perlu tahu tentang status permainan untuk menentukan gerakan apa yang tersedia pada waktu tertentu, tetapi negara permainan perlu menanyakan RuleBook untuk tata letak awal awal dan untuk efek samping apa yang menyertai gerakan sekali itu dibuat (misalnya siapa yang bergerak selanjutnya).

Saya mencoba mengatur kumpulan kelas baru secara hierarkis, dengan RuleBook di atas karena perlu tahu tentang segalanya. Tetapi ini berakibat harus memindahkan banyak metode ke kelas RuleBook (seperti membuat langkah) menjadikannya monolitik dan tidak terlalu mewakili apa yang seharusnya menjadi RuleBook.

Jadi apa cara yang tepat untuk mengatur ini? Haruskah saya mengubah RuleBook menjadi BigClassThatDoesAlmostEverythingInTheGame untuk menghindari referensi melingkar, mengabaikan upaya untuk memodelkan game dunia nyata secara akurat? Atau haruskah saya tetap dengan kelas yang saling tergantung dan membujuk kompiler untuk mengkompilasinya, mempertahankan model dunia nyata saya? Atau ada beberapa struktur valid yang jelas saya hilang?

Terima kasih atas bantuan yang bisa Anda berikan!

Damian Walker
sumber
7
Bagaimana jika RuleBookmengambil mis Statesebagai argumen, dan mengembalikan yang valid MoveList, yaitu "di sinilah kita sekarang, apa yang bisa dilakukan selanjutnya?"
jonrsharpe
Apa yang dikatakan @jonrsharpe. Saat memainkan game papan nyata, buku peraturan juga tidak tahu tentang game sebenarnya yang sedang dimainkan. Saya bahkan mungkin akan memperkenalkan kelas lain untuk benar-benar menghitung gerakan, tapi itu mungkin tergantung pada seberapa besar kelas RuleBook ini.
Sebastiaan van den Broek
4
Menghindari objek dewa (BigClassThatDoesAlmostEverythingInTheGame) jauh lebih penting daripada menghindari referensi melingkar.
user281377
2
@ user281377 mereka tidak selalu merupakan tujuan yang saling eksklusif!
jonrsharpe
1
Bisakah Anda menunjukkan upaya pemodelan? Diagram misalnya?
Pengguna

Jawaban:

47

Saya telah bergulat dengan masalah dalam proyek Java tentang referensi melingkar.

Pengumpul sampah Jawa tidak mengandalkan teknik penghitungan referensi. Referensi lingkaran tidak menyebabkan masalah apa pun di Jawa. Waktu yang dihabiskan untuk menghilangkan referensi melingkar yang sempurna di Jawa adalah waktu yang terbuang.

Saya mengkodekan ini [...] tetapi masalahnya adalah penuh referensi melingkar. Saya menerapkannya saat itu dengan memasukkan semua kelas yang saling terkait dalam satu file sumber , [...]

Tidak perlu. Jika Anda hanya mengkompilasi semua file sumber sekaligus (misalnya, javac *.java), kompiler akan menyelesaikan semua referensi maju tanpa masalah.

Atau haruskah saya tetap dengan kelas yang saling tergantung dan membujuk kompiler untuk mengkompilasinya, [...]

Iya nih. Kelas aplikasi diharapkan saling tergantung. Mengkompilasi semua file sumber Java yang termasuk dalam paket yang sama sekaligus bukanlah hack yang cerdik, justru cara Java seharusnya bekerja.

Atsby
sumber
24
"Referensi melingkar tidak menyebabkan masalah apa pun di Jawa." Dalam hal kompilasi, ini benar. Referensi melingkar dianggap desain yang buruk .
Potong
22
Referensi melingkar sangat alami dalam banyak situasi, itu sebabnya Jawa dan bahasa modern lainnya menggunakan pengumpul sampah yang canggih alih-alih penghitung referensi sederhana.
user281377
3
Java dapat menyelesaikan referensi melingkar sangat bagus, dan memang benar bahwa mereka alami dalam banyak situasi. Tetapi OP menyajikan situasi tertentu , dan itu harus dipertimbangkan. Kode spageti kusut mungkin bukan cara terbaik untuk menangani masalah ini.
Matius Baca
3
Tolong jangan menyebar FUD tidak berdasar tentang bahasa pemrograman yang tidak terkait. Python telah mendukung GC siklus referensi sejak lama ( dokumen , juga pada SO: di sini dan di sini ).
Christian Aichinger
2
IMHO jawaban ini hanya biasa-biasa saja, karena tidak ada satu kata pun tentang referensi melingkar yang berguna misalnya untuk OP.
Doc Brown
22

Memang, dependensi melingkar adalah praktik yang dipertanyakan dari sudut pandang desain, tetapi mereka tidak dilarang, dan dari sudut pandang murni teknis mereka bahkan tidak selalu bermasalah , karena Anda tampaknya menganggapnya sebagai: mereka sangat legal dalam kebanyakan skenario, mereka tak terhindarkan dalam beberapa situasi, dan pada beberapa kesempatan langka mereka bahkan dapat dianggap sebagai hal yang berguna untuk dimiliki.

Sebenarnya, ada beberapa skenario di mana kompiler java akan menolak ketergantungan melingkar. (Catatan: mungkin ada lebih banyak, saya hanya bisa memikirkan yang berikut ini sekarang.)

  1. Dalam warisan: Anda tidak dapat memiliki kelas A memperluas kelas B yang pada gilirannya memperluas kelas A, dan sangat masuk akal bahwa Anda tidak dapat memiliki ini, karena alternatifnya sama sekali tidak masuk akal dari sudut pandang logis.

  2. Di antara kelas-kelas metode-lokal: kelas-kelas yang dideklarasikan dalam suatu metode mungkin tidak saling referensi satu sama lain. Ini mungkin hanyalah batasan dari kompiler java, mungkin karena kemampuan untuk melakukan hal seperti itu tidak cukup berguna untuk membenarkan kerumitan tambahan yang harus masuk ke kompiler untuk mendukungnya. (Kebanyakan programmer java bahkan tidak menyadari fakta bahwa Anda dapat mendeklarasikan kelas dalam suatu metode, apalagi mendeklarasikan beberapa kelas, dan kemudian membuat kelas-kelas ini saling referensi satu sama lain.)

Jadi, penting untuk menyadari dan mengeluarkannya dari jalan bahwa upaya untuk meminimalkan dependensi melingkar adalah pencarian kemurnian desain, bukan pencarian kebenaran teknis.

Sejauh yang saya tahu tidak ada pendekatan reduksionis untuk menghilangkan ketergantungan sirkuler, yang berarti bahwa tidak ada resep yang terdiri dari langkah-langkah sederhana yang telah ditentukan sebelumnya "tanpa-otak" untuk mengambil sebuah sistem dengan referensi melingkar, menerapkannya satu demi satu, dan mengakhiri dengan sistem yang bebas dari referensi melingkar. Anda harus meletakkan pikiran Anda untuk bekerja, dan Anda harus melakukan langkah-langkah refactoring yang tergantung pada sifat desain Anda.

Dalam situasi tertentu yang Anda miliki, menurut saya apa yang Anda butuhkan adalah entitas baru, mungkin disebut "Game" atau "GameLogic", yang mengetahui semua entitas lain, (tanpa ada entitas lain yang mengetahuinya, ) sehingga entitas lain tidak harus saling mengenal.

Misalnya, bagi saya tampaknya tidak masuk akal bahwa entitas RuleBook Anda perlu mengetahui apa pun tentang entitas GameState, karena buku aturan adalah sesuatu yang kami konsultasikan untuk bermain, itu bukan sesuatu yang berperan aktif dalam bermain. Jadi, entitas "Game" baru ini yang perlu berkonsultasi dengan buku peraturan dan status permainan untuk menentukan gerakan apa yang tersedia, dan ini menghilangkan dependensi melingkar.

Sekarang, saya pikir saya bisa menebak apa masalah Anda dengan pendekatan ini: mengkodekan entitas "Game" dengan cara agnostik game akan sangat sulit, jadi Anda kemungkinan besar akan berakhir dengan tidak hanya satu tetapi dua entitas yang perlu memiliki implementasi yang dibuat khusus untuk setiap jenis game yang berbeda: entitas "RuleBook" dan "Game". Yang pada gilirannya mengalahkan tujuan memiliki entitas "RuleBook" di tempat pertama. Yah, yang bisa saya katakan tentang ini adalah bahwa mungkin, mungkin saja, aspirasi awal Anda untuk menulis sebuah sistem yang dapat memainkan berbagai jenis permainan mungkin mulia, tetapi mungkin dikandung dengan buruk. Jika saya berada di posisi Anda, saya akan fokus menggunakan mekanisme umum untuk menampilkan keadaan semua game yang berbeda, dan mekanisme umum untuk menerima input pengguna untuk semua game ini,

Mike Nakis
sumber
1
Terima kasih Mike. Anda benar tentang kelemahan entitas Game; dengan kode applet lama saya bisa membuat game baru dengan sedikit lebih dari subclass RuleBook baru dan desain grafis yang sesuai.
Damian Walker
10

Teori permainan memperlakukan game sebagai daftar gerakan sebelumnya (tipe nilai termasuk siapa yang memainkannya) dan fungsi ValidMoves (priorMoves)

Saya akan mencoba dan mengikuti pola ini untuk bagian non UI dari permainan dan memperlakukan hal-hal seperti pengaturan papan sebagai gerakan.

UI kemudian dapat menjadi hal-hal OO standar dengan satu arah referensi ke logika


Perbarui untuk menyingkat komentar

Pertimbangkan Catur. Permainan catur biasanya dicatat sebagai daftar gerakan. http://en.wikipedia.org/wiki/Portable_Game_Notation

daftar langkah mendefinisikan kondisi lengkap permainan jauh lebih baik daripada gambar papan.

Katakan misalnya kita mulai membuat objek untuk Papan, Potongan, Pindahkan dll dan Metode seperti Piece.GetValidMoves ()

pertama-tama kita melihat kita harus memiliki referensi papan tulis, tapi kemudian kita mempertimbangkan castling. yang hanya dapat Anda lakukan jika Anda belum memindahkan raja atau benteng Anda. Jadi kita perlu bendera Pindah Sudah pada raja dan benteng. Demikian pula pion dapat memindahkan 2 kotak pada langkah pertama mereka.

Kemudian kita melihat bahwa dalam menjalankan langkah raja yang sah tergantung pada keberadaan dan keadaan benteng, sehingga papan perlu memiliki potongan-potongan di atasnya dan referensi potongan-potongan itu. kami masuk ke masalah ref melingkar Anda.

Namun, jika kita mendefinisikan Pindahkan sebagai status permainan dan struktur yang tidak dapat diubah sebagai daftar langkah sebelumnya, kami menemukan masalah ini hilang. Untuk melihat apakah kastil itu valid, kita bisa memeriksa daftar langkah keberadaan kastil dan langkah raja. Untuk melihat apakah pion dapat digunakan secara bersamaan, kita dapat memeriksa untuk melihat apakah pion lain membuat gerakan ganda pada langkah sebelumnya. Tidak diperlukan referensi kecuali Aturan -> Pindahkan

Sekarang catur memiliki papan statis, dan peices selalu diatur dengan cara yang sama. Tetapi katakanlah kita memiliki varian di mana kita mengizinkan pengaturan alternatif. mungkin menghilangkan beberapa bagian sebagai cacat.

Jika kita Tambahkan gerakan pengaturan sebagai gerakan, 'dari kotak ke kotak X' dan menyesuaikan objek Aturan untuk memahami gerakan itu, maka kita masih dapat mewakili permainan sebagai urutan gerakan.

Demikian pula jika dalam permainan Anda papan itu sendiri tidak statis, katakanlah kita dapat menambahkan kotak ke catur atau menghapus kotak dari papan sehingga mereka tidak bisa dipindahkan. Perubahan ini juga dapat direpresentasikan sebagai Moves tanpa mengubah struktur keseluruhan mesin Aturan Anda atau harus merujuk objek BoardSetup yang serupa

Ewan
sumber
Ini akan cenderung mempersulit implementasi ValidMoves, yang akan memperlambat logika Anda.
Taemyr
tidak juga, saya mengasumsikan pengaturan papan adalah variabel, jadi Anda perlu mendefinisikannya entah bagaimana. Jika Anda mengonversi pemindahan pengaturan ke beberapa struct atau objek lain untuk membantu perhitungan, Anda dapat menyimpan hasilnya jika diperlukan. Beberapa permainan memiliki papan yang berubah dengan permainan dan beberapa gerakan yang valid dapat bergantung pada gerakan sebelumnya alih-alih posisi saat ini (mis. Bermain catur)
Ewan
1
Menambahkan bendera dan hal-hal lain adalah kompleksitas yang Anda hindari dengan hanya memiliki riwayat perpindahan. itu tidak mahal untuk mengulang lebih dari mengatakan 100 gerakan catur untuk mendapatkan pengaturan papan saat ini dan Anda dapat men-cache hasil antara gerakan
Ewan
1
Anda juga menghindari mengubah model objek Anda untuk mencerminkan aturan. yaitu untuk catur, jika Anda membuat Gerakan yang sah -> Bagian + Papan, Anda gagal melakukan cast, en-passent, gerakan pertama untuk pion dan promosi bagian dan harus menambahkan info tambahan ke objek atau referensi objek ketiga. Anda juga kehilangan gagasan tentang siapa yang pergi dan konsep-konsep seperti cek yang ditemukan
Ewan
1
@ Gabe Ini boardLayoutadalah fungsi dari semua priorMoves(yaitu jika kita mempertahankannya sebagai status, tidak ada yang akan dikontribusikan selain masing-masing thisMove). Karenanya saran Ewan pada dasarnya adalah "memotong perantara" - bergerak yang sah fungsi langsung dari semua sebelumnya, bukan validMoves( boardLayout( priorMoves ) ).
OJFord
8

Cara standar untuk menghapus referensi melingkar antara dua kelas dalam pemrograman berorientasi objek adalah dengan memperkenalkan antarmuka yang kemudian dapat diimplementasikan oleh salah satu dari mereka. Jadi dalam kasus Anda, Anda bisa RuleBookmerujuk Stateyang kemudian merujuk ke InitialPositionProvider(yang akan menjadi antarmuka yang diimplementasikan oleh RuleBook). Ini juga membuat pengujian lebih mudah, karena Anda kemudian dapat membuat Stateyang menggunakan posisi awal yang berbeda (mungkin lebih sederhana) untuk tujuan pengujian.

Jules
sumber
6

Saya percaya referensi melingkar dan objek dewa dalam kasus Anda dapat dengan mudah dihapus dengan memisahkan kontrol aliran game dari negara dan model aturan permainan. Dengan melakukan itu Anda mungkin akan mendapatkan banyak fleksibilitas dan menyingkirkan beberapa kompleksitas yang tidak perlu.

Saya pikir Anda harus memiliki controller ("master game" jika Anda mau) yang mengontrol aliran game dan menangani perubahan status aktual alih-alih memberikan buku aturan atau game menyatakan tanggung jawab ini.

Objek status permainan tidak perlu mengubah dirinya sendiri atau menyadari aturan. Kelas hanya perlu menyediakan model yang mudah ditangani (dibuat, diperiksa, diubah, bertahan, dicatat, disalin, dll) dan objek keadaan permainan yang efisien untuk sisa aplikasi.

Buku peraturan seharusnya tidak perlu tahu tentang atau bermain-main dengan game yang sedang berlangsung. Seharusnya hanya memerlukan pandangan ke kondisi permainan untuk dapat mengetahui gerakan mana yang legal dan hanya perlu menjawab dengan keadaan permainan yang dihasilkan ketika ditanya apa yang terjadi ketika suatu langkah diterapkan pada kondisi permainan. Itu juga bisa memberikan keadaan awal permainan ketika ditanya tata letak awal.

Pengontrol harus mengetahui status permainan dan buku peraturan dan mungkin beberapa objek lain dari model permainan, tetapi tidak perlu mengacaukan detailnya.

BERASAL DARI
sumber
4
Persis menurut saya. OP mencampur terlalu banyak data dan prosedur di kelas yang sama. Lebih baik membagi ini lebih banyak. Ini adalah pembicaraan yang bagus tentang masalah ini. Btw, ketika saya membaca "view to a state game", saya pikir "argumen untuk fungsi." 100 jika saya bisa.
jpmc26
5

Saya pikir masalahnya di sini adalah Anda belum memberikan deskripsi yang jelas tentang tugas apa yang harus ditangani oleh kelas mana. Saya akan menjelaskan apa yang saya pikir merupakan deskripsi yang baik tentang apa yang harus dilakukan oleh setiap kelas, kemudian saya akan memberikan contoh kode generik yang menggambarkan ide-ide tersebut. Kita akan melihat bahwa kodenya kurang digabungkan, sehingga tidak benar-benar memiliki referensi melingkar.

Mari kita mulai dengan menggambarkan apa yang dilakukan setiap kelas.

The GameStatekelas saja harus berisi informasi tentang keadaan saat permainan. Seharusnya tidak mengandung informasi apa pun tentang kondisi permainan di masa lalu atau pergerakan di masa depan yang mungkin terjadi. Seharusnya hanya berisi informasi tentang potongan apa yang ada di kotak apa dalam catur, atau berapa banyak dan jenis catur apa pada poin apa di backgammon. The GameStateharus mengandung beberapa informasi tambahan, seperti informasi tentang Castling dalam catur atau sekitar kubus dua kali lipat di backgammon.

The Movekelas sedikit rumit. Saya akan mengatakan bahwa saya dapat menentukan langkah untuk bermain dengan menentukan GameStatehasil dari bermain langkah tersebut. Jadi Anda bisa membayangkan bahwa suatu langkah hanya dapat diimplementasikan sebagai GameState. Namun, dalam proses (misalnya) Anda dapat membayangkan bahwa jauh lebih mudah untuk menentukan langkah dengan menentukan satu titik di papan tulis. Kami ingin Movekelas kami cukup fleksibel untuk menangani semua kasus ini. Oleh karena itu Movekelas sebenarnya akan menjadi antarmuka dengan metode yang mengambil pra-pindah GameStatedan mengembalikan pasca-pindah baru GameState.

Sekarang RuleBookkelas bertanggung jawab untuk mengetahui segala sesuatu tentang aturan. Ini dapat dipecah menjadi tiga hal. Perlu mengetahui apa yang awal GameState, perlu tahu apa yang legal, dan perlu tahu apakah salah satu pemain telah menang.

Anda juga bisa membuat GameHistorykelas untuk melacak semua gerakan yang telah dilakukan dan semua GameStatesyang telah terjadi. Kelas baru diperlukan karena kami memutuskan bahwa satu GameStatetidak boleh bertanggung jawab untuk mengetahui semua GameStateyang datang sebelumnya.

Ini menyimpulkan kelas / antarmuka yang akan saya bahas. Anda juga memiliki Boardkelas. Tetapi saya pikir papan di permainan yang berbeda cukup berbeda sehingga sulit untuk melihat apa yang secara umum dapat dilakukan dengan papan. Sekarang saya akan memberikan antarmuka generik dan mengimplementasikan kelas generik.

Pertama adalah GameState. Karena kelas ini sepenuhnya tergantung pada gim tertentu, tidak ada Gamestateantarmuka atau kelas generik .

Berikutnya adalah Move. Seperti yang saya katakan, ini dapat diwakili dengan antarmuka yang memiliki metode tunggal yang mengambil keadaan pra-pindah dan menghasilkan keadaan pasca-pindah. Berikut ini kode untuk antarmuka ini:

package boardgame;

/**
 *
 * @param <T> The type of GameState
 */
public interface Move<T> {

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

Perhatikan bahwa ada parameter tipe. Ini karena, misalnya, ChessMoveperlu mengetahui tentang rincian pra-langkah ChessGameState. Jadi, misalnya, deklarasi kelas ChessMoveakan menjadi

class ChessMove extends Move<ChessGameState>,

di mana Anda sudah mendefinisikan ChessGameStatekelas.

Selanjutnya saya akan membahas RuleBookkelas generik . Ini kodenya:

package boardgame;

import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public interface RuleBook<T> {

    T makeInitialState();

    List<Move<T>> makeMoveList(T gameState);

    StateEvaluation evaluateState(T gameState);

    boolean isMoveLegal(Move<T> move, T currentState);

}

Sekali lagi ada parameter tipe untuk GameStatekelas. Karena RuleBookseharusnya mengetahui apa itu keadaan awal, kami telah menggunakan metode untuk memberikan keadaan awal. Karena yang RuleBookseharusnya mengetahui langkah apa yang legal, kami memiliki metode untuk menguji apakah langkah tersebut legal di negara tertentu dan memberikan daftar langkah hukum untuk negara tertentu. Akhirnya, ada metode untuk mengevaluasi GameState. Perhatikan bahwa RuleBookseharusnya hanya bertanggung jawab untuk menggambarkan jika salah satu atau pemain lain telah menang, tetapi tidak siapa yang berada di posisi yang lebih baik di tengah permainan. Memutuskan siapa yang berada dalam posisi yang lebih baik adalah hal yang rumit yang harus dipindahkan ke kelasnya sendiri. Oleh karena itu StateEvaluationkelas sebenarnya hanya enum sederhana yang diberikan sebagai berikut:

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

Terakhir, mari kita jelaskan GameHistorykelasnya. Kelas ini bertanggung jawab untuk mengingat semua posisi yang dicapai dalam permainan serta gerakan yang dimainkan. Hal utama yang harus dapat dilakukan adalah merekam Movesebagai dimainkan. Anda juga dapat menambahkan fungsionalitas untuk membatalkan Moves. Saya memiliki implementasi di bawah ini.

package boardgame;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public class GameHistory<T> {

    private List<T> states;
    private List<Move<T>> moves;

    public GameHistory(T initialState) {
        states = new ArrayList<>();
        states.add(initialState);
        moves = new ArrayList<>();
    }

    void recordMove(Move<T> move) throws IllegalArgumentException {
        moves.add(move);
        states.add(move.makeResultingState(getMostRecentState()));
    }

    void resetToNthState(int n) {
        states = states.subList(0, n + 1);
        moves = moves.subList(0, n);
    }

    void undoLastMove() {
        resetToNthState(getNumberOfMoves() - 1);
    }

    T getMostRecentState() {
        return states.get(getNumberOfMoves());
    }

    T getStateAfterNthMove(int n) {
        return states.get(n + 1);
    }

    Move<T> getNthMove(int n) {
        return moves.get(n);
    }

    int getNumberOfMoves() {
        return moves.size();
    }

}

Akhirnya, kita bisa membayangkan membuat Gamekelas untuk mengikat semuanya. Ini Gamekelas seharusnya mengekspos metode yang memungkinkan orang untuk melihat apa yang saat ini GameStateadalah, melihat siapa, jika seseorang memiliki satu, melihat apa yang bisa dimainkan bergerak, dan bermain bergerak. Saya memiliki implementasi di bawah ini

package boardgame;

import java.util.List;

/**
 *
 * @author brian
 * @param <T> The type of GameState
 */
public class Game<T> {

    GameHistory<T> gameHistory;
    RuleBook<T> ruleBook;

    public Game(RuleBook<T> ruleBook) {
        this.ruleBook = ruleBook;
        final T initialState = ruleBook.makeInitialState();
        gameHistory = new GameHistory<>(initialState);
    }

    T getCurrentState() {
        return gameHistory.getMostRecentState();
    }

    List<Move<T>> getLegalMoves() {
        return ruleBook.makeMoveList(getCurrentState());
    }

    void doMove(Move<T> move) throws IllegalArgumentException {
        if (!ruleBook.isMoveLegal(move, getCurrentState())) {
            throw new IllegalArgumentException("Move is not legal in this position");
        }
        gameHistory.recordMove(move);
    }

    void undoMove() {
        gameHistory.undoLastMove();
    }

    StateEvaluation evaluateState() {
        return ruleBook.evaluateState(getCurrentState());
    }

}

Perhatikan di kelas ini bahwa RuleBooktidak bertanggung jawab untuk mengetahui apa arus GameState. Itu GameHistorypekerjaannya. Jadi mereka Gamebertanya seperti GameHistoryapa keadaan saat ini dan memberikan informasi ini pada RuleBooksaat Gamekebutuhan untuk mengatakan apa langkah hukumnya atau jika ada yang menang.

Pokoknya, inti dari jawaban ini adalah begitu Anda telah membuat penentuan yang masuk akal tentang apa yang bertanggung jawab atas masing-masing kelas, dan Anda membuat setiap kelas fokus pada sejumlah kecil tanggung jawab, dan Anda menugaskan masing-masing tanggung jawab ke kelas yang unik, kemudian kelas-kelas tersebut cenderung dipisahkan, dan semuanya menjadi mudah dikodekan. Semoga itu terlihat dari contoh kode yang saya berikan.

Brian Moths
sumber
3

Dalam pengalaman saya, referensi melingkar umumnya menunjukkan bahwa desain Anda tidak dipikirkan dengan baik.

Dalam desain Anda, saya tidak mengerti mengapa RuleBook perlu "tahu" tentang Negara. Mungkin menerima Negara sebagai parameter untuk beberapa metode, tentu saja, tetapi mengapa harus tahu (yaitu sebagai variabel instan) referensi ke suatu Negara? Itu tidak masuk akal bagi saya. RuleBook tidak perlu "tahu" tentang keadaan permainan tertentu untuk melakukan tugasnya; aturan permainan tidak berubah tergantung pada kondisi permainan saat ini. Jadi Anda salah mendesainnya, atau mendesainnya dengan benar tetapi menjelaskannya dengan salah.

Mehrdad
sumber
+1. Anda membeli permainan papan fisik, Anda mendapatkan buku peraturan yang bisa menggambarkan aturan tanpa negara.
unperson325680
1

Ketergantungan melingkar tidak selalu merupakan masalah teknis, tetapi harus dianggap sebagai bau kode, yang biasanya merupakan pelanggaran terhadap Prinsip Tanggung Jawab Tunggal .

Ketergantungan melingkar Anda berasal dari kenyataan bahwa Anda mencoba melakukan terlalu banyak dari Stateobjek Anda .

Setiap objek stateful seharusnya hanya menyediakan metode yang berhubungan langsung dengan mengelola negara lokal itu. Jika membutuhkan lebih dari logika paling dasar, maka mungkin harus dipecah menjadi pola yang lebih besar. Beberapa orang memiliki pendapat berbeda tentang hal ini, tetapi sebagai aturan umum, jika Anda melakukan lebih dari sekadar getter dan setter pada data, Anda melakukan terlalu banyak.

Dalam hal ini, Anda sebaiknya memiliki StateFactory, yang bisa tahu tentang a Rulebook. Anda mungkin memiliki kelas pengontrol lain yang menggunakan Anda StateFactoryuntuk membuat game baru. Statepasti tidak seharusnya tahu Rulebook. Rulebookmungkin tahu tentang Statetergantung pada implementasi aturan Anda.

00500005
sumber
0

Apakah ada kebutuhan untuk objek buku aturan untuk terikat pada keadaan permainan tertentu, atau akankah lebih masuk akal untuk memiliki objek buku aturan dengan metode yang, mengingat keadaan permainan, akan melaporkan gerakan apa yang tersedia dari keadaan itu (dan, setelah melaporkan hal itu, tidak ingat apa-apa tentang keadaan tersebut)? Kecuali jika ada sesuatu yang bisa diperoleh dengan memiliki objek yang ditanyakan tentang gerakan yang tersedia mempertahankan memori keadaan permainan, tidak perlu untuk itu tetap referensi.

Mungkin saja dalam beberapa kasus akan ada keuntungan memiliki status mempertahankan aturan-mengevaluasi objek. Jika Anda berpikir situasi seperti itu mungkin muncul, saya sarankan menambahkan kelas "wasit", dan meminta buku aturan menyediakan metode "createReferee". Berbeda dengan buku aturan, yang tidak peduli apakah itu ditanya tentang satu pertandingan, atau lima puluh, objek wasit akan mengharapkan untuk memimpin satu pertandingan. Itu tidak akan diharapkan untuk merangkum semua negara yang terkait dengan permainan yang diresmikan, tetapi bisa menyimpan informasi apa pun tentang permainan yang dianggap berguna. Jika sebuah game mendukung fungsionalitas "undo", mungkin ada baiknya jika wasit menyertakan cara untuk menghasilkan objek "snapshot" yang dapat disimpan bersama dengan status game sebelumnya; objek itu harus,

Jika beberapa penggandaan mungkin diperlukan antara aspek-aspek pemrosesan aturan dan pemrosesan-status-permainan dari kode, menggunakan objek wasit akan memungkinkan untuk membuat sambungan seperti itu keluar dari buku aturan utama dan kelas-kelas game-state. Mungkin juga memungkinkan untuk membuat aturan baru mempertimbangkan aspek-aspek keadaan permainan yang tidak akan dianggap relevan oleh kelas-permainan (misalnya jika aturan ditambahkan yang mengatakan "Objek X tidak dapat melakukan Y jika pernah ke lokasi Z ", wasit dapat diubah untuk melacak objek yang telah ke lokasi Z tanpa harus mengubah kelas permainan-negara).

supercat
sumber
-2

Cara yang tepat untuk menangani ini adalah dengan menggunakan antarmuka. Alih-alih memiliki dua kelas yang tahu tentang satu sama lain, mintalah masing-masing kelas mengimplementasikan antarmuka dan referensi yang ada di kelas lainnya. Katakanlah Anda memiliki kelas A dan kelas B yang perlu saling referensi. Memiliki antarmuka implement kelas A dan antarmuka implement B kelas B maka Anda dapat mereferensikan antarmuka B dari kelas A dan antarmuka A dari kelas B. Kelas A dapat dari pada proyek itu sendiri, seperti halnya kelas B. Antarmuka berada dalam proyek terpisah yang kedua referensi proyek lainnya.

Peter
sumber
2
ini sepertinya hanya mengulangi poin-poin yang dibuat dan dijelaskan dalam jawaban sebelumnya yang diposting beberapa jam sebelum yang ini
agas