Cách tạo một kiểu dữ liệu cho một cái gì đó đại diện cho chính nó hoặc hai thứ khác


16

Lý lịch

Đây là vấn đề thực tế tôi đang giải quyết: Tôi muốn có một cách để thể hiện các lá bài trong trò chơi bài Magic: The Gathering . Hầu hết các thẻ trong trò chơi là các thẻ trông bình thường, nhưng một số thẻ được chia thành hai phần, mỗi phần có tên riêng. Mỗi nửa của hai thẻ này được coi là một thẻ. Vì vậy, để rõ ràng, tôi sẽ Cardchỉ sử dụng để chỉ một cái gì đó là thẻ thông thường hoặc một nửa của thẻ hai phần (nói cách khác, một cái gì đó chỉ có một tên).

thẻ

Vì vậy, chúng tôi có một loại cơ sở, Thẻ. Mục đích của các đối tượng này thực sự chỉ là để giữ các thuộc tính của thẻ. Họ không thực sự làm bất cứ điều gì một mình.

interface Card {
    String name();
    String text();
    // etc
}

Có hai lớp con Cardmà tôi đang gọi PartialCard(một nửa thẻ hai phần) và WholeCard(thẻ thông thường). PartialCardcó hai phương pháp bổ sung: PartialCard otherPart()boolean isFirstPart().

Đại diện

Nếu tôi có một cỗ bài, nó nên bao gồm WholeCards, không phải Cards, vì nó Cardcó thể là một PartialCard, và điều đó sẽ không có ý nghĩa. Vì vậy, tôi muốn một đối tượng đại diện cho "thẻ vật lý", nghĩa là một thứ có thể đại diện cho một WholeCardhoặc hai PartialCards. Tôi đang tạm gọi loại này Representative, và Cardsẽ có phương pháp getRepresentative(). A Representativesẽ cung cấp hầu như không có thông tin trực tiếp trên (các) thẻ mà nó đại diện, nó sẽ chỉ trỏ đến nó / chúng. Bây giờ, ý tưởng tuyệt vời / điên rồ / ngu ngốc của tôi (bạn quyết định) là WholeCard thừa hưởng từ cả hai CardRepresentative. Rốt cuộc, họ là những lá bài đại diện cho chính họ! WholeCards có thể thực hiện getRepresentativenhư return this;.

Về phần PartialCards, họ không đại diện cho mình, nhưng họ có một bên ngoài Representativekhông phải là một Card, nhưng cung cấp các phương thức để truy cập hai PartialCards.

Tôi nghĩ hệ thống phân cấp kiểu này có ý nghĩa, nhưng nó phức tạp. Nếu chúng ta nghĩ về Cards là "thẻ khái niệm" và Representatives là "thẻ vật lý", thì hầu hết các thẻ đều có cả! Tôi nghĩ rằng bạn có thể đưa ra một lập luận rằng các thẻ vật lý thực tế có chứa các thẻ khái niệm và rằng chúng không giống nhau , nhưng tôi sẽ tranh luận rằng chúng là như vậy.

Sự cần thiết của đúc

Bởi vì cả hai PartialCardWholeCardsđều là Cards, và thường không có lý do chính đáng để tách chúng ra, tôi thường chỉ cần làm việc với Collection<Card>. Vì vậy, đôi khi tôi cần truyền PartialCards để truy cập các phương thức bổ sung của họ. Ngay bây giờ, tôi đang sử dụng hệ thống được mô tả ở đây vì tôi thực sự không thích diễn xuất rõ ràng. Và giống như Card, Representativesẽ cần phải được chuyển sang một WholeCardhoặc Composite, để truy cập vào thực tế Cardmà họ đại diện.

Vì vậy, chỉ để tóm tắt:

  • Loại cơ sở Representative
  • Loại cơ sở Card
  • Loại WholeCard extends Card, Representative(không cần truy cập, nó đại diện cho chính nó)
  • Loại PartialCard extends Card(cung cấp quyền truy cập vào phần khác)
  • Loại Composite extends Representative(cung cấp quyền truy cập vào cả hai phần)

