Bersihkan cara OOP memetakan objek ke presenternya

13

Saya membuat permainan papan (seperti catur) di Jawa, di mana masing-masing bagian adalah jenisnya sendiri (seperti Pawn, Rookdll.). Untuk bagian GUI dari aplikasi saya perlu gambar untuk masing-masing bagian ini. Sejak melakukan berpikir seperti

rook.image();

melanggar pemisahan UI dan logika bisnis, saya akan membuat presenter yang berbeda untuk masing-masing bagian dan kemudian memetakan jenis potongan ke penyaji yang sesuai seperti

private HashMap<Class<Piece>, PiecePresenter> presenters = ...

public Image getImage(Piece piece) {
  return presenters.get(piece.getClass()).image();
}

Sejauh ini baik. Namun, saya merasakan guru OOP yang bijaksana akan mengerutkan kening ketika memanggil getClass()metode dan akan menyarankan menggunakan pengunjung misalnya seperti ini:

class Rook extends Piece {
  @Override 
  public <T> T accept(PieceVisitor<T> visitor) {
    return visitor.visitRook(this);
  }
}

class ImageVisitor implements PieceVisitor<Image> {
  @Override  
  public Image visitRook(Rook rook) {
    return rookImage;
  } 
}

Saya suka solusi ini (terima kasih, guru), tetapi memiliki satu kelemahan signifikan. Setiap kali tipe bagian baru ditambahkan ke aplikasi, PieceVisitor perlu diperbarui dengan metode baru. Saya ingin menggunakan sistem saya sebagai kerangka permainan papan di mana potongan-potongan baru dapat ditambahkan melalui proses sederhana di mana pengguna kerangka kerja hanya akan memberikan implementasi baik karya dan presenternya, dan cukup tancapkan ke kerangka kerja. Pertanyaan saya: apakah ada solusi OOP yang bersih tanpa instanceof, getClass()dll. Yang memungkinkan perluasan jenis ini?

lishaak
sumber
Apa tujuan memiliki kelas sendiri untuk setiap jenis bidak catur? Saya rasa benda sepotong memegang posisi, warna dan jenis potongan tunggal. Saya mungkin memiliki kelas Piece dan dua enum (PieceColor, PieceType) untuk tujuan itu.
DATANG DARI
@COMEFROM yah, jelas jenis karya yang berbeda memiliki perilaku yang berbeda, jadi perlu ada beberapa kode khusus yang membedakan antara kata benteng dan bidak. Yang mengatakan, saya umumnya lebih suka memiliki kelas sepotong standar yang menangani semua jenis dan menggunakan objek Strategi untuk menyesuaikan perilaku.
Jules
@ Jules dan apa untungnya memiliki strategi untuk masing-masing bagian daripada memiliki kelas yang terpisah untuk setiap bagian yang mengandung perilaku itu sendiri?
lishaak
2
Salah satu manfaat yang jelas dari memisahkan aturan permainan dari benda-benda stateful yang mewakili masing-masing bagian adalah bahwa itu memecahkan masalah Anda segera. Saya biasanya tidak akan mencampur model aturan dengan model negara sama sekali ketika menerapkan permainan papan berbasis giliran.
DATANG DARI
2
Jika Anda tidak ingin memisahkan aturan, maka Anda bisa menentukan aturan gerakan dalam enam objek PieceType (bukan kelas!). Bagaimanapun, saya pikir cara terbaik untuk menghindari jenis masalah yang Anda hadapi adalah dengan memisahkan masalah dan menggunakan warisan hanya ketika itu benar-benar bermanfaat.
DATANG DARI

Jawaban:

10

apakah ada solusi OOP bersih tanpa instanceof, getClass () dll. yang akan memungkinkan untuk ekstensibilitas seperti ini?

Ya ada.

Izinkan saya bertanya kepada Anda. Dalam contoh Anda saat ini, Anda menemukan cara untuk memetakan jenis potongan ke gambar. Bagaimana cara mengatasi masalah sepotong yang dipindahkan?

Teknik yang lebih kuat daripada bertanya tentang tipe adalah mengikuti Tell, jangan bertanya . Bagaimana jika masing-masing bagian menggunakan PiecePresenterantarmuka dan tampilannya seperti ini:

class PiecePresenter implements PieceOutput {

  BoardPresenter board;
  Image pieceImage;

  @Override
  PiecePresenter(BoardPresenter board, Image image) {
    public void display(int rank, int file) {
      board.display(pieceImage, rank, file);
    } 
  }
}

Konstruksi akan terlihat seperti ini:

rookWhiteImage = new Image("Rook-White.png");
PieceOutput rookWhiteOutPort = new PiecePresenter(boardPresenter, rookWhiteImage);
PieceInput rookWhiteInPort = new Rook(rookWhiteOutPort);
board[0, 0] = rookWhiteInPort;

Gunakan akan terlihat seperti:

board[rank, file].display(rank, file);

Idenya di sini adalah untuk menghindari mengambil tanggung jawab untuk melakukan apa pun yang hal-hal lain bertanggung jawab dengan tidak bertanya tentang hal itu atau membuat keputusan berdasarkan itu. Alih-alih pegang referensi untuk sesuatu yang tahu apa yang harus dilakukan tentang sesuatu dan katakan itu untuk melakukan sesuatu tentang apa yang Anda ketahui.

Ini memungkinkan terjadinya polimorfisme. Anda tidak PEDULI dengan apa yang Anda bicarakan. Anda tidak peduli apa yang harus dikatakan. Anda hanya peduli bahwa itu dapat melakukan apa yang perlu Anda lakukan.

Diagram yang baik yang membuat ini di lapisan yang terpisah, mengikuti tell-don't-ask, dan menunjukkan bagaimana untuk tidak memasangkan layer ke layer secara tidak adil adalah ini :

masukkan deskripsi gambar di sini

Ia menambahkan lapisan use case yang belum pernah kami gunakan di sini (dan tentu saja dapat menambahkan) tetapi kami mengikuti pola yang sama seperti yang Anda lihat di sudut kanan bawah.

Anda juga akan melihat bahwa Presenter tidak menggunakan warisan. Ini menggunakan komposisi. Warisan harus menjadi cara terakhir untuk mendapatkan polimorfisme. Saya lebih suka desain yang disukai menggunakan komposisi dan delegasi. Mengetik keyboard sedikit lebih banyak tetapi lebih banyak daya.

candied_orange
sumber
2
Saya pikir ini mungkin jawaban yang baik (+1), tetapi saya tidak yakin bahwa solusi Anda adalah contoh yang baik dari Arsitektur Bersih: entitas Rook sekarang sangat langsung memegang referensi ke presenter. Bukankah ini semacam kopling yang berusaha dicegah oleh Arsitektur Bersih? Dan apa jawaban Anda tidak cukup jelas: pemetaan antara entitas dan presenter sekarang ditangani oleh siapa pun yang membuat entitas tersebut. Ini mungkin lebih elegan daripada solusi alternatif, tetapi ini adalah tempat ketiga yang harus dimodifikasi ketika entitas baru ditambahkan - sekarang, ini bukan Pengunjung tetapi Pabrik.
amon
@amon, seperti yang saya katakan, lapisan use case hilang. Terry Pratchett menyebut hal-hal semacam ini "kebohongan untuk anak-anak". Saya mencoba untuk tidak membuat contoh yang luar biasa. Jika Anda pikir saya perlu dibawa ke sekolah dalam hal Clean Architecture, saya mengundang Anda untuk membawa saya ke tempat tugas di sini .
candied_orange
maaf, tidak, saya tidak ingin sekolah Anda, saya hanya ingin memahami pola arsitektur ini dengan lebih baik. Saya benar-benar baru saja menemukan konsep "arsitektur bersih" dan "arsitektur heksagonal" kurang dari sebulan yang lalu.
amon
@amon dalam hal itu memberikan tampilan yang keras dan kemudian membawa saya ke tugas. Saya akan senang jika Anda melakukannya. Saya masih mencari tahu bagian dari ini sendiri. Saat ini saya sedang bekerja untuk meningkatkan proyek python berbasis menu ke gaya ini. Tinjauan kritis Arsitektur Bersih dapat ditemukan di sini .
candied_orange
1
Instansiasi @lishaak dapat terjadi di main. Lapisan dalam tidak tahu tentang lapisan luar. Lapisan luar hanya tahu tentang antarmuka.
candied_orange
5

Bagaimana dengan itu:

Model Anda (kelas angka) memiliki metode umum yang mungkin Anda perlukan dalam konteks lain juga:

interface ChessFigure {
  String getPlayerColor();
  String getFigureName();
}

Gambar yang akan digunakan untuk menampilkan gambar tertentu mendapatkan nama file dengan skema penamaan:

King-White.png
Queen-Black.png

Kemudian Anda dapat memuat gambar yang sesuai tanpa mengakses informasi tentang kelas java.

new File(FIGURE_IMAGES_DIR,
         String.format("%s-%s.png",
                       figure.getFigureName(),
                       figure.getPlayerColor)));

Saya juga tertarik pada solusi umum untuk masalah seperti ini ketika saya perlu melampirkan beberapa informasi (tidak hanya gambar) ke sekumpulan kelas yang berpotensi berkembang. "

