Khi nào nên dừng thừa kế?


9

Ngày xưa tôi đã hỏi một câu hỏi về Stack Overflow về tính kế thừa.

Tôi đã nói tôi thiết kế động cơ cờ vua theo kiểu OOP. Vì vậy, tôi thừa hưởng tất cả các phần của tôi từ lớp trừu tượng Piece nhưng kế thừa vẫn đi. Hãy để tôi hiển thị bằng mã

public abstract class Piece
{
    public void MakeMove();
    public void TakeBackMove();
}

public abstract class Pawn: Piece {}

public class WhitePawn :Pawn {}

public class BlackPawn:Pawn {}

Các lập trình viên đã được tìm thấy thiết kế của tôi một chút so với kỹ thuật và đề nghị loại bỏ các lớp mảnh màu và giữ màu mảnh như một thành viên thuộc tính như dưới đây.

public abstract class Piece
{
    public Color Color { get; set; }
    public abstract void MakeMove();
    public abstract void TakeBackMove();
}

Bằng cách này, Piece có thể biết màu sắc của mình. Sau khi thực hiện tôi đã thấy việc thực hiện của tôi diễn ra như dưới đây.

public abstract class Pawn: Piece
{
    public override void MakeMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }

    public override void TakeBackMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }
}

Bây giờ tôi thấy rằng giữ thuộc tính màu gây ra nếu các câu lệnh trong triển khai. Nó làm cho tôi cảm thấy chúng ta cần các mảnh màu cụ thể trong hệ thống phân cấp thừa kế.

Trong trường hợp như vậy, bạn sẽ có các lớp như WhitePawn, BlackPawn hay bạn sẽ thiết kế bằng cách giữ thuộc tính Color?

Không thấy vấn đề như vậy, bạn muốn bắt đầu thiết kế như thế nào? Giữ màu tài sản hoặc có giải pháp thừa kế?

Chỉnh sửa: Tôi muốn xác định rằng ví dụ của tôi có thể không hoàn toàn khớp với ví dụ thực tế. Vì vậy, trước khi cố gắng đoán chi tiết thực hiện chỉ cần tập trung câu hỏi.

Tôi thực sự chỉ đơn giản là hỏi nếu sử dụng thuộc tính Color sẽ gây ra nếu câu lệnh tốt hơn để sử dụng kế thừa?


3
Tôi thực sự không hiểu tại sao bạn lại làm mô hình những mảnh như thế này. Khi bạn gọi MakeMove () nó sẽ di chuyển? Tôi muốn nói những gì bạn thực sự cần bắt đầu là mô hình hóa trạng thái của hội đồng quản trị và xem những lớp bạn cần cho điều đó
jk.

11
Tôi nghĩ rằng quan niệm sai lầm cơ bản của bạn là không nhận ra rằng trong "cờ" màu cờ là một thuộc tính thực sự của người chơi chứ không phải là quân cờ. Đó là lý do tại sao chúng ta nói "người da đen di chuyển", v.v., màu sắc ở đó để xác định người chơi nào sở hữu quân cờ. Một con tốt tuân theo các quy tắc và hạn chế di chuyển tương tự cho dù màu sắc đó là biến thể duy nhất là hướng nào là "chuyển tiếp".
James Anderson

5
khi bạn không thể tìm ra "khi nào nên dừng" thừa kế, tốt hơn hết hãy bỏ nó hoàn toàn. Bạn có thể giới thiệu lại kế thừa trong giai đoạn thiết kế / thực hiện / bảo trì sau này, khi đó hệ thống phân cấp sẽ trở nên rõ ràng đối với bạn. Tôi sử dụng phương pháp này, hoạt động như một bùa mê
gnat

1
Vấn đề khó khăn hơn là liệu có tạo ra một lớp con cho từng loại mảnh hay không. Loại Piece xác định nhiều khía cạnh hành vi hơn màu mảnh.
NoChance

