Làm sạch cách OOP ánh xạ một đối tượng đến người trình bày của nó


13

Tôi đang tạo một trò chơi cờ (như cờ vua) trong Java, trong đó mỗi quân cờ là loại riêng của nó (như Pawn, Rookv.v.). Đối với phần GUI của ứng dụng, tôi cần một hình ảnh cho mỗi phần này. Kể từ khi làm như nghĩ

rook.image();

vi phạm sự tách biệt giữa UI và logic nghiệp vụ, tôi sẽ tạo một người thuyết trình khác nhau cho mỗi phần và sau đó ánh xạ các loại phần cho người thuyết trình tương ứng của họ như

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

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

Càng xa càng tốt. Tuy nhiên, tôi cảm thấy một bậc thầy OOP khôn ngoan sẽ cau mày khi gọi một getClass()phương thức và sẽ đề nghị sử dụng một khách truy cập chẳng hạn như thế này:

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;
  } 
}

Tôi thích giải pháp này (cảm ơn bạn, guru), nhưng nó có một nhược điểm đáng kể. Mỗi khi một loại mảnh mới được thêm vào ứng dụng, PieceVisitor cần được cập nhật với một phương thức mới. Tôi muốn sử dụng hệ thống của mình làm khung trò chơi hội đồng, nơi các phần mới có thể được thêm vào thông qua một quy trình đơn giản trong đó người dùng của khung sẽ chỉ cung cấp triển khai cả phần và người trình bày của nó, và chỉ cần cắm nó vào khung. Câu hỏi của tôi: có một giải pháp OOP sạch mà không có instanceof, getClass()vv sẽ cho phép loại mở rộng này?


Mục đích của việc có một lớp học riêng cho từng loại quân cờ là gì? Tôi nghĩ một đối tượng mảnh giữ vị trí, màu sắc và loại của một mảnh duy nhất. Tôi có thể có một lớp Piece và hai enum (PieceColor, PieceType) cho mục đích đó.
ĐẾN TỪ

@COMEFROM tốt, rõ ràng các loại mảnh khác nhau có hành vi khác nhau, vì vậy cần có một số mã tùy chỉnh để phân biệt giữa nói rooks và cầm đồ. Điều đó nói rằng, tôi thường muốn có một lớp mảnh tiêu chuẩn xử lý tất cả các loại và sử dụng các đối tượng Chiến lược để tùy chỉnh hành vi.
Jules

@Jules và lợi ích của việc có một chiến lược cho mỗi phần so với việc có một lớp riêng biệt cho mỗi phần có chứa hành vi trong chính nó là gì?
lishaak

2
Một lợi ích rõ ràng của việc tách các quy tắc của trò chơi khỏi các đối tượng có trạng thái lặp lại các phần riêng lẻ là nó giải quyết vấn đề của bạn ngay lập tức. Nói chung, tôi sẽ không trộn lẫn mô hình quy tắc với mô hình trạng thái khi thực hiện trò chơi theo lượt.
ĐẾN TỪ

2
Nếu bạn không muốn tách các quy tắc, thì bạn có thể xác định quy tắc di chuyển trong sáu đối tượng PieceType (không phải các lớp!). Dù sao, tôi nghĩ cách tốt nhất để tránh các loại vấn đề bạn gặp phải là tách biệt mối quan tâm và chỉ sử dụng quyền thừa kế khi nó thực sự hữu ích.
ĐẾN TỪ

Câu trả lời:


10

Có một giải pháp OOP sạch mà không có instanceof, getClass (), v.v ... sẽ cho phép loại mở rộng này không?

Có, có.

Hãy để tôi hỏi bạn điều này. Trong các ví dụ hiện tại của bạn, bạn đang tìm cách ánh xạ các loại mảnh thành hình ảnh. Làm thế nào để giải quyết vấn đề của một mảnh được di chuyển?

Một kỹ thuật mạnh hơn so với hỏi về loại là làm theo Tell, đừng hỏi . Điều gì sẽ xảy ra nếu mỗi phần lấy một PiecePresentergiao diện và nó trông như thế này:

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);
    } 
  }
}

Việc xây dựng sẽ trông giống như thế này:

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

