ทำความสะอาดวิธีการทำแผนที่ 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 และสอง enums (PieceColor, PieceType) เพื่อจุดประสงค์นั้น
จาก

@COMEFROM เป็นอย่างดีชิ้นส่วนที่แตกต่างกันอย่างเห็นได้ชัดมีพฤติกรรมที่แตกต่างกันดังนั้นจึงจำเป็นต้องมีรหัสที่กำหนดเองที่แยกความแตกต่างระหว่างคำว่า rooks และเบี้ย ที่กล่าวว่าโดยทั่วไปฉันจะค่อนข้างมีคลาสชิ้นมาตรฐานที่จัดการทุกประเภทและใช้วัตถุกลยุทธ์เพื่อปรับแต่งพฤติกรรม
จูลส์

@Jules และสิ่งที่เป็นประโยชน์ของการมีกลยุทธ์สำหรับแต่ละชิ้นส่วนที่มีชั้นแยกสำหรับทุกชิ้นที่มีพฤติกรรมในตัวเอง?
lishaak

2
ข้อดีอย่างหนึ่งที่ชัดเจนของการแยกกฎของเกมออกจากวัตถุที่เป็นมลรัฐที่นำเสนอแต่ละชิ้นคือการแก้ปัญหาของคุณทันที โดยทั่วไปฉันจะไม่ผสมกฏกับโมเดลของรัฐเมื่อใช้เกมกระดานแบบใช้เทิร์นเบส
จาก

2
หากคุณไม่ต้องการแยกกฎคุณสามารถกำหนดกฎการเคลื่อนไหวในวัตถุหกชิ้นของ PieceType (ไม่ใช่คลาส!) อย่างไรก็ตามฉันคิดว่าวิธีที่ดีที่สุดในการหลีกเลี่ยงปัญหาที่คุณเผชิญคือการแยกข้อกังวลและใช้การสืบทอดเมื่อมันมีประโยชน์จริงๆเท่านั้น
จาก

คำตอบ:


10

จะมีวิธีการแก้ปัญหา OOP สะอาดโดยไม่ต้อง 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);

ความคิดที่นี่คือการหลีกเลี่ยงความรับผิดชอบในการทำสิ่งที่มีความรับผิดชอบโดยไม่ถามเกี่ยวกับมันหรือตัดสินใจบนพื้นฐานของมัน แทนที่จะมีการอ้างอิงถึงสิ่งที่รู้ว่าจะทำอะไรเกี่ยวกับบางสิ่งบางอย่างและบอกให้ทำบางสิ่งบางอย่างเกี่ยวกับสิ่งที่คุณรู้

สิ่งนี้ทำให้เกิด polymorphism คุณไม่สนใจสิ่งที่คุณกำลังพูดถึง คุณไม่สนใจว่าจะพูดอะไร คุณแค่แคร์ว่ามันสามารถทำสิ่งที่คุณต้องการได้

แผนภาพที่ดีที่ช่วยให้เหล่านี้ในชั้นที่แยกจากกันดังต่อไปนี้บอกอย่าถามและแสดงให้เห็นวิธีการที่จะไม่ได้ชั้นสองสามชั้นอย่างไม่เป็นธรรมเป็นนี้ :

ป้อนคำอธิบายรูปภาพที่นี่

มันเพิ่มชั้นกรณีการใช้งานที่เราไม่ได้ใช้ที่นี่ (และสามารถเพิ่มได้อย่างแน่นอน) แต่เรากำลังติดตามรูปแบบเดียวกันกับที่คุณเห็นที่มุมล่างขวา

คุณจะสังเกตเห็นว่าผู้นำเสนอไม่ได้ใช้การสืบทอด มันใช้องค์ประกอบ การสืบทอดควรเป็นทางเลือกสุดท้ายในการรับความแตกต่าง ฉันชอบการออกแบบที่ชอบการใช้องค์ประกอบและการมอบหมาย มันเป็นคีย์บอร์ดที่พิมพ์ได้มากกว่า แต่ก็มีพลังมากกว่า