3
Nếu bạn muốn bắt đầu thiết kế với ABC (Các lớp cơ sở trừu tượng), tất cả chúng ta đều ủng hộ và không ... Các trường hợp ABC thực sự hữu ích là rất hiếm và thậm chí sau đó, việc sử dụng giao diện thường hữu ích hơn thay thế.
Evan Plaice

Câu trả lời:


12

Hãy tưởng tượng bạn thiết kế một ứng dụng có chứa phương tiện. Bạn có tạo ra một cái gì đó như thế này?

class BlackVehicle : Vehicle { }
class DarkBlueVehicle : Vehicle { }
class DarkGreenVehicle : Vehicle { }
// hundreds of other classes...

Trong cuộc sống thực, màu sắc là tài sản của một vật thể (có thể là ô tô hoặc quân cờ) và phải được đại diện bởi một tài sản.

MakeMoveTakeBackMovekhác nhau cho người da đen và người da trắng? Hoàn toàn không: các quy tắc đều giống nhau cho cả hai người chơi, điều đó có nghĩa là hai phương thức đó sẽ hoàn toàn giống nhau đối với người da đen và người da trắng , điều đó có nghĩa là bạn đang thêm một điều kiện mà bạn không cần nó.

Mặt khác, nếu bạn có WhitePawnBlackPawn, tôi sẽ không ngạc nhiên nếu sớm hay muộn bạn sẽ viết:

var piece = (Pawn)this.CurrentPiece;
if (piece is WhitePawn)
{
}
else // The pice is of type BlackPawn
{
}

hoặc thậm chí một cái gì đó như:

public abstract class Pawn
{
    public Color Player
    {
        get
        {
            return this is WhitePawn ? Color.White : Color.Black;
        }
    }
}

// Now imagine doing the same thing for every other piece.

4
@Freshblood Tôi nghĩ sẽ tốt hơn nếu có một directiontài sản và sử dụng nó để di chuyển hơn là sử dụng thuộc tính màu. Màu sắc lý tưởng chỉ nên được sử dụng để kết xuất, không phải cho logic.
Gyan aka Gary Buyn

7
Bên cạnh đó, các mảnh di chuyển theo cùng một hướng, tiến, lùi, trái, phải. Sự khác biệt là từ phía nào của bảng họ bắt đầu. Trên một con đường, xe ô tô ở làn đường đối diện cả hai di chuyển về phía trước.
slipset

1
@Blrfl Bạn có thể đúng rằng directiontài sản là một boolean. Tôi không thấy có gì sai với điều đó. Điều làm tôi bối rối về câu hỏi cho đến khi nhận xét của Freshblood ở trên là lý do tại sao anh ấy muốn thay đổi logic chuyển động dựa trên màu sắc. Tôi sẽ nói điều đó là khó hiểu hơn bởi vì nó không có ý nghĩa cho màu sắc để thực hiện hành vi. Nếu tất cả những gì bạn muốn làm là phân biệt người chơi nào sở hữu mảnh nào bạn có thể đặt chúng vào hai bộ sưu tập riêng biệt và tránh sự cần thiết cho một tài sản hoặc thừa kế. Bản thân các quân cờ có thể hạnh phúc không biết mình thuộc về cầu thủ nào.
Gyan aka Gary Buyn

1
Tôi nghĩ rằng chúng ta đang nhìn vào cùng một điều từ hai quan điểm khác nhau. Nếu hai bên có màu đỏ và màu xanh thì hành vi của họ sẽ khác với khi họ có màu đen và trắng? Tôi sẽ nói không, đó là lý do tôi nói màu sắc không phải là nguyên nhân của bất kỳ sự khác biệt hành vi. Đó chỉ là một sự khác biệt tinh tế nhưng đó là lý do tại sao tôi sẽ sử dụng một directionhoặc có thể một playertài sản thay vì một tài sản colortrong logic trò chơi.
Gyan aka Gary Buyn

