Kế thừa so với thành phần cho quân cờ


9

Một tìm kiếm nhanh của stackexchange này cho thấy rằng trong thành phần chung thường được coi là linh hoạt hơn so với thừa kế nhưng như mọi khi nó phụ thuộc vào dự án, v.v. và đôi khi sự kế thừa là sự lựa chọn tốt hơn. Tôi muốn làm một trò chơi cờ vua 3D trong đó mỗi quân cờ có một lưới, có thể là hoạt hình khác nhau, v.v. Trong ví dụ cụ thể này, có vẻ như bạn có thể tranh luận một trường hợp cho cả hai cách tiếp cận, tôi có sai không?

Kế thừa sẽ trông giống như thế này (với hàm tạo thích hợp, v.v.)

class BasePiece
{
    virtual Squares GetValidMoveSquares() = 0;
    Mesh* mesh;
    // Other fields
}

class Pawn : public BasePiece
{
   Squares GetValidMoveSquares() override;
}

mà chắc chắn tuân theo nguyên tắc "is-a" trong khi thành phần sẽ trông giống như thế này

class MovementComponent
{
    virtual Squares GetValidMoveSquares() = 0;
}

class PawnMovementComponent
{
     Squares GetValidMoveSquares() override;
}

enum class Type
{
     PAWN,
     BISHOP, //etc
}


class Piece
{
    MovementComponent* movementComponent;
    MeshComponent* mesh;
    Type type;
    // Other fields
 }

Đây có phải là vấn đề sở thích cá nhân hay một cách tiếp cận rõ ràng là một lựa chọn thông minh hơn so với phương pháp khác ở đây?

EDIT: Tôi nghĩ rằng tôi đã học được điều gì đó từ mỗi câu trả lời vì vậy tôi cảm thấy tồi tệ khi chỉ chọn một. Giải pháp cuối cùng của tôi sẽ lấy cảm hứng từ một số bài viết ở đây (vẫn đang làm việc trên nó). Cảm ơn mọi người đã dành thời gian trả lời.


Tôi tin rằng cả hai có thể tồn tại cạnh nhau, hai cái này là dành cho mục đích khác nhau, nhưng chúng không dành riêng cho nhau .. Cờ vua bao gồm các quân cờ và ván khác nhau, và các quân cờ thuộc các loại khác nhau. Những mảnh này chia sẻ một số tài sản và hành vi cơ bản, và cũng có tài sản và hành vi cụ thể. Vì vậy, theo tôi, thành phần nên được áp dụng trên bảng và miếng, trong khi kế thừa nên được theo sau trên các loại mảnh.
Hitesh Bò tót

Mặc dù bạn có thể khiến một số người cho rằng tất cả các thừa kế đều xấu, tôi nghĩ rằng ý tưởng chung là kế thừa giao diện là tốt / tốt và kế thừa thực hiện là hữu ích nhưng có thể có vấn đề. Bất cứ điều gì ngoài một mức độ thừa kế duy nhất là nghi vấn, theo kinh nghiệm của tôi. Nó có thể ổn hơn thế nhưng không đơn giản để tháo gỡ mà không tạo ra một mớ hỗn độn.
JimmyJames

Mô hình chiến lược là phổ biến trong lập trình trò chơi vì một lý do. var Pawn = new Piece(howIMove, howITake, whatILookLike)có vẻ đơn giản hơn, dễ quản lý hơn và dễ bảo trì hơn so với hệ thống phân cấp thừa kế.
Ant P

@AntP Đó là một điểm tốt, cảm ơn!
CanIS ngủYet

@AntP Làm cho nó một câu trả lời! ... Có rất nhiều công đức cho điều đó!
Svidgen

Câu trả lời:


4

Thoạt nhìn, câu trả lời của bạn giả vờ giải pháp "sáng tác" không sử dụng tính kế thừa. Nhưng tôi đoán bạn chỉ đơn giản là quên thêm điều này:

class PawnMovementComponent : MovementComponent
{
     Squares GetValidMoveSquares() override;
}

(và nhiều hơn các dẫn xuất này, một cho mỗi trong sáu loại mảnh). Bây giờ điều này trông giống như mô hình "Chiến lược" cổ điển, và nó cũng đang sử dụng sự kế thừa.

