将对象映射到演示者的干净的OOP方法


13

我正在用Java创建一个棋盘游戏(例如国际象棋),其中每个棋子都是自己的类型(例如PawnRook等等)。对于应用程序的GUI部分,我需要为每个这些部分提供一个图像。既然做的像

rook.image();

违反了UI和业务逻辑的分离,我将为每个作品创建一个不同的演示者,然后将作品类型映射到其相应的演示者,例如

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

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

到目前为止,一切都很好。但是,我认为谨慎的OOP专家在调用getClass()方法时会皱眉,并建议像这样使用访客:

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

我喜欢这种解决方案(谢谢您,老师),但是它有一个明显的缺点。每次将新的样片类型添加到应用程序时,都需要使用新的方法来更新PieceVisitor。我想将我的系统用作棋盘游戏框架,可以通过一个简单的过程添加新作品,其中框架的用户只能提供作品及其演示者的实现,并将其插入框架中即可。我的问题:是否有没有等等的干净的OOP解决方案instanceofgetClass()它将允许这种可扩展性?


对每种棋子都有自己的类的目的是什么?我认为一块物体可以容纳一块的位置,颜色和类型。为此,我可能有一个Piece类和两个枚举(PieceColor,PieceType)。

@COMEFROM很好,很明显,不同类型的片段具有不同的行为,因此需要一些自定义代码来区分说白话和典当。就是说,我通常宁愿有一个标准件类来处理所有类型,并使用Strategy对象自定义行为。
Jules

@Jules,为每件事情制定策略,而不是为每件包含行为本身的单独的类,有什么好处?
lishaak

2
将游戏规则与代表单个作品的有状态对象分开的一个明显好处是,它可以立即解决您的问题。在实施基于回合的棋盘游戏时,我通常不会将规则模型与状态模型混在一起。

2
如果不想分隔规则,则可以在六个PieceType对象(不是类!)中定义移动规则。无论如何,我认为避免出现此类问题的最佳方法是分离关注点并仅在真正有用时才使用继承。

Answers:


10

是否有一个没有instanceof,getClass()等的干净的OOP解决方案,它将允许这种可扩展性?

就在这里。

让我问你一下。在当前示例中,您正在寻找将作品类型映射到图像的方法。这如何解决一块被搬走的问题?

比询问类型更有效的技术是遵循“告诉,不要询问”。如果每个片段都采用一个PiecePresenter界面并且看起来像这样,该怎么办:

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

构造看起来像这样:

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

使用看起来像:

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

这里的想法是通过不询问也不基于它做出决定来避免对其他事情负责的事情承担责任。而是持有对某事的引用,该事知道该怎么办,并告诉它对您所知道的事做某事。

这允许多态性。你不在乎你在说什么。你不在乎它怎么说。您只关心它可以完成您需要做的事情。

一个很好的图将它们保持在单独的层中,紧随其后的是询问,并说明了如何不将层与层不公平地耦合在一起,这样的

在此处输入图片说明

它添加了一个我们这里未曾使用过的用例层(当然可以添加),但是我们遵循的是您在右下角看到的相同模式。

您还会注意到Presenter不使用继承。它使用组成。继承应该是获得多态性的最后手段。我更喜欢使用合成和委托的设计。键盘输入更多,但功能更多。


2
我认为这可能是一个不错的答案(+1),但是我不相信您的解决方案是“干净架构”的一个很好的例子:Rook实体现在非常直接地引用了一个演示者。清洁架构正在努力防止这种耦合吗?而且您的答案还不是很清楚:实体和演示者之间的映射现在由实例化实体的人来处理。这可能比替代解决方案更为优雅,但是当添加新实体时,它是要修改的第三位–现在,它不是访客,而是工厂。
阿蒙

就像我说的,@ amon缺少用例层。特里·普拉切特(Terry Pratchett)称这类事情为“孩子的谎言”。我试图不创建一个压倒性的例子。如果您认为我需要在清洁建筑方面上学,我邀请您带我去这里上课。
candied_orange

抱歉,不,我不想上学,我只是想更好地了解这种架构模式。不到一个月前,我确实遇到了“清洁建筑”和“六角建筑”的概念。
阿蒙

在这种情况下,@ amon会给它一个好看的外观,然后带我去执行任务。如果您愿意,我会喜欢的。我仍然自己弄清楚其中的一部分。目前,我正在将菜单驱动的python项目升级为这种样式。可以在这里找到有关Clean Architecture的评论。
candied_orange

1
@lishaak实例化可以在main中进行。内层不了解外层。外层仅了解接口。
candied_orange

5

那个怎么样:

您的模型(图形类)也具有在其他上下文中可能还需要的通用方法:

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

用于显示特定图形的图像通过命名模式获取文件名:

King-White.png
Queen-Black.png

然后,您可以加载适当的映像,而无需访问有关Java类的信息。

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

当我需要将一些信息(不仅是图像)附加到可能增长的类集上时,我也对解决此类问题的通用解决方案感兴趣。”