1
@Freshblood white castle's moves differ from black's- thế nào? Bạn có nghĩa là đúc? Cả lâu đài bên King và castling phía Queen đều đối xứng 100% với Đen và Trắng.
Konrad Morawski

4

Điều bạn nên tự hỏi mình là tại sao bạn thực sự cần những câu nói đó trong lớp Cầm đồ của bạn:

    if (this.Color == Color.White)
    {

    }
    else
    {
    }

Tôi khá chắc chắn rằng nó sẽ đủ để có một bộ chức năng nhỏ như

public abstract class Piece
{
    bool DoesPieceHaveSameColor(Piece otherPiece) { /*...*/   }

    Color OtherColor{get{/*...*/}}
    // ...
}

trong lớp Piece của bạn và thực hiện tất cả các hàm trong Pawn(và các dẫn xuất khác của Piece) bằng cách sử dụng các hàm đó, mà không bao giờ có bất kỳ ifcâu lệnh nào ở trên. Chỉ có ngoại lệ có thể là chức năng xác định hướng di chuyển cho Cầm đồ bằng màu sắc của nó, vì điều này là đặc trưng cho cầm đồ, nhưng trong hầu hết các trường hợp khác, bạn sẽ không cần bất kỳ mã cụ thể màu nào.

Một câu hỏi thú vị khác là liệu bạn có thực sự nên có các lớp con khác nhau cho các loại mảnh khác nhau không. Điều đó có vẻ hợp lý và tự nhiên đối với tôi, vì các quy tắc cho các bước di chuyển là khác nhau và bạn chắc chắn có thể thực hiện điều này bằng cách sử dụng các hàm quá tải khác nhau cho từng loại mảnh.

Tất nhiên, người ta có thể cố gắng để mô hình này một cách chung chung hơn, bằng cách tách các thuộc tính của các mảnh từ quy tắc di chuyển của họ, nhưng điều này có thể kết thúc trong một lớp trừu tượng MovingRulevà một hệ thống phân cấp lớp MovingRulePawn, MovingRuleQueen... Tôi sẽ không bắt đầu nó mà cách, vì nó có thể dễ dàng dẫn đến một hình thức kỹ thuật quá mức khác.


+1 để chỉ ra rằng mã cụ thể màu có thể là không. Những con tốt trắng và đen về cơ bản hoạt động theo cùng một cách, giống như tất cả các mảnh khác.
Konrad Morawski

2

Kế thừa không hữu ích khi tiện ích mở rộng không ảnh hưởng đến khả năng bên ngoài của đối tượng .

Thuộc tính Color không ảnh hưởng đến cách sử dụng đối tượng Piece . Ví dụ: tất cả các mảnh có thể gọi một phương thức moveForward hoặc moveBackwards (ngay cả khi di chuyển không hợp pháp), nhưng logic đóng gói của Piece sử dụng thuộc tính Color để xác định giá trị tuyệt đối đại diện cho chuyển động tiến hoặc lùi (-1 hoặc + 1 trên "trục y"), theo như API của bạn có liên quan, siêu lớp và các lớp con của bạn là các đối tượng giống hệt nhau (vì tất cả chúng đều phơi bày các phương thức giống nhau).

Cá nhân, tôi định nghĩa một số hằng đại diện cho từng loại mảnh, sau đó sử dụng câu lệnh chuyển đổi để phân nhánh thành các phương thức riêng trả lại các di chuyển có thể cho trường hợp "này".


Chuyển động ngược chỉ là bất hợp pháp trong trường hợp cầm đồ. Và họ thực sự không cần phải biết họ là đen hay trắng - điều đó là đủ (và hợp lý) để khiến họ phân biệt tiến và lùi. Các Boardlớp (ví dụ) có thể chịu trách nhiệm chuyển đổi các khái niệm vào -1 hoặc +1 tùy thuộc vào màu sắc của tác phẩm.
Konrad Morawski

0

