Очистити OOP спосіб відображення об’єкта на його презентаторі


13

Я створюю настільну гру (наприклад, шахи) на Java, де кожен твір є власним типом (наприклад Pawn, Rookтощо). Для частини програми GUI мені потрібно зображення для кожної з цих частин. Оскільки робити мислить, як

rook.image();

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

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


Яка мета мати власний клас для кожного типу шахової фігури? Я вважаю, що предмет, який займає позицію, колір і тип одиничного шматка. Для цього я, мабуть, мав клас Piece та два перерахунки (PieceColor, PieceType).
ПРИЙДАЙТЕ

@COMEFROM добре, що, очевидно, різні типи творів мають різну поведінку, тому повинен бути якийсь спеціальний код, який розрізняє скажімо граків та пішаків. Зважаючи на це, я б, як правило, мав стандартний класовий клас, який обробляє всі типи і використовує об'єкти стратегії для налаштування поведінки.
Жуль

@Jules, і яка користь матиме стратегію для кожного твору над тим, щоб мати окремий клас для кожного твору, що містить поведінку в собі?
лишаак

2
Однією з явних переваг відокремлення правил гри від об'єктивних об'єктів, які відбивають окремі фігури, є те, що вона негайно вирішує вашу проблему. Я б взагалі не змішував модель правила з державною моделлю взагалі, коли впроваджував покрокову настільну гру.
ВІД

2
Якщо ви не хочете відокремлювати правила, то ви можете визначити правила руху в шести об'єктах PieceType (не класах!). У будь-якому разі, я думаю, що найкращий спосіб уникнути проблем, з якими ви стикаєтеся, - це розділити проблеми та використовувати спадщину лише тоді, коли це дійсно корисно.
ВІД

Відповіді:


10

чи є чисте рішення OOP без instanceof, getClass () тощо, що дозволило б розширити цей вид?

Так, є.

Дозвольте запитати у вас це. У своїх поточних прикладах ви знаходите способи зіставлення типів деталей із зображеннями. Як це вирішує проблему переміщення твору?

Більш потужна техніка, ніж запитувати про тип - це слідувати Tell, не запитувати . Що робити, якщо кожен фрагмент взяв 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);

Ідея тут полягає у тому, щоб не брати на себе відповідальність за те, щоб робити щось, за що відповідають інші речі, не запитуючи про це та не приймаючи рішення на його основі. Замість цього тримайте посилання на щось, що знає, що з цим робити, і скажіть йому, щоб він робив щось, що ви знаєте.

Це дозволяє поліморфізм. Ви не дбаєте про те, з чим ви розмовляєте. Вам все одно, що це має сказати. Вам просто байдуже, що він може робити те, що вам потрібно зробити.

Хороша схема , яка тримає їх в окремих шарах, слід сказати-ні-запитати, і показує , як не пара шару до шару незаслужено знаходиться це :

введіть тут опис зображення

Він додає шар шару використання, який ми тут не використовували (і, безумовно, можемо додати), але ми дотримуємось тієї ж схеми, яку ви бачите в нижньому правому куті.

Ви також помітите, що презентатор не використовує спадщину. Тут використовується композиція. Спадщина має бути крайнім способом отримати поліморфізм. Я віддаю перевагу дизайнам, які надають перевагу використанню композиції та делегації. Це трохи більше набору клавіатури, але це набагато більше потужності.


2
Я думаю, що це, мабуть, хороша відповідь (+1), але я не переконаний, що ваше рішення є хорошим прикладом чистої архітектури: сутність Rook зараз дуже безпосередньо містить посилання на презентатора. Чи не такий тип зв’язків намагається запобігти чистій архітектурі? І те, що ваша відповідь, не зовсім уточнює: відображенням між сутностями та презентаторами зараз займається той, хто інстанціює сутність. Це, мабуть, більш елегантно, ніж альтернативні рішення, але це третє місце, яке слід змінювати, коли додаються нові об'єкти - тепер це не відвідувач, а фабрика.
амон

@amon, як я вже сказав, відсутній шар випадку використання. Террі Пратчетт назвав подібні речі "брехнею для дітей". Я намагаюся не створювати приклад, який є непосильним. Якщо ви думаєте, що мене потрібно відвезти до школи, де йдеться про чисту архітектуру, я запрошую вас взяти мене на завдання тут .
candied_orange

вибачте, ні, я не хочу вас навчати, я просто хочу зрозуміти цю модель архітектури краще. Я буквально щойно натрапив на поняття "чиста архітектура" та "шестикутна архітектура" менше місяця тому.
амон

@amon у такому випадку дай йому гарний погляд, а потім візьми мене на завдання. Мені б подобалося, якби ти. Я все ще сам розгадую частини цього. На даний момент я працюю над модернізацією проекту, керованого пітоном, до цього стилю. Критичний огляд чистої архітектури можна знайти тут .
candied_orange

1
@lishaak instantiation може статися в основному. Внутрішні шари не знають про зовнішні шари. Зовнішні шари знають лише про інтерфейси.
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)));