Đây có phải là điên rồ? Tôi nghĩ rằng nó thực sự có rất nhiều ý nghĩa, nhưng tôi thực sự không chắc chắn.


1
Các thẻ PartialCards có hiệu quả ngoài các thẻ khác nhau không? Liệu thứ tự họ được chơi có vấn đề? Bạn có thể không chỉ tạo một lớp "Deck Slot" và "WholeCard" và cho phép DeckSlots có nhiều WholeCards, sau đó chỉ cần làm một cái gì đó như DeckSlot.WholeCards.Play và nếu có 1 hoặc 2 chơi cả hai như thể chúng tách biệt thẻ? Dường như với thiết kế phức tạp hơn nhiều của bạn, chắc chắn sẽ thiếu thứ gì đó :)
enderland

Tôi thực sự không cảm thấy mình có thể sử dụng đa hình giữa toàn bộ thẻ và thẻ một phần để giải quyết vấn đề này. Có, nếu tôi muốn một chức năng "chơi", thì tôi có thể thực hiện điều đó một cách dễ dàng, nhưng vấn đề là thẻ một phần và toàn bộ thẻ có các bộ thuộc tính khác nhau. Tôi thực sự cần phải có khả năng xem các đối tượng này như những gì chúng là, không chỉ là lớp cơ sở của chúng. Trừ khi có một giải pháp tốt hơn cho vấn đề đó. Nhưng tôi đã thấy rằng đa hình không kết hợp tốt với các loại có chung một lớp cơ sở chung, nhưng có các phương thức khác nhau giữa chúng, nếu điều đó có ý nghĩa.
codebreaker

1
Thành thật mà nói, tôi nghĩ điều này phức tạp đến nực cười. Bạn dường như chỉ mô hình các mối quan hệ "là một", dẫn đến một thiết kế rất cứng nhắc với khớp nối chặt chẽ. Thú vị hơn sẽ là những gì những thẻ đang thực sự làm?
Daniel Jour

2
Nếu chúng là "chỉ là các đối tượng dữ liệu", thì bản năng của tôi sẽ là có một lớp chứa một mảng các đối tượng của lớp thứ hai và để nó ở đó; không thừa kế hoặc các biến chứng vô nghĩa khác. Cho dù hai lớp đó có phải là PlayableCard và DrawableCard hay WholeCard và CardPart tôi không biết, vì tôi không biết gần như đủ về cách trò chơi của bạn hoạt động, nhưng tôi chắc chắn bạn có thể nghĩ ra thứ gì đó để gọi chúng.
Ixrec

1
Những loại tài sản nào họ nắm giữ? Bạn có thể cho ví dụ về các hành động sử dụng các thuộc tính này?
Daniel Jour

Câu trả lời:


14

Dường như với tôi rằng bạn nên có một lớp học như

class PhysicalCard {
    List<LogicalCard> getLogicalCards();
}

Mã liên quan đến thẻ vật lý có thể xử lý lớp thẻ vật lý và mã liên quan đến thẻ logic có thể xử lý vấn đề đó.

Tôi nghĩ rằng bạn có thể đưa ra một lập luận rằng các thẻ vật lý thực tế có chứa các thẻ khái niệm và rằng chúng không giống nhau, nhưng tôi sẽ tranh luận rằng chúng là như vậy.

Không quan trọng bạn có nghĩ rằng thẻ vật lý và logic là giống nhau hay không. Đừng cho rằng chỉ vì chúng là cùng một đối tượng vật lý, chúng phải là cùng một đối tượng trong mã của bạn. Điều quan trọng là việc áp dụng mô hình đó có làm cho lập trình viên dễ đọc và viết hơn không. Thực tế là, việc áp dụng một mô hình đơn giản hơn trong đó mọi thẻ vật lý được coi là một bộ sưu tập thẻ logic một cách nhất quán, 100% thời gian, sẽ dẫn đến mã đơn giản hơn.