Cá nhân tôi sẽ không thừa hưởng bất cứ điều gì từ Piecetất cả, bởi vì tôi không nghĩ rằng bạn có được bất cứ điều gì từ việc đó. Có lẽ một mảnh không thực sự quan tâm đến cách nó di chuyển, chỉ có hình vuông nào là đích đến hợp lệ cho bước di chuyển tiếp theo của nó.

Có lẽ bạn có thể tạo Máy tính Di chuyển hợp lệ , biết các mẫu chuyển động có sẵn cho một loại mảnh và có thể truy xuất danh sách các ô vuông đích hợp lệ, ví dụ:

interface IMoveCalculator
{
    List<Square> GetValidDestinations();
}

class KnightMoveCalculator : IMoveCalculator;
class QueenMoveCalculator : IMoveCalculator;
class PawnMoveCalculator : IMoveCalculator; 
// etc.

class Piece
{
    IMoveCalculator move_calculator;
public:
    Piece(IMoveCalculator mc) : move_calculator(mc) {}
}

Nhưng ai xác định phần nào được máy tính di chuyển? Nếu bạn có một lớp cơ sở mảnh và có PawnPiece, bạn có thể đưa ra giả định đó. Nhưng sau đó, với tốc độ đó, bản thân tác phẩm có thể thực hiện cách nó di chuyển, như được thiết kế trong câu hỏi ban đầu
Steven Striga

@WeekendWar Warrior - "Cá nhân tôi sẽ không thừa hưởng bất cứ thứ gì từ Piecetất cả". Có vẻ như Piece chịu trách nhiệm nhiều hơn là chỉ đơn giản là tính toán các bước di chuyển, đó là lý do để tách ra chuyển động như một mối quan tâm riêng biệt. Xác định những mảnh bị mà máy tính sẽ là một vấn đề của dependency injection, ví dụ:var my_knight = new Piece(new KnightMoveCalculator())
Bến Cottrell

Đây sẽ là một ứng cử viên tốt cho mô hình bay bổng. Có 16 con tốt trên bàn cờ và tất cả chúng đều có chung hành vi . Hành vi này có thể dễ dàng được trích xuất ra thành một con ruồi. Tất cả các phần khác cũng vậy.
MattDavey

-2

Trong câu trả lời này, tôi cho rằng bằng "công cụ cờ vua", tác giả của câu hỏi có nghĩa là "cờ vua AI", không chỉ đơn thuần là một công cụ xác nhận di chuyển.

Tôi nghĩ rằng sử dụng OOP cho các mảnh và chuyển động hợp lệ của chúng là một ý tưởng tồi vì hai lý do:

  1. Sức mạnh của một động cơ cờ vua phụ thuộc rất nhiều vào tốc độ nó có thể điều khiển các bàn cờ , xem ví dụ: các đại diện cho bảng chương trình Cờ vua . Xem xét mức độ tối ưu hóa mà bài viết này mô tả, tôi không nghĩ rằng một cuộc gọi chức năng thông thường là giá cả phải chăng. Một cuộc gọi đến một phương thức ảo sẽ thêm một mức độ gián tiếp và phải chịu một hình phạt hiệu suất cao hơn nữa. Chi phí của OOP không phải là giá cả phải chăng ở đó.
  2. Những lợi ích của OOP như khả năng mở rộng là không thể sử dụng cho các quân cờ, vì cờ vua không có khả năng sớm có được quân cờ mới. Một sự thay thế khả thi cho hệ thống phân cấp kiểu dựa trên thừa kế bao gồm sử dụng enums và switch. Rất thấp công nghệ và giống như C, nhưng khó đánh bại hiệu suất-khôn ngoan.

Di chuyển ra khỏi các cân nhắc hiệu suất, bao gồm cả màu sắc của mảnh trong phân cấp loại theo ý kiến ​​của tôi là một ý tưởng tồi. Bạn sẽ thấy mình trong các tình huống mà bạn cần kiểm tra xem hai mảnh có màu khác nhau hay không, ví dụ để chụp. Kiểm tra điều kiện này được thực hiện dễ dàng bằng cách sử dụng một tài sản. Làm điều đó bằng cách sử dụng is WhitePiece-tests sẽ là xấu hơn so với.

