Чистый ООП способ отображения объекта на его презентатора

13

Я создаю настольную игру (например, шахматы) на Java, где каждая фигура имеет свой собственный тип (например Pawn, Rookи т. Д.). Для графической части приложения мне нужно изображение для каждой из этих частей. Поскольку делать думает, как

rook.image();

нарушает разделение пользовательского интерфейса и бизнес-логики, я создаю отдельного презентатора для каждого фрагмента, а затем сопоставляю типы фрагментов с соответствующими им презентаторами, например

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

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

Все идет нормально. Однако я чувствую, что осмотрительный гуру ООП будет недоволен вызовом 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 должен обновляться новым методом. Я хотел бы использовать мою систему в качестве фреймворка для настольной игры, где новые части могут быть добавлены с помощью простого процесса, когда пользователь фреймворка будет только обеспечивать реализацию как фрагмента, так и его презентатора, и просто подключит его к фреймворку. Мой вопрос: есть ли чистое решение ООП без instanceofи getClass()т. Д., Которое позволило бы для такого типа расширяемости?

lishaak
источник
Какова цель иметь собственный класс для каждого типа шахматной фигуры? Я считаю, что объект фигуры содержит позицию, цвет и тип отдельной фигуры. Вероятно, для этой цели у меня будет класс Piece и два перечисления (PieceColor, PieceType).
ПРИХОДИТ ОТ
@COMEFROM, конечно, разные типы фигур имеют разное поведение, поэтому должен быть какой-то специальный код, различающий, скажем, ладьи и пешки. Тем не менее, я бы предпочел иметь стандартный класс частей, который обрабатывает все типы и использует объекты Strategy для настройки поведения.
Жюль
@Jules, а какая выгода от наличия стратегии для каждого куска по сравнению с отдельным классом для каждого куска, содержащего поведение в себе?
Лишак
2
Одно явное преимущество отделения правил игры от объектов с состоянием, которые представляют отдельные части, состоит в том, что это немедленно решает вашу проблему. Я бы вообще не смешивал модель правил с моделью состояний при реализации пошаговой настольной игры.
ПРИХОДИТ ОТ
2
Если вы не хотите разделять правила, то вы можете определить правила движения в шести объектах PieceType (не в классах!). В любом случае, я думаю, что лучший способ избежать проблем, с которыми вы сталкиваетесь, - это разделять проблемы и использовать наследование только тогда, когда это действительно полезно.
ПРИХОДИТ ОТ

Ответы:

10

Есть ли чистое решение ООП без instanceof, getClass () и т. д., которое позволило бы для такого типа расширяемости?

Да, есть.

Позвольте мне спросить вас об этом. В ваших текущих примерах вы находите способы сопоставления типов элементов изображениям. Как это решает проблему перемещения части?

Более мощная техника, чем спрашивать о типе - это следовать Скажи, не спрашивай . Что делать, если каждый кусок взял 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 не использует наследование. Он использует композицию. Наследование должно быть последним средством получения полиморфизма. Я предпочитаю проекты, которые предпочитают использовать композицию и делегирование. Это немного больше клавиатурного набора, но это намного больше мощности.

candied_orange
источник
2
Я думаю, что это, вероятно, хороший ответ (+1), но я не уверен, что ваше решение является хорошим примером чистой архитектуры: объект Rook теперь очень прямо содержит ссылку на докладчика. Разве это не такая связь, которую Чистая Архитектура пытается предотвратить? И то, что ваш ответ не совсем разъясняет: сопоставление между сущностями и презентаторами теперь обрабатывается тем, кто создает экземпляр сущности. Это, вероятно, более элегантно, чем альтернативные решения, но это третье место, которое нужно изменить, когда добавляются новые сущности - теперь это не посетитель, а фабрика.
Амон
@amon, как я уже сказал, слой варианта использования отсутствует. Терри Пратчетт назвал подобные вещи "ложью детям". Я пытаюсь не создавать пример, который является подавляющим. Если вы думаете, что меня нужно отвести в школу, где речь идет о «Чистой архитектуре», я приглашаю вас взять меня с собой сюда .
candied_orange
извини, нет, я не хочу тебя учить, я просто хочу лучше понять эту архитектуру. Я буквально только что столкнулся с понятиями «чистой архитектуры» и «гексагональной архитектуры» менее месяца назад.
Амон
@amon в этом случае хорошенько взгляни, а потом приведи меня к задаче. Я бы с удовольствием, если бы ты это сделал. Я до сих пор сам разбираюсь в этом. В данный момент я работаю над обновлением проекта Python с меню. Критический обзор чистой архитектуры можно найти здесь .
candied_orange
1
Инстанциация @lishaak может произойти в основном. Внутренние слои не знают о внешних слоях. Внешние слои знают только об интерфейсах.
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 для каждой части, которая содержит визуальную информацию. Каждый из этих классов кусочков имеет указатель на свою модель / бизнес-аналог, который содержит позицию и правила игры фигуры.