Я також зацікавлений у загальному вирішенні подібних проблем, коли мені потрібно додавати певну інформацію (не лише зображення) до потенційно зростаючого набору класів ".

Я думаю, вам не варто так зосереджуватися на заняттях . Швидше думайте з точки зору бізнес-об’єктів .

І загальне рішення - це відображення будь-якого типу. Хитрість IMHO полягає в тому, щоб перенести це зіставлення з коду на ресурс, який легше підтримувати.

Мій приклад робить це відображення за умовами, яке досить легко здійснити і не дозволяє додавати інформацію, що стосується перегляду, у бізнес-модель . З іншого боку, ви можете вважати це "прихованим" відображенням, оскільки воно ніде не виражається.

Інший варіант - розглянути це як окремий діловий випадок із власними шарами MVC, включаючи стійкий шар, що містить відображення.


Я розглядаю це як дуже практичне і невід'ємне рішення для цього конкретного сценарію. Я також зацікавлений у загальному вирішенні подібних проблем, коли мені потрібно додавати певну інформацію (не лише зображення) до потенційно зростаючого набору класів.
лишаак

3
@lishaak: загальний підхід полягає в наданні достатньої кількості метаданих у ваших бізнес-об’єктах, щоб загальне відображення елементів ресурсу чи користувальницького інтерфейсу можна зробити автоматично.
Док Браун

2

Я створив би окремий інтерфейс / клас перегляду для кожного фрагмента, який містить візуальну інформацію. Кожен із цих класів огляду має вказівник на свою модель / діловий аналог, який містить положення та правила гри.

Тому візьміть наприклад пішака:

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для відображення, а не a RookViewчи KingView? Або мені потрібно створити супровідний вигляд або презентатора відразу, коли я створюю нову п'єсу? Це в основному рішення @ CandiedOrange з переверненими залежностями. У цьому випадку PawnViewконструктор також міг би взяти a Pawn*, а не просто a Piece*.
амон

Так, мені шкода, конструктор PawnView взяв би пішака *. І вам не обов’язково одночасно створювати PawnView та пішака. Припустимо, є гра, яка може мати 100 пішаків, але лише 10 може бути візуальною одночасно; у цьому випадку ви можете повторно використовувати ломбарди для кількох пішаків.
Лассе Джейкобс

І я погоджуюся з рішенням @ CandiedOrange, я хоч би поділив свої 2 центи.
Лассе Джейкобс

0

Я би підійшов до цього, зробивши Pieceзагальне, де його параметр - це тип перерахування, який ідентифікує тип фрагмента, кожен фрагмент має посилання на один такий тип. Тоді користувальницький інтерфейс міг використовувати карту з перерахування, як і раніше:

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), ви повинні зауважити, що кожне значення enum не є просто незалежним об'єктом, але воно може мати і свій клас. Це означає, що ви можете використовувати об'єкти типу шматок як об’єкти стратегії, щоб замовити поведінку фрагмента:

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

Як ви бачили, параметр презентатора повинен передаватися на кожному пристрої (презентаційному шарі) по-різному. Це означає, що ваш презентаційний шар вирішить, як представити кожен твір. Що не так у цьому рішенні?


Ну спочатку, фрагмент повинен знати, що презентаційний шар є, і що він вимагає зображень. Що робити, якщо для деякого шару презентації не потрібні зображення? По-друге, фрагмент повинен бути ініційований шаром інтерфейсу користувача, оскільки фрагмент не може існувати без презентатора. Уявіть, що гра працює на сервері, де не потрібен інтерфейс користувача. Тоді ви не можете створити фрагмент, оскільки у вас немає інтерфейсу користувача.
лишаак

Ви також можете визначити репрезентатор як необов'язковий параметр.
Freshblood

0

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

Для цього можна скористатися рядком Fen . Рядок Fen - це в основному інформація про стан борту, і він дає поточні фрагменти та їх положення на борту. Таким чином, на вашій дошці може бути метод, який повертає поточний стан плати за допомогою рядка Fen, тоді ваш шар інтерфейсу може представляти дошку за своїм бажанням. Це насправді, як працюють сучасні шахові двигуни. Шахові двигуни - консольне застосування без GUI, але ми використовуємо їх через зовнішній графічний інтерфейс. Шаховий двигун спілкується з GUI через фен-струни та шахові позначення.

Ви запитуєте, що робити, якщо я додам новий твір? Нереально, що шахи введуть новий твір. Це було б величезними змінами у вашому домені. Тому дотримуйтесь принципу YAGNI.


Ну, це рішення, безумовно, функціональне, і я ціную цю ідею. Однак шахи дуже обмежені. Я використовував шахи більше як приклад, щоб проілюструвати загальну проблему (я, можливо, зробив це яснішим у питанні). Пропоноване вами рішення не може використовуватися в іншому домені, і як ви правильно сказали, немає можливості поширити його новими бізнес-об'єктами (штуками). Дійсно, існує багато способів розширити шахи на більшу кількість штук ....
Лишаак
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.