Mempertahankan pemisahan masalah

8

Saya membuat aplikasi C # pertama saya dan saya mengalami sedikit kesulitan dengan pemisahan kekhawatiran. Saya mengerti konsepnya, tetapi saya tidak tahu apakah saya melakukannya dengan benar. Saya memiliki ini sebagai contoh cepat untuk menggambarkan pertanyaan saya. Dalam aplikasi seperti game, ada kelas utama yang menjalankan loop utama, seperti Program atau Game. Pertanyaan saya adalah, apakah saya mempertahankan setiap referensi untuk setiap instance dari kelas di kelas ini, dan menjadikannya satu-satunya cara mereka berinteraksi?

Misalnya, permainan kartu dengan pemain, kartu, dan papan. Katakanlah pemain ingin meletakkan kartu di papan tulis, tetapi kelas Pemain hanya memiliki Daftar <> Kartu dan tidak tahu tentang papan permainan. Namun, kelas Game utama tahu tentang para pemain, kartu, dan gameboard. Jika tergantung pada kelas Game untuk meletakkan kartu di papan, atau apakah lebih masuk akal bahwa, karena itu adalah tindakan pemain, itu harus berada di dalam kelas Player.

Contoh:

public class Game{
    private GameBoard gameBoard;
    private Player[] players;

   public Game(){
     gameBoard = new GameBoard(10,10);
     Player player1 = new Player();
     Player player2 = new Player();
     players = {player1, player2};
   }

   // Create method here?
   public void PlayerPlaceCard(int x, int y, int cardIndex){
      gameBoard.grid[1,1] = player1.cards[cardIndex];
   }
}

public class Player {
     public List<Cards> cards = new List<Cards>();

     public Player(){
     }

     // Or place method here?
     public PlaceCard(Card card, int x, int y, GameBoard gameBoard){
     }
}

public class GameBoard{
    public Card[,] grid;

    public GameBoard(int x, int y){
       // Make the game board
    }
}

public class Card{
   public string name;
   public string value;
}

Bagi saya itu lebih masuk akal untuk memiliki metode dalam Game, karena Game tahu tentang segalanya. Tetapi ketika saya menambahkan lebih banyak kode, Game menjadi sangat membengkak dan saya menulis banyak fungsi PlayerDoesThis ().

Terima kasih atas sarannya

nada31
sumber

Jawaban:

12

Kuncinya di sini bukan hanya pemisahan keprihatinan , tetapi juga prinsip tanggung jawab tunggal . Keduanya pada dasarnya adalah sisi yang berbeda dari koin yang sama: ketika saya berpikir SOC saya pikir top-down (saya punya masalah ini, bagaimana saya memisahkan mereka?) Sementara SRP lebih bottom-up (saya punya objek ini, apakah ada kekhawatiran tunggal? Haruskah dibelah? Apakah kekhawatirannya sudah terlalu banyak?).

Dalam contoh Anda, Anda memiliki entitas berikut dan tanggung jawabnya:

  • Game: ini adalah kode yang membuat program "pergi."
  • GameBoard: mempertahankan kondisi area bermain.
  • Kartu: satu entitas di papan permainan.
  • Player: melakukan tindakan yang mengubah status papan permainan.

Setelah Anda memikirkan tanggung jawab tunggal masing-masing entitas, garis menjadi lebih jelas.

Dalam aplikasi seperti game, ada kelas utama yang menjalankan loop utama, seperti Program atau Game. Pertanyaan saya adalah, apakah saya mempertahankan setiap referensi untuk setiap instance dari kelas di kelas ini, dan menjadikannya satu-satunya cara mereka berinteraksi?

Sebenarnya ada dua masalah di sini yang perlu diingat. Hal pertama yang harus diputuskan adalah entitas apa yang tahu tentang entitas lain? Entitas mana yang milik entitas lain?

Lihatlah tanggung jawab yang saya uraikan di atas. Pemain melakukan tindakan yang mengubah status papan permainan. Dengan kata lain, Pemain mengirim pesan ke (memanggil metode pada) papan permainan. Pesan-pesan itu kemungkinan melibatkan kartu: misalnya, pemain dapat meletakkan kartu di tangannya di papan, atau mengubah status kartu yang ada (misalnya membalikkan kartu atau memindahkannya ke lokasi baru).

Jelas, seorang pemain harus tahu tentang papan permainan yang bertentangan dengan asumsi yang Anda buat dalam pertanyaan Anda. Kalau tidak, pemain harus mengirim pesan ke permainan, yang kemudian menyampaikan pesan itu ke papan permainan. Karena pemain melakukan aksi di papan permainan, pemain harus tahu tentang papan permainan. Ini meningkatkan penggabungan: alih-alih pemain yang mengirim pesan secara langsung, sekarang dua aktor harus tahu cara mengirim pesan itu. The Law of Demeter menyiratkan bahwa jika sebuah objek harus bertindak pada objek lain, dalam skenario ini, bahwa objek lain harus disahkan pada melalui parameter untuk mengurangi coupling.