Взять хотя бы пешку:

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

Это позволяет полностью разделить логику и пользовательский интерфейс. Вы можете передать указатель логической части на игровой класс, который будет обрабатывать перемещение фигур. Единственным недостатком является то, что создание экземпляров должно происходить в классе пользовательского интерфейса.

Лассе Джейкобс
источник
Хорошо, так скажем, у меня есть немного Piece* p. Как я знаю, что я должен создать PawnViewдля отображения, а не RookViewили KingView? Или мне нужно сразу создавать сопутствующее представление или докладчика, когда я создаю новую пьесу? Это было бы в основном решением @ CandiedOrange с инвертированными зависимостями. В этом случае PawnViewконструктор также может принимать, а Pawn*не просто Piece*.
Амон
Да, я извиняюсь, конструктор PawnView взял бы Pawn *. И вам не обязательно создавать PawnView и Pawn одновременно. Предположим, есть игра, которая может иметь 100 пешек, но только 10 могут быть визуальными одновременно, в этом случае вы можете повторно использовать пешки для нескольких пешек.
Лассе Джейкобс
И я согласен с решением @ CandiedOrange, хотя я бы хотел поделиться своими 2 центами.
Лассе Джейкобс
0

Я хотел бы подойти к этому, сделав Piecegeneric, где его параметром является тип перечисления, который идентифицирует тип элемента, причем каждый элемент имеет ссылку на один такой тип. Тогда пользовательский интерфейс может использовать карту из перечисления, как и раньше:

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

Это имеет два интересных преимущества:

Во-первых, применимо к большинству статически типизированных языков: если вы настраиваете свою доску в соответствии с типом фигуры, то вы не сможете вставить в нее неправильный тип фигуры.

Во-вторых, и, возможно, более интересно, если вы работаете в 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

Я прагматичный программист, и мне действительно все равно, что такое чистая или грязная архитектура. Я считаю, требования и это должно быть обработано простым способом.

Ваше требование заключается в том, что логика вашего шахматного приложения будет представлена ​​на разных уровнях представления (устройствах), таких как веб-приложение, мобильное приложение или даже консольное приложение, поэтому вам необходимо поддерживать эти требования. Вы можете предпочесть использовать очень разные цвета, штучные изображения на каждом устройстве.

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

Как вы видели, параметр презентатора должен передаваться на каждом устройстве (уровне представления) по-разному. Это означает, что ваш уровень представления будет решать, как представлять каждую часть. Что не так в этом решении?

Свежая кровь
источник
Ну, во-первых, часть должна знать, что там есть слой представления и что ему нужны изображения. Что если некоторый уровень представления не требует изображений? Во-вторых, кусок должен быть создан на уровне UI, так как кусок не может существовать без презентатора. Представьте, что игра запускается на сервере, где пользовательский интерфейс не нужен. Тогда вы не можете создать экземпляр, потому что у вас нет пользовательского интерфейса.
Лишак
Вы также можете определить представитель как необязательный параметр.
Freshblood
0

Есть еще одно решение, которое поможет вам полностью абстрагировать пользовательский интерфейс и доменную логику. Ваша доска должна быть открыта для вашего уровня пользовательского интерфейса, и ваш уровень пользовательского интерфейса может решить, как представлять фигуры и позиции.

Для этого вы можете использовать строку Fen . Строка фенов - это, в основном, информация о состоянии платы и она дает текущие фигуры и их позиции на доске. Таким образом, ваша доска может иметь метод, который возвращает текущее состояние платы через строку Fen, тогда ваш уровень пользовательского интерфейса может представлять доску по своему усмотрению. Вот как на самом деле работают современные шахматные движки. Шахматные движки - это консольные приложения без графического интерфейса, но мы используем их через внешний графический интерфейс. Шахматный движок связывается с графическим интерфейсом через строки и шахматную нотацию.

Вы спрашиваете, что, если я добавлю новый кусок? Нереально, что шахматы представят новую фигуру. Это было бы огромным изменением в вашем домене. Так что следуйте принципу ЯГНИ.

Свежая кровь
источник
Ну, это решение, безусловно, функционально, и я ценю эту идею. Однако это очень ограничено шахматами. Я использовал шахматы в качестве примера, чтобы проиллюстрировать общую проблему (возможно, я прояснил этот вопрос в этом вопросе). Решение, которое вы предлагаете, не может быть использовано в другом домене, и, как вы правильно сказали, нет способа расширить его новыми бизнес-объектами (частями). Есть действительно много способов расширить шахматы с помощью большего количества фигур ....
lishaak