Thật không may, giải pháp này có một lỗ hổng: Piecehiện tại mỗi lần lưu giữ thông tin loại của nó hai lần:

  • bên trong biến thành viên type

  • bên trong movementComponent(được phản hồi bởi tiểu loại)

Sự dư thừa này là những gì thực sự có thể khiến bạn gặp rắc rối - một lớp đơn giản như Piececần cung cấp một "nguồn sự thật duy nhất", chứ không phải hai nguồn.

Tất nhiên, bạn có thể cố gắng lưu trữ thông tin loại chỉ trong typevà không tạo ra các lớp con MovementComponent. Nhưng thiết kế này rất có thể sẽ dẫn đến một switch(type)tuyên bố " " lớn trong việc thực hiện GetValidMoveSquares. Và đó chắc chắn là một dấu hiệu mạnh mẽ cho sự kế thừa là sự lựa chọn tốt hơn ở đây .

Lưu ý trong thiết kế "kế thừa", khá dễ dàng để cung cấp trường "loại" theo cách không dư thừa: thêm một phương thức ảo GetType()vào BasePiecevà thực hiện nó theo từng lớp cơ sở.

Liên quan đến "khuyến mãi": Tôi ở đây với @svidgen, tôi thấy các đối số được trình bày trong câu trả lời của @ TheCatWhisperer gây tranh cãi.

Giải thích "khuyến mãi" như một sự trao đổi vật lý của các mảnh thay vì diễn giải nó như là một sự thay đổi của cùng một loại cảm thấy khá tự nhiên hơn đối với tôi. Vì vậy, thực hiện điều này theo cách tương tự - bằng cách trao đổi một mảnh với một loại khác - có thể sẽ không gây ra bất kỳ vấn đề lớn nào - ít nhất là không phải cho trường hợp cụ thể của cờ vua.


Cảm ơn vì đã cho tôi biết tên của mẫu, tôi đã không nhận ra nó được gọi là Chiến lược. Bạn cũng đúng Tôi đã bỏ lỡ sự kế thừa của thành phần chuyển động trong câu hỏi của tôi. Là loại thực sự dư thừa mặc dù? Nếu tôi muốn chiếu sáng tất cả các nữ hoàng trên bảng, có vẻ lạ khi kiểm tra xem họ có thành phần chuyển động nào, cộng với tôi sẽ cần phải đúc thành phần chuyển động để xem loại nào sẽ siêu xấu không?
CanIS ngủYet

@CanIS ngủYet: vấn đề với trường "loại" được đề xuất của bạn là, nó có thể phân kỳ khỏi kiểu con của movementComponent. Xem chỉnh sửa của tôi cách cung cấp trường loại một cách an toàn trong thiết kế "kế thừa".
Doc Brown

4

Có lẽ tôi thích tùy chọn đơn giản hơn trừ khi bạn có lý do rõ ràng, được xác định rõ ràng hoặc mối quan tâm bảo trì và mở rộng mạnh mẽ .

Tùy chọn kế thừa là ít dòng mã hơn và ít phức tạp hơn. Mỗi loại mảnh có mối quan hệ 1-1 "bất biến" với các đặc điểm chuyển động của nó. (Không giống như đại diện của nó, là 1-1, nhưng không nhất thiết là bất biến - bạn có thể muốn cung cấp nhiều "giao diện" khác nhau.)

Nếu bạn phá vỡ mối quan hệ 1-1 bất biến này giữa các mảnh và hành vi / chuyển động của chúng thành nhiều lớp, có lẽ bạn chỉ thêm sự phức tạp - và không nhiều thứ khác. Vì vậy, nếu tôi đang xem xét hoặc kế thừa mã đó, tôi sẽ thấy các lý do tốt, được ghi lại cho sự phức tạp.

Tất cả những gì đã nói, một tùy chọn thậm chí đơn giản hơn , IMO, là tạo ra một Piece giao diện mà các mảnh riêng lẻ của bạn thực hiện . Trong hầu hết các ngôn ngữ, điều này thực sự rất khác với kế thừa , bởi vì một giao diện sẽ không hạn chế bạn thực hiện giao diện khác . ... Bạn không nhận được bất kỳ hành vi lớp cơ sở nào miễn phí trong trường hợp đó: Bạn muốn đặt hành vi được chia sẻ ở một nơi khác.