2
Ủng hộ vì đây là câu trả lời tốt nhất, mặc dù không phải vì lý do được đưa ra. Đây là câu trả lời tốt nhất không phải vì nó đơn giản, mà bởi vì thẻ vật lý và thẻ khái niệm không giống nhau, và giải pháp này mô hình chính xác mối quan hệ của họ. Đúng là một mô hình OOP tốt không phải lúc nào cũng phản ánh hiện thực vật lý, nhưng nó phải luôn phản ánh hiện thực khái niệm. Đơn giản là tốt, tất nhiên, nhưng nó cần một chỗ dựa để chính xác khái niệm.
Kevin Krumwiede

2
@KevinKrumwiede, như tôi thấy nó không có một thực tế khái niệm nào. Những người khác nhau nghĩ về cùng một thứ theo những cách khác nhau và mọi người có thể thay đổi cách họ nghĩ về nó. Bạn có thể nghĩ về thẻ vật lý và logic như các thực thể riêng biệt. Hoặc bạn có thể nghĩ về các thẻ chia là một loại ngoại lệ đối với khái niệm chung về thẻ. Về bản chất không phải là không chính xác, nhưng người ta cho vay để mô hình hóa đơn giản hơn.
Winston Ewert

8

Nói thẳng ra, tôi nghĩ rằng giải pháp được đề xuất là quá hạn chế và quá méo mó và rời rạc khỏi thực tế vật lý là mô hình, với rất ít lợi thế.

Tôi muốn đề xuất một trong hai phương án:

Tùy chọn 1. Xử lý nó như một thẻ duy nhất, được xác định là Half A // Half B , giống như trang MTG liệt kê Wear // Tear . Nhưng, cho phép Cardthực thể của bạn chứa N của mỗi thuộc tính: tên có thể phát, chi phí mana, loại, độ hiếm, văn bản, hiệu ứng, v.v.

interface Card {
  List<String> Names();
  List<ManaCost> Costs();
  List<CardTypes> Types();
  /* etc. */
}

Tùy chọn 2. Không hoàn toàn giống với Tùy chọn 1, mô hình hóa nó sau thực tế vật lý. Bạn có một Cardthực thể đại diện cho một thẻ vật lý . Và, mục đích của nó là giữ N Playable thứ. Những Playable's mỗi thanh lại có một tên riêng biệt, chi phí mana, danh sách các hiệu ứng, danh sách các khả năng, vv .. Và bạn 'vật lý' Cardcó thể có nhận dạng riêng của mình (hoặc tên) mà là một hợp chất của mỗi Playable' s tên, giống như cơ sở dữ liệu MTG dường như để làm.

interface Card {
  String Name();
  List<Playable> Playables();
}

interface Playable {
  String Name();
  ManaCost Cost();
  CardType Type();
  /* etc. */
}

Tôi nghĩ một trong hai lựa chọn này khá gần với thực tế vật lý. Và, tôi nghĩ rằng điều đó sẽ có lợi cho bất cứ ai nhìn vào mã của bạn. (Giống như chính bạn trong 6 tháng.)


5

Mục đích của các đối tượng này thực sự chỉ là để giữ các thuộc tính của thẻ. Họ không thực sự làm bất cứ điều gì một mình.

Câu này là một dấu hiệu cho thấy có gì đó không đúng trong thiết kế của bạn: trong OOP, mỗi lớp nên có chính xác một vai trò và việc thiếu hành vi cho thấy Lớp dữ liệu tiềm năng , có mùi khó chịu trong mã.

Rốt cuộc, họ là những lá bài đại diện cho chính họ!

IMHO, nghe có vẻ hơi lạ, và thậm chí hơi lạ. Một đối tượng của loại "Thẻ" sẽ đại diện cho một thẻ. Giai đoạn = Stage.

