객체를 발표자에게 매핑하는 깔끔한 OOP 방식


13

Java로 보드 게임 (예 : 체스)을 만들고 있는데 각 조각은 자체 유형 (예 Pawn: Rook등)입니다. 응용 프로그램의 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 솔루션이 instanceof, getClass()등 확장 성이 종류를 허용 것인가?


각 유형의 체스 피스에 대한 자체 클래스를 갖는 목적은 무엇입니까? 나는 조각 개체가 단일 조각의 위치, 색상 및 유형을 보유한다고 생각합니다. 아마도 그 목적을 위해 Piece 클래스와 두 개의 열거 형 (PieceColor, PieceType)이있을 것입니다.
에서 오는

@COMEFROM 글쎄, 분명히 다른 유형의 조각은 다른 행동을 가지므로 루크와 폰을 구별하는 사용자 정의 코드가 필요합니다. 즉, 일반적으로 모든 유형을 처리하고 전략 객체를 사용하여 동작을 사용자 정의하는 표준 피스 클래스를 사용하려고합니다.
Jules

@ Jules와 행동 자체를 포함하는 모든 조각에 대해 별도의 클래스를 갖는 것보다 각 조각에 대한 전략을 갖는 이점은 무엇입니까?
lishaak

2
개별 규칙을 반복하는 상태 저장 개체와 게임 규칙을 분리하면 얻을 수있는 분명한 이점은 문제를 즉시 해결한다는 것입니다. 턴 기반 보드 게임을 구현할 때는 일반적으로 규칙 모델을 상태 모델과 전혀 혼합하지 않습니다.
에서 오는

2
규칙을 분리하지 않으려면 클래스가 아닌 6 개의 PieceType 객체에서 이동 규칙을 정의 할 수 있습니다. 어쨌든, 나는 당신이 직면 한 종류의 문제를 피하는 가장 좋은 방법은 우려를 분리하고 실제로 유용 할 때만 상속을 사용하는 것입니다.
에서 오는

답변:


10

이러한 종류의 확장 성을 허용하는 getClass () 등 instanceof가없는 깨끗한 OOP 솔루션이 있습니까?

그렇습니다.

이걸 물어 보도록하겠습니다. 현재 예제에서 조각 유형을 이미지에 매핑하는 방법을 찾고 있습니다. 이것은 조각이 움직이는 문제를 어떻게 해결합니까?

타입에 대해 묻는 것보다 더 강력한 기술은 Tell, do n't ask 를 따르는 것 입니다. 각 부분이 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)이라고 생각하지만, 귀하의 솔루션이 Clean Architecture의 좋은 예라고 확신하지 못합니다. Rook 엔티티는 이제 발표자에 대한 참조를 매우 직접 보유합니다. 클린 아키텍쳐가 이런 종류의 커플 링을 방지하려고하지 않습니까? 그리고 당신의 대답이 명확하지 않은 것 : 엔티티와 발표자 간의 매핑은 이제 엔티티를 인스턴스화하는 사람이 처리합니다. 이것은 대체 솔루션보다 더 우아하지만 새로운 엔터티가 추가 될 때 세 번째로 수정됩니다. 이제는 방문자가 아니라 팩토리입니다.
amon

@amon, 내가 말했듯이 유스 케이스 레이어가 없습니다. Terry Pratchett는 이런 종류의 것들을 "어린이에게 거짓말"이라고했습니다. 압도적 인 예를 만들려고하지 않습니다. Clean Architecture와 관련하여 학교에 데려다 주어야한다고 생각한다면 여기 에서 과제를 시작하도록 초대합니다 .
candied_orange

죄송합니다. 학교에 가고 싶지 않습니다.이 아키텍처 패턴을 더 잘 이해하고 싶습니다. 나는 문자 그대로 한 달 전에“청결한 건축”과“육각 건축”개념을 발견했습니다.
amon

이 경우 @amon은 잘 보이게하고 작업으로 데려갑니다. 당신이 한 경우 그것을 사랑합니다. 나는 아직도 이것의 일부를 스스로 알아 내고있다. 현재 메뉴 기반 파이썬 프로젝트를이 스타일로 업그레이드하려고합니다. Clean Architecture에 대한 비판적 검토는 여기 에서 찾을 수 있습니다 .
candied_orange

1
@lishaak 인스턴스화는 메인에서 발생할 수 있습니다. 내부 레이어는 외부 레이어에 대해 알지 못합니다. 외부 레이어는 인터페이스에 대해서만 알고 있습니다.
candied_orange 22시 02 분

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 계층 이있는 별도의 비즈니스 사례 로 보는 것입니다.