Ngoài ra còn có một lý do thực tế khác để tránh di chuyển thế hệ di chuyển sang các phương pháp cụ thể, cụ thể là các phần khác nhau có chung các bước di chuyển, ví dụ như tân binh và nữ hoàng. Để tránh mã trùng lặp, bạn sẽ phải tính toán mã phổ biến thành phương thức cơ bản và có một cuộc gọi tốn kém khác.

Điều đó đang được nói, nếu đó là công cụ cờ vua đầu tiên bạn đang viết, tôi chắc chắn sẽ khuyên bạn nên tuân thủ các nguyên tắc lập trình sạch sẽ và tránh các thủ thuật tối ưu hóa được mô tả bởi tác giả của Crafty. Đối phó với các đặc điểm cụ thể của các quy tắc cờ vua (đúc, thăng cấp, vượt qua) và các kỹ thuật AI phức tạp (bảng băm, cắt alpha-beta) là đủ thách thức.


1
Nó có thể không có mục đích để viết động cơ cờ vua quá nhanh.
Freshblood

5
-1. Các phán đoán như "giả thuyết nó có thể trở thành một vấn đề hiệu suất, vì vậy nó là xấu", IMHO hầu như luôn luôn là mê tín thuần túy. Và thừa kế không chỉ là về "khả năng mở rộng" theo nghĩa bạn đề cập đến nó. Các quy tắc cờ khác nhau áp dụng cho các quân cờ khác nhau và mỗi loại quân cờ có cách thực hiện khác nhau của một phương pháp như WhichMovesArePossiblelà một cách tiếp cận hợp lý.
Doc Brown

2
Nếu bạn không cần khả năng mở rộng, cách tiếp cận dựa trên switch / enum là thích hợp hơn vì nó nhanh hơn so với việc gửi phương thức ảo. Một công cụ cờ vua nặng CPU và các hoạt động được thực hiện thường xuyên nhất (và do đó là hiệu quả quan trọng) đang thực hiện các động tác và hoàn tác chúng. Chỉ cần một cấp trên là liệt kê tất cả các động thái có thể. Tôi hy vọng điều này cũng sẽ là thời gian quan trọng. Thực tế là, tôi đã lập trình các công cụ cờ vua trong C. Ngay cả khi không có OOP, việc tối ưu hóa ở mức độ thấp trên bảng đại diện là rất đáng kể. Những loại kinh nghiệm nào bạn có để loại bỏ của tôi?
Joh

2
@Joh: Tôi sẽ không tôn trọng trải nghiệm của bạn, nhưng tôi khá chắc chắn rằng đó không phải là những gì OP đã làm sau đó. Và mặc dù tối ưu hóa ở mức độ thấp có thể có ý nghĩa đối với một số vấn đề, tôi coi đó là một sai lầm khi biến chúng thành cơ sở cho các quyết định thiết kế cấp cao - đặc biệt là khi một người không phải đối mặt với bất kỳ vấn đề hiệu suất nào cho đến nay. Kinh nghiệm của tôi là Knuth đã rất đúng khi nói "Tối ưu hóa sớm là gốc rễ của mọi tội lỗi".
Doc Brown

1
-1 Tôi phải đồng ý với Đốc. Thực tế là, "cỗ máy" mà OP đang nói đến có thể chỉ đơn giản là công cụ xác nhận cho một ván cờ 2 người chơi. Trong trường hợp đó, suy nghĩ về hiệu suất của OOP so với bất kỳ phong cách nào khác là hoàn toàn lố bịch, việc xác thực các khoảnh khắc đơn giản sẽ mất một phần nghìn giây ngay cả trên thiết bị ARM cấp thấp. Đừng đọc nhiều hơn vào các yêu cầu hơn là thực sự được nêu.
Timothy Baldridge
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.