Tôi không biết gì về Magic: Việc thu thập , nhưng tôi đoán bạn muốn sử dụng thẻ của mình theo cách tương tự, bất kể cấu trúc thực tế của chúng là gì: bạn muốn hiển thị một chuỗi đại diện, bạn muốn tính giá trị tấn công, v.v.

Đối với vấn đề bạn mô tả, tôi muốn giới thiệu Mẫu thiết kế tổng hợp , mặc dù thực tế DP này thường được trình bày để giải quyết vấn đề chung hơn:

  1. Tạo một Cardgiao diện, như bạn đã làm.
  2. Tạo một ConcreteCard, thực hiện Cardvà xác định một thẻ mặt đơn giản. Đừng ngần ngại đặt hành vi của một thẻ bình thường trong lớp này.
  3. Tạo một CompositeCard, thực hiện Cardvà có thêm hai (và một riêng tư) Card. Hãy gọi cho họ leftCardrightCard.

Sự thanh lịch của phương pháp này là một CompositeCardthẻ chứa hai thẻ, bản thân chúng có thể là ConcreteCard hoặc CompositeCard. Trong trò chơi của bạn, leftCardrightCardcó thể sẽ có hệ thống ConcreteCard, nhưng Mẫu thiết kế cho phép bạn thiết kế các tác phẩm cấp cao hơn miễn phí nếu bạn muốn. Thao tác thẻ của bạn sẽ không tính đến loại thẻ thực sự của bạn và do đó bạn không cần những thứ như chuyển sang phân lớp.