Sử dụng sẽ trông giống như:

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

Ý tưởng ở đây là để tránh chịu trách nhiệm làm bất cứ điều gì mà những thứ khác chịu trách nhiệm bằng cách không hỏi về nó cũng như không đưa ra quyết định dựa trên nó. Thay vào đó hãy tham khảo một cái gì đó biết phải làm gì về một cái gì đó và bảo nó làm gì đó về những gì bạn biết.

Điều này cho phép đa hình. Bạn không CHĂM SÓC những gì bạn đang nói chuyện. Bạn không quan tâm những gì nó nói. Bạn chỉ cần quan tâm rằng nó có thể làm những gì bạn cần làm.

Một sơ đồ tốt giữ các lớp này trong các lớp riêng biệt, tuân theo câu hỏi không hỏi và chỉ ra làm thế nào để không ghép lớp này thành lớp một cách bất công là đây :

nhập mô tả hình ảnh ở đây

Nó thêm một lớp ca sử dụng mà chúng tôi chưa sử dụng ở đây (và chắc chắn có thể thêm) nhưng chúng tôi đang theo cùng một mẫu bạn nhìn thấy ở góc dưới bên phải.

Bạn cũng sẽ nhận thấy rằng Người trình bày không sử dụng quyền thừa kế. Nó sử dụng thành phần. Kế thừa nên là cách cuối cùng để có được đa hình. Tôi thích các thiết kế ủng hộ sử dụng thành phần và ủy quyền. Nó gõ bàn phím nhiều hơn một chút nhưng nó mạnh hơn rất nhiều.


2
Tôi nghĩ rằng đây có lẽ là một câu trả lời hay (+1), nhưng tôi không tin rằng giải pháp của bạn là một ví dụ điển hình về Kiến trúc sạch: thực thể Rook hiện đang trực tiếp giữ tham chiếu đến người thuyết trình. Đây có phải là loại khớp nối mà Kiến trúc sạch đang cố gắng ngăn chặn không? Và câu trả lời của bạn không hoàn toàn chính tả: ánh xạ giữa các thực thể và người thuyết trình hiện được xử lý bởi bất kỳ ai khởi tạo thực thể. Điều này có thể thanh lịch hơn các giải pháp thay thế, nhưng nó là nơi thứ ba được sửa đổi khi các thực thể mới được thêm vào - bây giờ, nó không phải là Khách truy cập mà là Nhà máy.
amon

@amon, như tôi đã nói, lớp tình huống sử dụng bị thiếu. Terry Pratchett gọi những điều này là "lời nói dối với trẻ em". Tôi đang cố gắng không tạo ra một ví dụ quá sức. Nếu bạn nghĩ rằng tôi cần được đưa đến trường nơi có Kiến trúc sạch, tôi mời bạn đưa tôi đến đây làm nhiệm vụ .
candied_orange

xin lỗi, không, tôi không muốn học bạn, tôi chỉ muốn hiểu mẫu kiến ​​trúc này tốt hơn. Tôi thực sự vừa bắt gặp các kiến ​​trúc sạch sẽ của tinh vi
amon

@amon trong trường hợp đó cho nó một cái nhìn tốt và sau đó đưa tôi đến nhiệm vụ. Tôi sẽ thích nó nếu bạn đã làm. Tôi vẫn đang tự mình tìm ra những phần này. Hiện tại tôi đang làm việc để nâng cấp một dự án python điều khiển theo phong cách này. Một đánh giá quan trọng của Kiến trúc sạch có thể được tìm thấy ở đây .
candied_orange

1
@lishaak khởi tạo có thể xảy ra trong chính. Các lớp bên trong không biết về các lớp bên ngoài. Các lớp bên ngoài chỉ biết về giao diện.
candied_orange

5

Thế còn

Mô hình của bạn (các lớp hình) cũng có một phương thức phổ biến mà bạn có thể cần trong ngữ cảnh khác:

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

Các hình ảnh được sử dụng để hiển thị một hình nhất định có được tên tệp bằng lược đồ đặt tên:

King-White.png
Queen-Black.png

Sau đó, bạn có thể tải hình ảnh phù hợp mà không cần truy cập thông tin về các lớp java.

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