2
ฉันคิดว่านี่อาจเป็นคำตอบที่ดี (+1) แต่ฉันไม่เชื่อว่าโซลูชันของคุณเป็นตัวอย่างที่ดีของ Clean Architecture: ตอนนี้เอนทิตีโกงใช้การอ้างอิงโดยตรงกับผู้นำเสนอโดยตรง ไม่ใช่การแต่งงานกันแบบนี้ที่ Clean Architecture พยายามป้องกันหรือไม่ และสิ่งที่คำตอบของคุณไม่ชัดเจนนัก: การทำแผนที่ระหว่างเอนทิตีและผู้นำเสนอได้รับการจัดการโดยใครก็ตามที่สร้างเอนทิตีทันที นี่น่าจะหรูหรากว่าโซลูชันทางเลือก แต่เป็นสถานที่ที่สามที่จะแก้ไขเมื่อมีการเพิ่มเอนทิตีใหม่ - ตอนนี้ไม่ใช่ผู้เยี่ยมชม แต่เป็นโรงงาน
amon

@amon อย่างที่ฉันบอกชั้นกรณีการใช้หายไป Terry Pratchett เรียกสิ่งนี้ว่า "โกหกเด็ก" ฉันพยายามที่จะไม่สร้างตัวอย่างที่ครอบงำ หากคุณคิดว่าฉันต้องถูกนำไปยังโรงเรียนที่มันมาถึงการทำความสะอาดสถาปัตยกรรมผมขอเชิญคุณพาฉันไปที่งานที่นี่
candied_orange

ขอโทษฉันไม่ต้องการเรียนคุณฉันแค่ต้องการเข้าใจรูปแบบสถาปัตยกรรมนี้ให้ดีขึ้น แท้จริงฉันเพิ่งเจอแนวคิด "สถาปัตยกรรมที่สะอาด" และ "สถาปัตยกรรมหกเหลี่ยม" น้อยกว่าหนึ่งเดือนที่ผ่านมา
amon

@amon ในกรณีนี้ให้ดูดีและนำฉันไปทำภารกิจ ฉันจะรักถ้าคุณทำ ฉันยังคงหาข้อมูลบางส่วนของตัวฉันเอง ในขณะนี้ฉันกำลังทำงานเพื่ออัปเกรดโครงการที่ใช้เมนูหลามเป็นสไตล์นี้ การตรวจสอบที่สำคัญของสถาปัตยกรรมที่สะอาดสามารถพบได้ที่นี่
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)));

ฉันยังสนใจในการแก้ปัญหาทั่วไปสำหรับปัญหาประเภทนี้เมื่อฉันต้องการแนบข้อมูลบางอย่าง (ไม่เฉพาะภาพ) กับชุดเรียนที่มีแนวโน้มเพิ่มขึ้น "

ฉันคิดว่าคุณไม่ควรมุ่งเน้นไปที่คลาสนั้นมาก แต่คิดในแง่ของวัตถุธุรกิจ

และวิธีแก้ปัญหาทั่วไปคือการทำแผนที่ทุกชนิด 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เพื่อแสดงและไม่ได้เป็นRookViewหรือKingView? หรือฉันต้องสร้างมุมมองหรือพรีเซนเตอร์ที่แนบมาทันทีเมื่อใดก็ตามที่ฉันสร้างงานชิ้นใหม่? โดยทั่วไปแล้วจะเป็นวิธีการแก้ปัญหาของ @ CandiedOrange เมื่อมีการอ้างอิงกลับกัน ในกรณีที่PawnViewคอนสตรัคนอกจากนี้ยังอาจจะใช้ไม่เพียงPawn* Piece*
amon

ใช่ฉันขอโทษตัวสร้าง PawnView ใช้ Pawn * และคุณไม่จำเป็นต้องสร้าง PawnView และ Pawn ในเวลาเดียวกัน สมมติว่ามีเกมที่สามารถมี 100 เบี้ย แต่มีเพียง 10 คนเท่านั้นที่สามารถมองเห็นได้ในคราวเดียวในกรณีนี้คุณสามารถใช้การจำนำอีกครั้งสำหรับการจำนำหลาย ๆ
Lasse Jacobs

และฉันเห็นด้วยกับวิธีการแก้ปัญหาของ @ CandiedOrange ฉันเพียง แต่ฉันจะแบ่งปัน 2 เซ็นต์ของฉัน
Lasse Jacobs

0