Selanjutnya, di mana Anda menyimpan status apa? Gim adalah penggeraknya di sini, ia harus menggulung semua objek baik secara langsung atau melalui proksi (mis. Pabrik atau konstruktor yang dipanggil gim). Pertanyaan logis selanjutnya adalah objek apa yang membutuhkan objek lain? Ini pada dasarnya apa yang saya minta di atas, tetapi cara yang berbeda untuk menanyakannya.

Cara saya mendesainnya adalah seperti ini:

  • Game menciptakan semua objek yang diperlukan untuk game.

  • Game mengocok kartu dan membaginya menurut permainan apa pun yang diwakilinya (poker, solitaire, dll).

  • Game menempatkan kartu di lokasi awal mereka: mungkin beberapa di papan permainan, yang lain di tangan pemain.

  • Game kemudian masuk ke loop utama yang mewakili giliran.

Setiap belokan akan terlihat seperti ini:

  • Game mengirim pesan ke (memanggil metode pada) pemain saat ini dan memberikan referensi ke papan permainan.

  • Player melakukan logika internal apa saja (pemutar komputer) atau interaksi pengguna yang diperlukan untuk menentukan permainan apa yang akan dilakukan.

  • Pemain mengirim pesan ke papan permainan yang disediakan untuk itu meminta untuk mengubah status papan permainan.

  • Board game memutuskan apakah langkah tersebut valid atau tidak ( bertanggung jawab untuk mempertahankan status game yang valid).

  • Kontrol kembali ke permainan, yang kemudian memutuskan apa yang harus dilakukan selanjutnya. Periksa kondisi menang? Seri? Pemain berikutnya? Giliran berikutnya? Tergantung pada permainan kartu tertentu yang sedang dimainkan.

Jika tergantung pada kelas Game untuk meletakkan kartu di papan, atau apakah lebih masuk akal bahwa, karena itu adalah tindakan pemain, itu harus berada di dalam kelas Player.

Keduanya: Game bertanggung jawab untuk pengaturan awal, tetapi Player melakukan tindakan di papan tulis. GameBoard bertanggung jawab untuk menjamin keadaan yang valid. Sebagai contoh, dalam Solitaire klasik hanya kartu teratas pada tumpukan yang dapat menghadap ke atas.


Kembali ke poin awal saya: Anda memiliki pemisahan kekhawatiran yang tepat. Anda mengidentifikasi objek yang tepat. Yang membuat Anda tersandung adalah mencari tahu bagaimana pesan mengalir melalui sistem dan objek mana yang harus menyimpan referensi ke objek lain. Saya akan mendesainnya seperti ini, yaitu pseudocode:

class Game {
  main();
}

class GameBoard {
  // Data structures specific to the game being played. There is a
  // lot of hand-waving here to give the general idea without
  // getting bogged down in the implementation.
  Map<Card, Location> cards;

  GameBoard(Map<Card, Location>);

  // Return false if the move is invalid.
  bool flip(Card);
  bool move(Card, Location);
}

class Card {
  // Make Rank and Suit enums.
  Suit suit;
  Rank rank;
  bool faceUp;
}

class Player {
  Set<Card> hand;

  Player(Set<Card>);
  void takeTurn(GameBoard);
}

sumber
1
Jawaban bagus, hanya satu hal hilang yang kurasakan. Dalam contoh OP, dia memberikan x, y ke gameboard untuk memberitahukan dengan tepat ke mana harus meletakkan kartu. Sementara pemain akan memanggil gameboard untuk meletakkan kartu, itu harus menjadi gameboard yang memutuskan x, y. Membutuhkan pemain untuk mengetahui tentang koordinat papan menciptakan abstraksi bocor.
David Arno
@ Davidvido jika kartu dapat dimainkan di lokasi dalam kotak persegi panjang, bagaimana seharusnya pemain menunjukkan di mana dari mereka yang memainkannya, jika tidak dengan koordinat? (Itu bukan coordintes layar, tetapi koordinat kotak.)
Paŭlo Ebermann
1
Rincian spesifik tentang cara menempatkan kartu harus diabstraksikan dengan entah bagaimana oleh kelas Lokasi, yang akan menjadi bagian yang sangat kecil dalam desain ini. Tentu, koordinat mungkin berfungsi. Mungkin juga lebih tepat untuk menggunakan lokasi bernama seperti "tumpukan tumpukan." Detail implementasi tidak penting ketika memetakan desain seperti yang dinyatakan dalam pertanyaan.
@Snowman Terima kasih, jawaban ini tepat. Jika kita memiliki Pemain yang selalu bertindak atas GameBoard, tidakkah lebih masuk akal untuk memiliki referensi lokal di dalam kelas, yang akan ditetapkan selama konstruktornya? Dengan begitu, GameBoard tidak harus diteruskan ke Player setiap saat (akan ada banyak interaksi dengan papan).
nada31
@ Davidvido Pemain benar-benar perlu memberi tahu gameboard tempat meletakkannya, gameboard perlu memvalidasinya. Pemain dapat mengambil dan memindahkan kartu.
Nada31