Tôi cũng quan tâm đến giải pháp chung cho loại vấn đề này khi tôi cần đính kèm một số thông tin (không chỉ hình ảnh) vào một nhóm các lớp có khả năng phát triển. "

Tôi nghĩ bạn không nên tập trung vào các lớp học nhiều như vậy. Thay vì nghĩ về các đối tượng kinh doanh .

Và giải pháp chung là một bản đồ của bất kỳ loại nào. IMHO mẹo là di chuyển ánh xạ từ mã sang tài nguyên dễ bảo trì hơn.

Ví dụ của tôi thực hiện ánh xạ này theo quy ước khá dễ thực hiện và tránh thêm thông tin liên quan vào mô hình kinh doanh . Mặt khác, bạn có thể coi nó là một ánh xạ "ẩn" vì nó không được thể hiện ở bất cứ đâu.

Một tùy chọn khác là xem đây là một trường hợp kinh doanh riêng biệt với các lớp MVC riêng bao gồm một lớp kiên trì có chứa ánh xạ.


Tôi thấy đây là một giải pháp rất thực tế và thực tế cho kịch bản cụ thể này. Tôi cũng quan tâm đến giải pháp chung cho loại vấn đề này khi tôi cần đính kèm một số thông tin (không chỉ hình ảnh) vào một tập hợp các lớp có khả năng phát triển.
lishaak

3
@lishaak: cách tiếp cận chung ở đây là cung cấp đủ dữ liệu meta trong các đối tượng kinh doanh của bạn để việc ánh xạ chung tới tài nguyên hoặc các thành phần giao diện người dùng có thể được thực hiện tự động.
Doc Brown

2

Tôi sẽ tạo một lớp UI / view riêng cho từng phần chứa thông tin trực quan. Mỗi một trong các lớp pieceview này có một con trỏ tới mô hình / đối tác kinh doanh của nó chứa vị trí và quy tắc trò chơi của tác phẩm.

Vì vậy, hãy cầm đồ chẳng hạn:

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;
}

Điều này cho phép tách hoàn toàn logic và UI. Bạn có thể chuyển con trỏ mảnh logic đến một lớp trò chơi sẽ xử lý việc di chuyển các mảnh. Hạn chế duy nhất là việc khởi tạo sẽ phải xảy ra trong lớp UI.


OK, vì vậy hãy nói rằng tôi có một số Piece* p. Làm thế nào để tôi biết rằng tôi phải tạo một PawnViewđể hiển thị nó, chứ không phải là RookViewhay KingView? Hoặc tôi phải tạo một chế độ xem đi kèm hoặc người dẫn chương trình ngay lập tức bất cứ khi nào tôi tạo một Mảnh mới? Về cơ bản đó sẽ là giải pháp của @ CandiedOrange với các phụ thuộc được đảo ngược. Trong trường hợp đó, hàm PawnViewtạo cũng có thể lấy Pawn*, không chỉ a Piece*.
amon

Có, tôi xin lỗi, nhà xây dựng PawnView sẽ cầm đồ *. Và bạn không nhất thiết phải tạo PawnView và Pawn cùng một lúc. Giả sử có một trò chơi có thể có 100 Pawn nhưng chỉ có thể hiển thị 10 trò chơi cùng một lúc, trong trường hợp này bạn có thể sử dụng lại các hiệu cầm đồ cho nhiều Chân.
Lasse Jacobs

Và tôi đồng ý với giải pháp của @ CandiedOrange, mặc dù tôi sẽ chia sẻ 2 xu của mình.
Lasse Jacobs

0

Tôi sẽ tiếp cận điều này bằng cách tạo ra Piecechung chung, trong đó tham số của nó là kiểu liệt kê xác định loại mảnh, mỗi mảnh có tham chiếu đến một loại như vậy. Sau đó, UI có thể sử dụng bản đồ từ bảng liệt kê như trước:

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); }

Điều này có hai lợi thế thú vị:

Đầu tiên, áp dụng cho hầu hết các ngôn ngữ được nhập tĩnh: Nếu bạn đặt bảng của bạn với loại mảnh thành xpect, thì bạn không thể chèn loại mảnh sai vào đó.