Tôi không mua rằng giải pháp thừa kế là 'ít dòng mã hơn'. Có lẽ trong một lớp học nhưng không tổng thể.
JimmyJames

@JimmyJames Thật sao? ... Chỉ cần đếm các dòng mã trong OP ... phải thừa nhận không phải là toàn bộ giải pháp, nhưng không phải là một chỉ báo xấu về giải pháp nào LỘC và phức tạp hơn để duy trì.
Svidgen

Tôi không muốn đưa ra quan điểm quá rõ ràng về vấn đề này bởi vì tất cả đều đáng chú ý ở điểm này nhưng vâng, tôi thực sự nghĩ vậy. Sự tranh cãi của tôi là bit mã này không đáng kể so với toàn bộ ứng dụng. Nếu điều này đơn giản hóa việc thực hiện các quy tắc (gây tranh cãi), thì bạn có thể lưu hàng chục hoặc thậm chí hàng trăm dòng mã.
JimmyJames

Cảm ơn, tôi đã không nghĩ về giao diện nhưng bạn có thể đúng rằng đây là cách tốt nhất để đi. Đơn giản hơn là luôn luôn tốt hơn.
CanIS ngủYet

2

Điều với cờ vua là các quy tắc chơi và chơi trò chơi được quy định và cố định. Bất kỳ thiết kế nào hoạt động đều tốt - sử dụng bất cứ thứ gì bạn thích! Thử nghiệm và thử tất cả.

Tuy nhiên, trong thế giới kinh doanh, không có gì được quy định chặt chẽ như thế này - các quy tắc và yêu cầu kinh doanh thay đổi theo thời gian và các chương trình phải thay đổi theo thời gian để phù hợp. Đây là nơi mà dữ liệu so với dữ liệu tạo nên sự khác biệt. Ở đây, sự đơn giản làm cho các giải pháp phức tạp dễ dàng hơn để duy trì theo thời gian và thay đổi yêu cầu. Nói chung, trong kinh doanh, chúng tôi cũng phải đối phó với sự kiên trì, có thể liên quan đến một cơ sở dữ liệu. Vì vậy, chúng ta có các quy tắc như không sử dụng nhiều lớp khi một lớp duy nhất thực hiện công việc và không sử dụng tính kế thừa khi thành phần là đủ. Nhưng tất cả các quy tắc này đều hướng đến việc làm cho mã có thể được duy trì trong thời gian dài khi đối mặt với việc thay đổi các quy tắc & yêu cầu - điều không chỉ xảy ra với cờ vua.

Với cờ vua, con đường bảo trì dài hạn rất có thể là chương trình của bạn cần ngày càng thông minh hơn, điều đó có nghĩa là tối ưu hóa tốc độ và lưu trữ sẽ chiếm ưu thế. Vì thế, nhìn chung bạn sẽ phải đánh đổi sự hy sinh khả năng đọc cho hiệu suất, và do đó, ngay cả thiết kế OO tốt nhất cuối cùng cũng sẽ đi theo hướng đó.


Lưu ý rằng đã có nhiều trò chơi thành công lấy ý tưởng và sau đó ném một số thứ mới vào hỗn hợp. Nếu bạn có thể muốn làm điều này trong tương lai với trò chơi cờ vua của mình, bạn thực sự nên tính đến những thay đổi có thể có trong tương lai.
Flater

2

Tôi sẽ bắt đầu bằng cách thực hiện một số quan sát về miền vấn đề (tức là luật cờ vua):

  • Tập hợp các bước di chuyển hợp lệ cho một mảnh không chỉ phụ thuộc vào loại mảnh mà là trạng thái của bảng (cầm đồ có thể di chuyển về phía trước nếu hình vuông trống / có thể di chuyển theo đường chéo nếu chụp một mảnh).
  • Một số hành động nhất định liên quan đến việc loại bỏ một mảnh hiện có khỏi trò chơi (quảng cáo / chụp một mảnh) hoặc di chuyển nhiều mảnh (đúc).