我认为您不应过多地关注课程。宁可考虑业务对象

通用解决方案是任何类型的映射。恕我直言,诀窍是将映射从代码移动到更易于维护的资源。

我的示例按照约定进行了这种映射,这很容易实现,并且避免将与视图相关的信息添加到业务模型中。另一方面,您可以将其视为“隐藏”映射,因为它没有在任何地方表达。

另一个选择是将其视为具有自己的MVC层(包括包含映射的持久层)的独立业务案例


对于这种特殊情况,我认为这是一种非常实用且切实可行的解决方案。当我需要将一些信息(不仅是图像)附加到可能增长的类集时,我也对此类问题的一般解决方案感兴趣。
lishaak

3
@lishaak:这里的常规方法是在业务对象中提供足够的元数据,以便可以自动完成到资源或用户界面元素的常规映射。
布朗

2

我将为每个包含视觉信息的片段创建一个单独的UI / view类。这些pieceview类中的每个类都有一个指向其模型/业务对应项的指针,该指针包含该作品的位置和游戏规则。

因此,以典当为例:

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

这样可以完全分离逻辑和UI。您可以将逻辑棋子指针传递给将处理棋子移动的游戏类。唯一的缺点是实例化必须在UI类中进行。


好,让我们说我有一些Piece* p。我怎么知道我必须创建一个PawnView才能显示它,而不是RookViewor KingView?还是每次创建新作品时都必须立即创建一个伴随视图或演示者?那基本上是@CandiedOrange的解决方案,其中的依赖项倒置了。在这种情况下,PawnView构造函数也可以采用a Pawn*,而不仅仅是a Piece*
阿蒙

是的,很抱歉,PawnView构造函数将使用Pawn *。您不必同时创建PawnView和Pawn。假设有一个游戏可以拥有100个Pawn,但一次只能显示10个,在这种情况下,您可以将Pawnviews重复用于多个Pawn。
拉瑟·雅各布斯

我同意@CandiedOrange的解决方案,尽管我愿意分享2美分。
拉瑟·雅各布斯

0

我将通过Piece泛型来实现这一点,其中其参数是标识段类型的枚举的类型,每个段都引用一个这样的类型。然后,UI可以像以前一样使用枚举中的映射:

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

这有两个有趣的优点:

首先,适用于大多数静态类型的语言:如果使用xpect的零件类型对板进行参数化,则无法在其中插入错误的零件。

其次,也许更有趣的是,如果您使用Java(或其他JVM语言)工作,则应注意,每个枚举值不仅是一个独立的对象,而且也可以有自己的类。这意味着您可以将作品类型对象用作策略对象,以了解作品的行为:

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

(显然,实际的实现需要比这更复杂,但是希望您能理解这个想法)


0

我是一个务实的程序员,我真的不在乎什么是干净的或肮脏的体系结构。我相信需求,应该以简单的方式来处理。

您的要求是您的国际象棋应用程序逻辑将在不同的表示层(设备)上表示,例如在Web,移动应用程序甚至是控制台应用程序上,因此您需要支持这些要求。您可能更喜欢使用非常不同的颜色,在每个设备上分割图像。

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

如您所见,presenter参数应该在每个设备(表示层)上以不同的方式传递。这意味着您的表示层将决定如何表示每个片段。此解决方案有什么问题?


首先,作品必须知道表示层在那里,并且需要图像。如果某些表示层不需要图像怎么办?其次,该片段必须由UI层实例化,因为没有演示者就无法存在该片段。想象一下,游戏运行在不需要UI的服务器上。然后,由于没有UI,因此无法实例化片段。
lishaak

您也可以将表示符定义为可选参数。
Freshblood

0

还有另一种解决方案可以帮助您完全抽象UI和域逻辑。电路板应该暴露在UI层中,并且UI层可以决定如何表示零件和位置。

为了实现这一点,可以使用Fen string。芬弦基本上是板状态信息,它提供当前棋子及其在木板上的位置。因此,您的电路板可以具有通过Fen字符串返回电路板当前状态的方法,然后您的UI层可以按需要表示电路板。实际上,这就是当前国际象棋引擎的工作方式。国际象棋引擎是没有GUI的控制台应用程序,但是我们通过外部GUI使用它们。国际象棋引擎通过分字符串和国际象棋符号与GUI进行通信。

您在问,如果我要增加一块呢?下象棋引入新棋子是不现实的。那将是您域中的巨大变化。因此,请遵循YAGNI原则。


好吧,该解决方案肯定可以起作用,我对此表示赞赏。但是,它仅限于国际象棋。我以国际象棋为例来说明一般性问题(我可能在问题中更清楚了这一点)。您提出的解决方案不能在其他领域中使用,并且正如您正确地说的那样,没有办法用新的业务对象(件)扩展它。确实,有很多方法可以扩大象棋的棋子
数量。...– lishaak
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.