Thứ hai, và có lẽ thú vị hơn, nếu bạn đang làm việc trong Java (hoặc các ngôn ngữ JVM khác), bạn nên lưu ý rằng mỗi giá trị enum không chỉ là một đối tượng độc lập mà còn có thể có lớp riêng. Điều này có nghĩa là bạn có thể sử dụng các đối tượng loại mảnh của mình làm đối tượng Chiến lược để khách hàng hành vi của mảnh:

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

(Rõ ràng việc triển khai thực tế cần phức tạp hơn thế, nhưng hy vọng bạn có ý tưởng)


0

Tôi là một lập trình viên thực dụng và tôi thực sự không quan tâm kiến ​​trúc sạch hay bẩn là gì. Tôi tin rằng các yêu cầu và nó nên được xử lý cách đơn giản.

Yêu cầu của bạn là logic ứng dụng cờ vua của bạn sẽ được thể hiện trên các lớp trình bày (thiết bị) khác nhau như trên web, ứng dụng di động hoặc thậm chí ứng dụng bảng điều khiển để bạn cần hỗ trợ các yêu cầu này. Bạn có thể thích sử dụng màu sắc rất khác nhau, hình ảnh mảnh trên mỗi thiết bị.

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; }
}

Như bạn đã thấy tham số của người thuyết trình nên được truyền trên mỗi thiết bị (lớp trình bày) khác nhau. Điều đó có nghĩa là lớp trình bày của bạn sẽ quyết định cách thể hiện từng phần. Điều gì là sai trong giải pháp này?


Đầu tiên, tác phẩm phải biết lớp trình bày ở đó và nó yêu cầu hình ảnh. Điều gì nếu một số lớp trình bày không yêu cầu hình ảnh? Thứ hai, mảnh phải được khởi tạo bởi lớp UI, vì một mảnh không thể tồn tại mà không có người trình bày. Hãy tưởng tượng rằng trò chơi chạy trên một máy chủ không cần UI. Sau đó, bạn không thể khởi tạo một phần vì bạn không có UI.
lishaak

Bạn có thể định nghĩa representer là tham số tùy chọn là tốt.
Freshblood

0

Có một giải pháp khác sẽ giúp bạn trừu tượng hóa hoàn toàn UI và tên miền logic. Bảng của bạn phải được tiếp xúc với lớp UI và lớp UI của bạn có thể quyết định cách thể hiện các phần và vị trí.

Để đạt được điều này, bạn có thể sử dụng chuỗi Fen . Chuỗi Fen về cơ bản là thông tin trạng thái của bảng và nó cung cấp cho các mảnh hiện tại và vị trí của chúng trên tàu. Vì vậy, bảng của bạn có thể có một phương thức trả về trạng thái hiện tại của bảng thông qua chuỗi Fen, sau đó lớp UI của bạn có thể đại diện cho bảng theo ý muốn. Đây thực sự là cách các động cơ cờ vua hiện tại hoạt động. Công cụ cờ vua là ứng dụng bàn điều khiển không có GUI nhưng chúng tôi sử dụng chúng thông qua GUI bên ngoài. Công cụ cờ vua giao tiếp với GUI thông qua chuỗi fen và ký hiệu cờ vua.

Bạn đang hỏi rằng nếu tôi thêm một mảnh mới thì sao? Nó không thực tế rằng cờ vua sẽ giới thiệu một quân cờ mới. Đó sẽ là thay đổi lớn trong miền của bạn. Vì vậy, hãy làm theo nguyên tắc YAGNI.


Vâng, giải pháp này chắc chắn là chức năng và tôi đánh giá cao ý tưởng. Tuy nhiên, nó rất hạn chế đối với cờ vua. Tôi đã sử dụng cờ vua nhiều hơn như một ví dụ để minh họa vấn đề chung (tôi có thể đã làm cho điều đó rõ ràng hơn trong câu hỏi). Giải pháp bạn đề xuất không thể được sử dụng trong tên miền khác và như bạn nói chính xác, không có cách nào để mở rộng nó với các đối tượng kinh doanh mới (phần). Thực sự có nhiều cách để mở rộng cờ vua với nhiều quân cờ hơn ....
lishaak
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.