Đây là những vụng về để mô hình hóa như là một trách nhiệm của chính tác phẩm, và cả sự kế thừa cũng như sáng tác đều không cảm thấy tuyệt vời ở đây. Sẽ là tự nhiên hơn khi mô hình hóa các quy tắc này như là công dân hạng nhất:

// pseudo-code, not pure C++

interface MovementRule {
  set<Square> getValidMoves(Board board, Square from); // what moves can I make from the given square?
  void makeMove(Board board, Square from, Square to); // update board state to reflect a specific move
}

class Game {
  Board board;
  map<PieceType, MovementRule> rules;
}

MovementRulelà một giao diện đơn giản nhưng linh hoạt cho phép các triển khai cập nhật trạng thái bảng theo bất kỳ cách nào cần thiết để hỗ trợ các di chuyển phức tạp như đúc và quảng cáo. Đối với cờ tiêu chuẩn, bạn sẽ có một triển khai cho từng loại quân cờ, nhưng bạn cũng có thể cắm các quy tắc khác nhau trong một Gameví dụ để hỗ trợ các biến thể khác nhau của cờ vua.


Cách tiếp cận thú vị - Thực phẩm tốt cho suy nghĩ
CanIS ngủYet

1

Tôi nghĩ rằng trong trường hợp này thừa kế sẽ là sự lựa chọn sạch hơn. Sáng tác có thể thanh lịch hơn một chút, nhưng dường như tôi bị ép buộc hơn một chút.

Nếu bạn đang dự định phát triển các trò chơi khác sử dụng các quân cờ chuyển động khác nhau, thì bố cục có thể là lựa chọn tốt hơn, đặc biệt nếu bạn sử dụng mô hình nhà máy để tạo ra các mảnh cần thiết cho mỗi trò chơi.


2
Làm thế nào bạn sẽ đối phó với khuyến mãi bằng cách sử dụng thừa kế?
TheCatWhisperer

4
@TheCatWhisperer Làm thế nào để bạn xử lý nó khi bạn chơi trò chơi? (Hy vọng bạn thay thế tác phẩm - bạn không khắc lại cầm đồ, phải không!?)
svidgen

0

mà chắc chắn tuân theo nguyên tắc "là-a"

Phải, vì vậy bạn sử dụng thừa kế. Bạn vẫn có thể sáng tác trong lớp Piece của mình nhưng ít nhất bạn sẽ có xương sống. Thành phần linh hoạt hơn nhưng tính linh hoạt được đánh giá cao, nói chung, và chắc chắn trong trường hợp này bởi vì cơ hội ai đó giới thiệu một loại tác phẩm mới sẽ yêu cầu bạn từ bỏ mô hình cứng nhắc của bạn bằng không. Trò chơi cờ vua sẽ không thay đổi bất cứ lúc nào sớm. Kế thừa cung cấp cho bạn một mô hình hướng dẫn, một cái gì đó liên quan quá, dạy mã, hướng. Thành phần không quá nhiều, các thực thể miền quan trọng nhất không nổi bật, nó là vô tận, bạn phải hiểu trò chơi để hiểu mã.


cảm ơn vì đã thêm điểm rằng với thành phần khó lý luận hơn về mã
CanIS ngủYet

0

Xin vui lòng không sử dụng thừa kế ở đây. Trong khi các câu trả lời khác chắc chắn có nhiều viên ngọc khôn ngoan, sử dụng sự kế thừa ở đây chắc chắn sẽ là một sai lầm. Sử dụng bố cục không chỉ giúp bạn xử lý các vấn đề bạn không lường trước được, nhưng tôi đã thấy một vấn đề ở đây là sự kế thừa không thể xử lý một cách duyên dáng.

Khuyến mãi, khi một con tốt được chuyển đổi thành một phần có giá trị hơn, có thể là một vấn đề với thừa kế. Về mặt kỹ thuật, bạn có thể đối phó với điều này bằng cách thay thế một mảnh bằng một mảnh khác, nhưng cách tiếp cận này có những hạn chế. Một trong những hạn chế này sẽ là theo dõi thống kê trên mỗi mảnh mà bạn có thể không muốn đặt lại thông tin mảnh khi quảng cáo (hoặc viết mã bổ sung để sao chép chúng).