Saya pikir Anda tidak harus fokus pada kelas sebanyak itu. Alih-alih berpikir dalam hal objek bisnis .

Dan solusi generik adalah pemetaan apa pun. Trik IMHO adalah memindahkan pemetaan itu dari kode ke sumber daya yang lebih mudah dipelihara.

Contoh saya melakukan pemetaan ini dengan konvensi yang cukup mudah diimplementasikan dan menghindari menambahkan informasi terkait tampilan ke dalam model bisnis . Di sisi lain Anda dapat menganggapnya sebagai pemetaan "tersembunyi" karena tidak diungkapkan di mana pun.

Pilihan lain adalah melihat ini sebagai kasus bisnis terpisah dengan lapisan MVC-nya sendiri termasuk lapisan ketekunan yang berisi pemetaan.

Timothy Truckle
sumber
Saya melihat ini sebagai solusi yang sangat praktis dan sederhana untuk skenario khusus ini. Saya juga tertarik pada solusi umum untuk masalah seperti ini ketika saya perlu melampirkan beberapa informasi (tidak hanya gambar) ke sekumpulan kelas yang berpotensi berkembang.
lishaak
3
@lishaak: pendekatan umum di sini adalah memberikan cukup meta data dalam objek bisnis Anda sehingga pemetaan umum ke sumber daya atau elemen antarmuka pengguna dapat dilakukan secara otomatis.
Doc Brown
2

Saya akan membuat UI / view class yang terpisah untuk setiap bagian yang berisi informasi visual. Setiap salah satu dari kelas tampilan ini memiliki penunjuk ke model / rekan bisnisnya yang berisi posisi dan aturan permainan potongan tersebut.

Jadi, ambil pion misalnya:

class Pawn : public Piece {
public:
    Vec2 position() const;
    /**
     The rest of the piece's interface
     */
}

class PawnView : public PieceView {
public:
    PawnView(Piece* piece) { _piece = piece; }
    void drawSelf(BoardView* board) const{
         board.drawPiece(_image, _piece->position);
    }
private:
    Piece* _piece;
    Image _image;
}

Ini memungkinkan untuk pemisahan logika dan UI sepenuhnya. Anda bisa meneruskan pointer potongan logika ke kelas game yang akan menangani pemindahan potongan. Satu-satunya kelemahan adalah bahwa Instansiasi harus terjadi di kelas UI.

Lasse Jacobs
sumber
OK, jadi katakanlah saya punya beberapa Piece* p. Bagaimana saya tahu bahwa saya harus membuat PawnViewuntuk menampilkannya, dan bukan a RookViewatau KingView? Atau apakah saya harus membuat tampilan atau presenter yang menyertainya segera setiap kali saya membuat Karya baru? Itu pada dasarnya akan menjadi solusi @ CandiedOrange dengan dependensi terbalik. Dalam hal ini, PawnViewkonstruktor juga dapat mengambil Pawn*, bukan hanya a Piece*.
amon
Ya saya minta maaf, konstruktor PawnView akan mengambil Pion *. Dan Anda tidak harus membuat PawnView dan Pion secara bersamaan. Misalkan ada permainan yang dapat memiliki 100 Pion tetapi hanya 10 yang bisa dilihat secara bersamaan, dalam hal ini Anda dapat menggunakan kembali pion tampilan untuk beberapa Pion.
Lasse Jacobs
Dan saya setuju dengan solusi @ CandiedOrange, saya hanya ingin berbagi 2 sen.
Lasse Jacobs
0

Saya akan mendekati ini dengan membuat Piecegenerik, di mana parameternya adalah jenis enumerasi yang mengidentifikasi jenis potongan, masing-masing bagian memiliki referensi ke satu jenis tersebut. Kemudian UI dapat menggunakan peta dari enumerasi seperti sebelumnya:

public abstract class Piece<T>
{
    T type;
    public Piece (T type) { this.type = type; }
    public T getType() { return type; }
}
enum ChessPieceType { PAWN, ... }
public class Pawn extends Piece<ChessPieceType>
{
    public Pawn () { super (ChessPieceType.PAWN); }

Ini memiliki dua keunggulan menarik:

Pertama, berlaku untuk sebagian besar bahasa yang diketik secara statis: Jika Anda menentukan papan Anda dengan tipe potongan untuk xpect, Anda tidak dapat memasukkan jenis potongan yang salah ke dalamnya.

Kedua, dan mungkin lebih menarik, jika Anda bekerja di Jawa (atau bahasa JVM lainnya), Anda harus mencatat bahwa setiap nilai enum bukan hanya objek independen, tetapi juga dapat memiliki kelasnya sendiri. Ini berarti bahwa Anda dapat menggunakan objek jenis potongan Anda sebagai objek Strategi untuk memberi pelanggan perilaku potongan:

 public class ChessPiece extends Piece<ChessPieceType> {
    ....
   boolean isMoveValid (Move move)
    {
         return getType().movePatterns().contains (move.asVector()) && ....


 public enum ChessPieceType {
    public abstract Set<Vector2D> movePatterns();
    PAWN {
         public Set<Vector2D> movePatterns () {
              return Util.makeSet(
                    new Vector2D(0, 1),
                    ....

(Jelas implementasi yang sebenarnya harus lebih kompleks dari itu, tetapi mudah-mudahan Anda mendapatkan ide)

Jules
sumber
0

Saya seorang programmer pragmatis dan saya benar-benar tidak peduli apa itu arsitektur yang bersih atau kotor. Saya percaya persyaratan dan itu harus ditangani dengan cara sederhana.

Persyaratan Anda adalah logika aplikasi catur Anda akan diwakili di berbagai lapisan presentasi (perangkat) seperti di web, aplikasi seluler, atau bahkan aplikasi konsol sehingga Anda perlu mendukung persyaratan ini. Anda mungkin lebih suka menggunakan warna yang sangat berbeda, membuat gambar pada setiap perangkat.

public class Program
{
    public static void Main(string[] args)
    {
        new Rook(new Presenter { Image = "rook.png", Color = "blue" });
    }
}

public abstract class Piece
{
    public Presenter Presenter { get; private set; }
    public Piece(Presenter presenter)
    {
        this.Presenter = presenter;
    }
}

public class Pawn : Piece
{
    public Pawn(Presenter presenter) : base(presenter) { }
}

public class Rook : Piece
{
    public Rook(Presenter presenter) : base(presenter) { }
}

public class Presenter
{
    public string Image { get; set; }
    public string Color { get; set; }
}

Seperti yang Anda lihat, parameter presenter harus diteruskan pada setiap perangkat (lapisan presentasi) secara berbeda. Itu berarti lapisan presentasi Anda akan memutuskan bagaimana untuk mewakili setiap bagian. Apa yang salah dalam solusi ini?

Darah segar
sumber
Yah pertama-tama, karya itu harus tahu lapisan presentasi di sana dan itu membutuhkan gambar. Bagaimana jika beberapa lapisan presentasi tidak memerlukan gambar? Kedua, karya harus dibuat oleh lapisan UI, karena karya tidak dapat ada tanpa presenter. Bayangkan permainan berjalan di server di mana tidak ada UI yang dibutuhkan. Maka Anda tidak dapat instantiate karya karena Anda tidak memiliki UI.
lishaak
Anda dapat mendefinisikan representer sebagai parameter opsional juga.
Freshblood
0

Ada solusi lain yang akan membantu Anda untuk abstrak UI dan logika domain sepenuhnya. Papan Anda harus diekspos ke lapisan UI Anda dan lapisan UI Anda dapat memutuskan bagaimana cara mewakili bagian dan posisi.

Untuk mencapai ini, Anda dapat menggunakan string Fen . String Fen pada dasarnya adalah informasi keadaan papan dan memberikan potongan saat ini dan posisi mereka di papan tulis. Jadi papan Anda dapat memiliki metode yang mengembalikan keadaan papan saat ini melalui string Fen maka lapisan UI Anda dapat mewakili papan sesuai keinginan. Inilah sebenarnya cara kerja mesin catur saat ini. Mesin catur adalah aplikasi konsol tanpa GUI tetapi kami menggunakannya melalui GUI eksternal. Mesin catur berkomunikasi dengan GUI melalui string fen dan notasi catur.

Anda bertanya bagaimana jika saya menambahkan bagian baru? Tidak realistis bahwa catur akan memperkenalkan karya baru. Itu akan menjadi perubahan besar di domain Anda. Jadi ikuti prinsip YAGNI.

Darah segar
sumber
Nah, solusi ini tentu fungsional dan saya menghargai idenya. Namun, ini sangat terbatas pada catur. Saya menggunakan catur lebih banyak sebagai contoh untuk menggambarkan masalah umum (saya mungkin telah membuat lebih jelas dalam pertanyaan). Solusi yang Anda usulkan tidak dapat digunakan di domain lain dan seperti yang Anda katakan dengan benar, tidak ada cara untuk memperluasnya dengan objek bisnis baru (potongan). Memang ada banyak cara untuk memperpanjang catur dengan lebih banyak ....
lishaak