CompositeCardtất nhiên phải thực hiện các phương thức được chỉ định trong Cardvà sẽ thực hiện bằng cách tính đến thực tế là một thẻ như vậy được làm bằng 2 thẻ (cộng với, nếu bạn muốn, một cái gì đó cụ thể choCompositeCard chính thẻ. Ví dụ, bạn có thể muốn thực hiện sau đây:

public class CompositeCard implements Card
{ 
   private final Card leftCard, rightCard;
   private final double factor;

   @Override // Defined in Card
   public double attack(Player p){
      return factor * (leftCard.attack(p) + rightCard.attack(p));
   }

   @Override // idem
   public String name()
   {
       return leftCard.name() + " combined with " + rightCard.name();
   }

   ...
}

Bằng cách đó, bạn có thể sử dụng một CompositeCard chính xác như bạn làm cho bất kỳ Cardvà hành vi cụ thể được ẩn đi nhờ tính đa hình.

Nếu bạn chắc chắn a CompositeCardsẽ luôn chứa hai Cards bình thường , bạn có thể giữ ý tưởng và chỉ cần sử dụng ConcreateCardlàm kiểu cho leftCardrightCard.


Bạn đang nói về mẫu tổng hợp , nhưng thực tế vì bạn đang giữ hai tài liệu tham khảo Cardtrong đó CompositeCardbạn đang triển khai mẫu trang trí . Tôi cũng giới thiệu với OP để sử dụng giải pháp này, trang trí là cách để đi!
Phát hiện

Tôi không thấy lý do tại sao có hai trường hợp Thẻ làm cho lớp của tôi trở thành một người trang trí. Theo liên kết của riêng bạn, một trình trang trí thêm các tính năng cho một đối tượng và chính nó là một thể hiện của cùng một lớp / giao diện so với đối tượng này. Trong khi, theo liên kết khác của bạn, một hỗn hợp cho phép sở hữu nhiều đối tượng của cùng một lớp / giao diện. Nhưng cuối cùng, những từ này không quan trọng, và chỉ có ý tưởng là tuyệt vời.
mgoeminne

@ Spiated Đây chắc chắn không phải là mẫu trang trí như thuật ngữ được sử dụng trong Java. Mẫu trang trí được thực hiện bằng cách ghi đè các phương thức trên một đối tượng riêng lẻ, tạo ra một lớp ẩn danh duy nhất cho đối tượng đó.
Kevin Krumwiede

@KevinKrumwiede miễn là CompositeCardkhông đưa ra các phương pháp bổ sung, CompositeCardchỉ là một trang trí.
Phát hiện

"... Mẫu thiết kế cho phép bạn thiết kế các tác phẩm cấp cao hơn miễn phí" - không, nó không miễn phí , ngược lại - giá của việc có một giải pháp phức tạp hơn mức cần thiết cho các yêu cầu.
Doc Brown

3

Có thể mọi thứ đều là Thẻ khi nó ở trên boong hoặc nghĩa địa, và khi bạn chơi nó, bạn xây dựng Sinh vật, Đất, Bùa mê, v.v. từ một hoặc nhiều đối tượng Thẻ, tất cả đều thực hiện hoặc mở rộng Có thể chơi được. Sau đó, một hỗn hợp trở thành một Playable duy nhất mà hàm tạo của nó có hai Thẻ một phần và một thẻ có kicker trở thành một Playable mà hàm tạo của nó có một đối số mana. Loại phản ánh những gì bạn có thể làm với nó (vẽ, chặn, xua tan, nhấn) và những gì có thể ảnh hưởng đến nó. Hoặc Playable chỉ là một Thẻ phải được hoàn nguyên cẩn thận (mất bất kỳ phần thưởng và bộ đếm nào, bị tách ra) khi bị loại khỏi cuộc chơi, nếu thực sự hữu ích khi sử dụng cùng một giao diện để gọi thẻ và dự đoán những gì nó làm.

Có lẽ Thẻ và Playable tác dụng.


Thật không may, tôi không chơi những thẻ này. Chúng thực sự chỉ là các đối tượng dữ liệu có thể được truy vấn. Nếu bạn muốn cấu trúc dữ liệu boong, bạn muốn có Danh sách <WholeCard> hoặc thứ gì đó. Nếu bạn muốn thực hiện tìm kiếm thẻ Instant màu xanh lá cây, bạn muốn có Danh sách <Thẻ>.
codebreaker

À được rồi. Không liên quan lắm đến vấn đề của bạn, sau đó. Có nên xóa?
Davislor

3

Mẫu khách truy cập là một kỹ thuật cổ điển để khôi phục thông tin loại ẩn. Chúng ta có thể sử dụng nó (một biến thể nhỏ của nó ở đây) ở đây để phân biệt giữa hai loại ngay cả khi chúng được lưu trữ trong các biến trừu tượng cao hơn.

Hãy bắt đầu với sự trừu tượng cao hơn đó, một Cardgiao diện:

public interface Card {
    public void accept(CardVisitor visitor);
}

Có thể có thêm một chút hành vi trên Cardgiao diện, nhưng hầu hết các trình thu thập thuộc tính chuyển sang một lớp mới , CardProperties:

public class CardProperties {
    // property methods, constructors, etc.

    String name();
    String text();
    // ...
}

Bây giờ chúng ta có thể có một SimpleCardthẻ đại diện cho toàn bộ thẻ với một bộ thuộc tính:

public class SimpleCard implements Card {
    private CardProperties properties;

    // Constructors, ...

    @Override
    public void accept(CardVisitor visitor) {
        visitor.visit(properties);
    }
}

Chúng tôi thấy cách CardPropertiesvà những gì sắp được viết CardVisitorđang bắt đầu khớp với nhau. Chúng ta hãy làm một CompoundCardđại diện cho một thẻ có hai mặt:

public class CompoundCard implements Card {
    private CardProperties firstFaceProperties;
    private CardProperties secondFaceProperties;

    // Constructors, ...

    public void accept(CardVisitor visitor) {
        visitor.visit(firstFaceProperties, secondFaceProperties);
    }
}

Sự CardVisitorbắt đầu nổi lên. Hãy thử viết giao diện đó ngay bây giờ:

public interface CardVisitor {
    public void visit(CardProperties properties);
    public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties);
}