Bây giờ, theo như thiết kế bố cục của bạn trong câu hỏi của bạn, tôi nghĩ nó phức tạp không cần thiết. Tôi không nghi ngờ bạn cần phải có một thành phần riêng cho mỗi hành động và có thể dính vào một lớp cho mỗi loại mảnh. Enum loại cũng có thể không cần thiết.

Tôi sẽ định nghĩa một Piecelớp (như bạn đã có), cũng như một PieceTypelớp. Các PieceTypeđịnh nghĩa lớp tất cả các phương pháp mà một cầm đồ, nữ hoàng, vv phải xác định, chẳng hạn như, CanMoveTo, GetPieceTypeName, vv. Sau đó , các lớp PawnQueen, ect hầu như sẽ kế thừa từ PieceTypelớp.


3
Các vấn đề bạn thấy với khuyến mãi và thay thế là tầm thường, IMO. Tách biệt mối quan tâm khá nhiều nhiệm vụ rằng "theo dõi từng mảnh" (hoặc bất cứ điều gì) được xử lý độc lập bằng mọi cách . ... Bất kỳ lợi thế tiềm năng nào mà giải pháp của bạn cung cấp đều bị lu mờ bởi những mối quan tâm tưởng tượng mà bạn đang cố gắng giải quyết, IMO.
Svidgen

5
Để làm rõ: chắc chắn có một số giá trị cho giải pháp đề xuất của bạn. Nhưng, chúng khó tìm thấy trong câu trả lời của bạn, trong đó tập trung chủ yếu vào một vấn đề thực sự dễ giải quyết trong "giải pháp thừa kế". ... Đó là loại gièm pha (đối với tôi dù thế nào) từ bất cứ giá trị nào bạn đang cố gắng truyền đạt về ý kiến ​​của mình.
Svidgen

1
Nếu Piecekhông phải là ảo, tôi đồng ý với giải pháp này. Mặc dù thiết kế lớp có vẻ phức tạp hơn một chút trên bề mặt, tôi nghĩ rằng việc thực hiện sẽ đơn giản hơn về tổng thể. Những điều chính sẽ được liên kết với Piecevà không phải PieceTypelà bản sắc và có khả năng lịch sử di chuyển của nó.
JimmyJames

1
@svidgen Một triển khai sử dụng một mảnh ghép và một triển khai sử dụng một mảnh dựa trên thừa kế sẽ trông cơ bản giống hệt nhau ngoài các chi tiết được mô tả trong giải pháp này. Giải pháp thành phần này thêm một mức độ duy nhất. Tôi không thấy đó là điều đáng để tránh.
JimmyJames

2
@JimmyJames Phải. Nhưng, lịch sử di chuyển của nhiều mảnh cần phải được theo dõi và tương quan - không phải là thứ mà bất kỳ mảnh nào cũng phải chịu trách nhiệm, IMO. Đó là một "mối quan tâm riêng biệt" nếu bạn tham gia vào các loại RẮN.
Svidgen

0

Theo quan điểm của tôi, với thông tin được cung cấp, sẽ không khôn ngoan khi trả lời liệu kế thừa hay thành phần nên là con đường để đi.

  • Kế thừa và thành phần chỉ là các công cụ trong bộ công cụ của nhà thiết kế hướng đối tượng.
  • Bạn có thể sử dụng các công cụ này để thiết kế nhiều mô hình khác nhau cho miền của vấn đề.
  • Vì có nhiều mô hình như vậy có thể đại diện cho một miền, tùy thuộc vào quan điểm của người thiết kế về các yêu cầu, bạn có thể kết hợp kế thừa và thành phần theo nhiều cách để đưa ra các mô hình tương đương về chức năng.

Các mô hình của bạn là hoàn toàn khác nhau, trong mô hình đầu tiên bạn mô hình xung quanh khái niệm quân cờ, trong mô hình thứ hai xung quanh khái niệm chuyển động của các quân cờ. Chúng có thể tương đương về chức năng và tôi sẽ chọn một tên đại diện cho tên miền tốt hơn và giúp tôi suy luận về nó dễ dàng hơn.

Ngoài ra, trong thiết kế thứ hai của bạn, thực tế là Piecelớp của bạn có một typetrường, cho thấy rõ ràng có một vấn đề thiết kế ở đây vì bản thân mảnh có thể có nhiều loại. Nghe có vẻ như thế này có lẽ nên sử dụng một số loại thừa kế, trong đó bản thân mảnh đó là loại?