ฉันจะเข้าใกล้สิ่งนี้ด้วยการสร้างPiecegeneric โดยที่พารามิเตอร์เป็นประเภทของการแจงนับที่ระบุประเภทของชิ้นส่วนแต่ละชิ้นมีการอ้างอิงถึงประเภทดังกล่าว จากนั้น 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 อื่น ๆ ) คุณควรทราบว่าแต่ละค่า 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; }
}

ดังที่คุณเห็นพารามิเตอร์ของผู้นำเสนอควรถูกส่งผ่านไปยังอุปกรณ์แต่ละตัว (เลเยอร์การนำเสนอ) แตกต่างกัน นั่นหมายถึงเลเยอร์การนำเสนอของคุณจะเป็นผู้ตัดสินว่าจะนำเสนอแต่ละชิ้นอย่างไร มีอะไรผิดปกติในการแก้ปัญหานี้?


ก่อนอื่นต้องรู้ว่าเลเยอร์การนำเสนออยู่ที่นั่นและต้องมีภาพ เกิดอะไรขึ้นถ้าเลเยอร์การนำเสนอบางรายการไม่ต้องการรูปภาพ ประการที่สองชิ้นจะต้องมีการยกตัวอย่างโดยชั้น UI เนื่องจากชิ้นส่วนไม่สามารถอยู่ได้โดยไม่ต้องมีผู้นำเสนอ ลองจินตนาการว่าเกมทำงานบนเซิร์ฟเวอร์ที่ไม่จำเป็นต้องมี UI จากนั้นคุณไม่สามารถยกตัวอย่างชิ้นส่วนได้เนื่องจากคุณไม่มี UI
lishaak

คุณสามารถกำหนดศูนย์การตอบโต้เป็นพารามิเตอร์ทางเลือกได้เช่นกัน
Freshblood

0

มีวิธีแก้ไขปัญหาอื่นที่จะช่วยให้คุณนามธรรม UI และตรรกะโดเมนอย่างสมบูรณ์ บอร์ดของคุณควรจะสัมผัสกับเลเยอร์ UI ของคุณและเลเยอร์ UI ของคุณสามารถตัดสินใจได้ว่าจะนำเสนอชิ้นส่วนและตำแหน่งต่างๆได้อย่างไร

เพื่อให้บรรลุนี้คุณสามารถใช้สตริง Fen สตริง Fen เป็นข้อมูลสถานะกระดานโดยทั่วไปและให้ชิ้นส่วนปัจจุบันและตำแหน่งบนกระดาน ดังนั้นบอร์ดของคุณสามารถมีวิธีที่ส่งคืนสถานะปัจจุบันของบอร์ดผ่านสตริง Fen จากนั้นเลเยอร์ UI ของคุณสามารถแสดงบอร์ดได้ตามต้องการ นี่เป็นวิธีการทำงานของเอ็นจิ้นหมากรุกปัจจุบัน เอ็นจิ้นหมากรุกเป็นแอปพลิเคชั่นคอนโซลที่ไม่มี GUI แต่เราใช้มันผ่าน GUI ภายนอก เอ็นจิ้นหมากรุกสื่อสารกับ GUI ผ่านสายเฟนและสัญกรณ์หมากรุก

คุณกำลังถามว่าถ้าฉันเพิ่มชิ้นใหม่? ไม่จริงที่หมากรุกจะแนะนำชิ้นใหม่ นั่นจะเป็นการเปลี่ยนแปลงครั้งใหญ่ในโดเมนของคุณ ดังนั้นทำตามหลักการ YAGNI


วิธีแก้ปัญหานี้ใช้งานได้จริงและฉันขอขอบคุณแนวคิดนี้ อย่างไรก็ตามมันมีข้อ จำกัด มากในการเล่นหมากรุก ฉันใช้หมากรุกเป็นตัวอย่างเพื่ออธิบายปัญหาทั่วไป (ฉันอาจทำให้คำถามนั้นชัดเจนยิ่งขึ้น) โซลูชันที่คุณเสนอไม่สามารถใช้ในโดเมนอื่นและอย่างที่คุณพูดถูกต้องไม่มีวิธีขยายด้วยวัตถุธุรกิจใหม่ (ชิ้น) มีแน่นอนหลายวิธีที่จะขยายหมากรุกกับชิ้นอื่น ๆ ....
lishaak
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.