나는 이것이이 특정 시나리오에 대한 매우 실용적이고 철저한 솔루션이라고 생각합니다. 또한 잠재적으로 증가하는 클래스 집합에 이미지뿐만 아니라 일부 정보를 첨부해야 할 때 이러한 종류의 문제에 대한 일반적인 해결책에 관심이 있습니다.
lishaak

3
@lishaak : 여기서 일반적인 접근 방식은 비즈니스 개체에 충분한 메타 데이터 를 제공 하여 리소스 또는 사용자 인터페이스 요소에 대한 일반적인 매핑을 자동으로 수행하는 것입니다.
Doc Brown

2

시각적 정보가 포함 된 각 부분에 대해 별도의 UI /보기 클래스를 만듭니다. 이러한 각 조각 뷰 클래스에는 조각의 위치 및 게임 규칙이 포함 된 모델 / 비즈니스 대응 물에 대한 포인터가 있습니다.

예를 들어 폰을 가져 가십시오.

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을 표시하기 위해, 그리고 RookViewKingView? 아니면 새 작품을 만들 때마다 즉시 동반자보기 또는 발표자를 만들어야합니까? 그것은 기본적으로 의존성이 반전 된 @CandiedOrange의 솔루션 일 것입니다. 이 경우 PawnView생성자는 Pawn*,뿐만 아니라을 사용할 수 Piece*있습니다.
amon

예, 죄송합니다. PawnView 생성자는 Pawn *을 사용합니다. 그리고 반드시 PawnView와 Pawn을 동시에 만들 필요는 없습니다. 100 Pawn을 가질 수 있지만 한 번에 10 개만 시각적으로 볼 수있는 게임이 있다고 가정합니다.이 경우 여러 Pawn에 대해 폰보기를 재사용 할 수 있습니다.
Lasse Jacobs

그리고 나는 2 센트를 공유하지만 @CandiedOrange의 솔루션에 동의합니다.
Lasse Jacobs

0

나는 Piece그것의 매개 변수가 조각의 유형을 식별하는 열거의 유형 인 generic 을 만들어서 접근 할 것입니다 . 각 조각은 그러한 유형 중 하나에 대한 참조를 가지고 있습니다. 그런 다음 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

나는 실용적인 프로그래머이며 실제로 깨끗하거나 더러운 아키텍처가 무엇인지 상관하지 않습니다. 요구 사항을 믿고 간단한 방법으로 처리해야합니다.

요구 사항은 체스 응용 프로그램 논리가 웹, 모바일 응용 프로그램 또는 콘솔 응용 프로그램과 같은 다른 프레젠테이션 계층 (장치)에 표시되므로 이러한 요구 사항을 지원해야합니다. 각 장치마다 매우 다른 색상, 조각 이미지를 사용하는 것이 좋습니다.

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 ​​레이어에서 작품을 인스턴스화해야합니다. UI가 필요없는 서버에서 게임을 실행한다고 상상해보십시오. 그런 다음 UI가 없기 때문에 조각을 인스턴스화 할 수 없습니다.
lishaak

표현자를 선택적 매개 변수로 정의 할 수도 있습니다.
Freshblood

0

UI와 도메인 로직을 완전히 추상화하는 데 도움이되는 또 다른 솔루션이 있습니다. 보드는 UI 레이어에 노출되어야하며 UI 레이어는 조각과 위치를 표현하는 방법을 결정할 수 있습니다.

이를 위해 Fen string을 사용할 수 있습니다 . Fen string은 기본적으로 보드 상태 정보이며 현재 조각 및 보드상의 위치를 ​​제공합니다. 따라서 보드는 Fen 문자열을 통해 보드의 현재 상태를 반환하는 메소드를 가질 수 있으며 UI 레이어는 원하는대로 보드를 나타낼 수 있습니다. 이것이 실제로 현재 체스 엔진이 작동하는 방식입니다. 체스 엔진은 GUI가없는 콘솔 응용 프로그램이지만 외부 GUI를 통해 사용합니다. 체스 엔진은 펜 문자열과 체스 표기법을 통해 GUI와 통신합니다.

새 작품을 추가하면 어떻게됩니까? 체스가 새로운 작품을 소개하는 것은 현실적이지 않습니다. 그것은 당신의 도메인에 큰 변화가 될 것입니다. YAGNI 원칙을 따르십시오.


글쎄,이 솔루션은 확실히 기능적이며 아이디어에 감사드립니다. 그러나 체스에만 국한됩니다. 나는 일반적인 문제를 설명하기 위해 체스를 더 예로 사용했습니다 (질문에서 더 명확하게 만들었을 수도 있음). 제안한 솔루션은 다른 도메인에서 사용할 수 없으며 올바르게 말하면 새 비즈니스 오브젝트 (피스)로 확장 할 수있는 방법이 없습니다. 실제로 더 많은 조각으로 체스를 확장하는 방법은 여러 가지가 있습니다 ....
lishaak
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.