(Đây là phiên bản đầu tiên của giao diện bây giờ. Chúng tôi có thể cải thiện, sẽ được thảo luận sau.)

Bây giờ chúng tôi đã bổ sung tất cả các phần. Bây giờ chúng ta chỉ cần đặt chúng lại với nhau:

List<Card> cards = new LinkedList<>();
cards.add(new SimpleCard(new CardProperties(/* ... */)));
cards.add(new CompoundCard(new CardProperties(/* ... */), new CardProperties(/* ... */)));

 for(Card card : cards) {
     card.accept(new CardVisitor() {
         @Override
         public void visit(CardProperties properties) {
             // Do something for simple cards with a single face
         }

         public void visit(CardProperties firstFaceProperties, CardProperties secondFaceProperties) {
             // Do something else for compound cards with two faces
         }
     });
 }

Thời gian chạy sẽ xử lý việc gửi đến phiên bản chính xác của #visitphương thức thông qua đa hình thay vì cố gắng phá vỡ nó.

Thay vì sử dụng một lớp ẩn danh, bạn thậm chí có thể thăng cấp CardVisitorcho một lớp bên trong hoặc thậm chí là một lớp đầy đủ nếu hành vi có thể tái sử dụng hoặc nếu bạn muốn có khả năng trao đổi hành vi khi chạy.


Chúng ta có thể sử dụng các lớp như hiện tại, nhưng có một số cải tiến chúng ta có thể thực hiện đối với CardVisitorgiao diện. Ví dụ, có thể đến một lúcCard s có thể có ba hoặc bốn hoặc năm khuôn mặt. Thay vì thêm các phương thức mới để thực hiện, chúng ta chỉ có thể có phương thức thứ hai lấy và mảng thay vì hai tham số. Điều này có ý nghĩa nếu các thẻ nhiều mặt được xử lý khác nhau, nhưng số mặt trên một mặt đều được xử lý tương tự nhau.

Chúng ta cũng có thể chuyển đổi CardVisitorsang một lớp trừu tượng thay vì giao diện và có các triển khai trống cho tất cả các phương thức. Điều này cho phép chúng tôi chỉ thực hiện các hành vi mà chúng tôi quan tâm (có thể chúng tôi chỉ quan tâm đến các mặt đơn Card). Chúng ta cũng có thể thêm các phương thức mới mà không buộc mọi lớp hiện có phải thực hiện các phương thức đó hoặc không biên dịch được.


1
Tôi nghĩ rằng điều này có thể dễ dàng mở rộng sang loại thẻ hai mặt khác (trước & sau thay vì cạnh nhau). ++
RubberDuck

Để khám phá thêm, việc sử dụng Khách truy cập để khai thác các phương thức dành riêng cho một lớp con đôi khi được gọi là Nhiều công văn. Double Dispatch có thể là một giải pháp thú vị cho vấn đề này.
mgoeminne

1
Tôi đang bỏ phiếu vì tôi nghĩ rằng mẫu khách truy cập thêm độ phức tạp không cần thiết và khớp nối với mã. Vì một giải pháp thay thế có sẵn (xem câu trả lời của mgoeminne), tôi sẽ không sử dụng nó.
Phát hiện

@ Spiated Khách truy cập một mẫu phức tạp và tôi cũng đã xem xét mẫu tổng hợp khi viết câu trả lời. Lý do tôi đã đi với khách truy cập là vì OP muốn đối xử với những điều tương tự khác nhau, thay vì những điều khác nhau tương tự. Nếu các thẻ có nhiều hành vi hơn và đang được sử dụng để chơi trò chơi, thì mẫu tổng hợp cho phép kết hợp dữ liệu để tạo ra một thống kê thống nhất. Tuy nhiên, chúng chỉ là các túi dữ liệu, có thể được sử dụng để hiển thị thẻ hiển thị, trong trường hợp nhận thông tin riêng biệt có vẻ hữu ích hơn một bộ tổng hợp đơn giản.
cbojar
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.