Vì vậy, bạn thấy, rất khó để tranh luận về giải pháp của bạn. Vấn đề không phải là bạn sử dụng tính kế thừa hay thành phần, mà là liệu mô hình của bạn có phản ánh chính xác miền của vấn đề hay không và liệu nó có hữu ích để giải thích về nó hay không và cung cấp việc triển khai nó.

Cần có một số kinh nghiệm để sử dụng kế thừa và sáng tác đúng cách, nhưng chúng là những công cụ hoàn toàn khác nhau và tôi không tin người ta có thể "thay thế" cái này bằng cái kia, mặc dù tôi có thể đồng ý rằng một hệ thống có thể được thiết kế hoàn toàn chỉ bằng cách sử dụng thành phần.


Bạn đưa ra một điểm tốt rằng đó là mô hình quan trọng và không phải là công cụ. Liên quan đến các loại dư thừa, thừa kế có thực sự giúp đỡ? Ví dụ: nếu tôi muốn làm nổi bật tất cả các con tốt hoặc kiểm tra xem một tác phẩm có phải là một vị vua không thì tôi có cần phải đúc không?
CanIS ngủYet

-1

Vâng, tôi đề nghị không.

Mặc dù hướng đối tượng là một công cụ tốt và thường giúp đơn giản hóa mọi thứ, nhưng nó không phải là công cụ duy nhất trong bộ công cụ.

Thay vào đó, hãy xem xét việc thực hiện một lớp bảng, chứa một phụ đề đơn giản.
Giải thích nó và tính toán mọi thứ trên đó được thực hiện trong các hàm thành viên bằng cách sử dụng mặt nạ bit và chuyển đổi.

Sử dụng MSB cho chủ sở hữu, để lại 7 bit cho mảnh, mặc dù không có dự trữ cho trống.
Ngoài ra, không cho trống, ký cho chủ sở hữu, giá trị tuyệt đối cho mảnh.

Nếu bạn muốn, bạn có thể sử dụng bitfield để tránh mặt nạ thủ công, mặc dù chúng không thể truy cập được:

class field {
    signed char data = 0;
public:
    constexpr field() = default;
    constexpr field(bool black, piece x) noexcept
    : data(x < piece::pawn || x > piece::king ? 0 : (black << 7) | x))
    {}
    constexpr bool is_black() noexcept { return data < 0; }
    constexpr bool is_white() noexcept { return data > 0; }
    constexpr bool empty() noexcept { return data == 0; }
    constexpr piece piece() noexcept { return piece(data & 0x7f); }
};

Thú vị ... và thích hợp xem xét đó là một câu hỏi C ++. Nhưng, không phải là một lập trình viên C ++, đây sẽ không phải là bản năng đầu tiên (hoặc thứ hai hoặc thứ ba ) của tôi! ... Đây có lẽ là hầu hết C ++ trả lời ở đây mặc dù!
Svidgen

7
Đây là một vi mô hóa mà không có ứng dụng thực sự nào nên tuân theo.
Nói dối Ryan

@LieRyan Nếu tất cả những gì bạn có là OOP, mọi thứ đều có mùi như một vật thể. Và liệu đó có thực sự là "vi mô" hay không cũng là một câu hỏi thú vị. Tùy thuộc vào những gì bạn làm với nó, điều đó có thể khá quan trọng.
Ded repeatator

Heh ... điều đó nói rằng , C ++ hướng đối tượng. Tôi giả sử có một lý do OP đang sử dụng C ++ thay vì chỉ C .
Svidgen

3
Ở đây tôi ít quan tâm đến việc thiếu OOP, nhưng thay vào đó làm xáo trộn mã với một chút thay đổi. Trừ khi bạn đang viết mã cho Nintendo 8 bit hoặc bạn đang viết các thuật toán yêu cầu xử lý hàng triệu bản sao của trạng thái bảng, đây là vi mô hóa sớm và không thể thực hiện được. Trong các kịch bản bình thường, có lẽ nó sẽ không nhanh hơn nữa vì truy cập bit không được phân bổ yêu cầu các thao tác bit bổ sung.
Lie Ryan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.