Создать новый класс объектов или написать метод, который преобразует объекты подкласса? или что-то другое? Производительность НЕ предпочтение

Я собрал две отдельные программы, которые играют в карточную игру под названием "Crazy Eights".

Классы, которые я написал для этой программы, основаны на пакете по умолчанию "карта", который предоставляет игровые карты и некоторые общие методы для игры в карты.

Я взял два отдельных подхода для достижения этого, которые оба функциональны сами по себе.

Вот две диаграммы классов UML, которые изображают два подхода:

Метод унаследованного подкласса "преобразование"

enter image description here

Составленный подкласс с аналогичными методами

enter image description here

Как вы можете видеть в подходе 1, класс EightsCard содержит метод convert (Card). Здесь метод:

  /**
   * Converts a Card into an EightsCard
   * @param card The card to be converted
   * @return The converted EightsCard
   */
   public EightsCard convert(Card card) {
     if (card != null) {
     EightsCard result = new EightsCard(card.getRank(), card.getSuit());
     return result;
     } 
     return null;
   }
 }

Этот метод позволяет вам вызывать методы из CardCollection, которые в противном случае не были бы законными. Например, в методе воспроизведения из класса EightsPlayer, показанного ниже:

  /**
   * Removes and returns a legal card from the player hand.
   */
  public EightsCard play(Eights eights, EightsCard prev) {
    EightsCard ecard = new EightsCard(0, 0);
    ecard = ecard.convert(searchForMatch(prev));
    if (ecard == null) {
      ecard = drawForMatch(eights, prev);
      return ecard;
    }
    return ecard;
  }

Подход 2 не требует каких-либо преобразований, поскольку подобные методы были написаны в новом классе EightsCardCollection, который расширяет CardCollection. Теперь методы воспроизведения можно записать следующим образом:

  public EightsCard play(Eights eights, EightsCard prev) {
    EightsCard card = searchForMatch(prev);
    if (card == null) {
      card = drawForMatch(eights, prev);
    }
    return card;
  }

Это подводит меня к двум вопросам:

  • Есть ли какие-либо преимущества для любого подхода, помимо личных предпочтений?
  • Есть ли лучший способ составить эту программу?

Например, может быть лучше написать "похожие" классы, которые более специфичны 1, а не использовать классы по умолчанию 2 вообще.

1 помечены как "crazyeights.syd.jjj" или "chaptwelvetofort" в диаграммах классов.

2 помечены 'defaults.syd.jjj' или cards.syd.jjj 'в диаграммах классов.

Ответ 1

Слишком много подклассов

Ни один из этих подходов не очень хорош, так как оба они имеют больше подклассов, чем необходимо. Почему EightsCard расширяет Card? Что делать, если вы хотите реализовать другую карточную игру? (Есть немало, вы знаете...) Вы бы сделали один подкласс для каждой карточной игры? Пожалуйста, не надо.

У меня были бы следующие классы:

  • Карта
  • CardCollection
  • игрок

У меня даже не было класса Deck так как кажется, что он не делает ничего, кроме расширения CardCollection не предоставляя больше функциональности.

Тогда у меня будет один класс для каждой реализации игры, поэтому один класс EightsController который обрабатывает логику игры для этой игры. Нет специального класса для EightsCard, нет специального класса для EightsCardCollection и т.д.

Причина проста: вам не нужно ничего больше, чем это. Карточка и CardCollection точно такие же, независимо от того, какую карточную игру вы играете. Игрок также является одним и тем же во всех играх.

Ответ 2

Что касается этих моделей домена, я согласен с Саймоном Форсбергом в том, что это слишком сложно, и я понимаю, что они предназначены для демонстрации некоторых концепций, но даже тогда они могли бы использовать более реалистичный пример, например, компоненты в автомобиле и их связи. Вам, вероятно, нужны только два класса домена: "Карта и игрок". Для заказанной коллекции карт используйте List<Card>. Когда дело доходит до сделок, по очереди и т.д., Существует много возможностей для творчества для организации игровой логики. Как правило, предпочтительно составлять типы из других, а не полагаться на наследование, которое на практике менее гибко.

Когда дело доходит до производительности, применяются общие правила. Например, если возможно, повторно используйте объекты. Если вы всегда используете колоду с 52 карточками, то нет причин даже иметь класс карты. Вероятно, нет ничего лучше, чем повторное использование значений перечисления во всем мире.

enum Card {
    ACE_CLUBS,
    TWO_CLUBS,
    // ...
    ACE_DIAMONDS,
    // ...
}

(Вы не можете сделать это перечисление более сложным, так как разные игры используют разные порядки, размещают разные значения на разных карточках в зависимости от контекста и т.д.)

Для отличного практического руководства по программированию для повышения эффективности я рекомендую книгу " Производительность Java: окончательное руководство".

Ответ 3

Комментарий Саймона привлек мое внимание: "Я предпочел бы иметь две перечисления: один для" Костюма "и один для" Ранг "вместо одного перечня для всей карты"

Есть ли лучший способ составить эту программу?

Ключевым моментом является то, что большая часть вашей программы должна быть полностью изолирована от представления карты в памяти.

В памяти может быть карта

  • целое число
  • байт
  • перечислить
  • строка
  • URL (http://example.org/cards/hearts/7)
  • целое/байтовое число, ограниченное диапазоном [0,52]
  • кортеж
    • int, int
    • enum, перечисление
    • байт, байт
    • смешивать и сочетать
  • ссылка на стороннюю реализацию

Большая часть вашего кода не должна волновать.

В идеале, единственными битами, которые вообще будут ухаживать, будет модуль, обеспечивающий реализацию, и, возможно, корень композиции.

В большинстве мест вашей диаграммы, где у вас есть <<Java Class>> вы должны иметь <<Java Interface>>.

См. Парнас, 1972

В такой конструкции вы использовали бы композицию, чтобы прикрепить свою сумасшедшую восьми семантику к общему определению карты, определенному где-то еще.

Eights.Deck deck(Iterable<Generic.Card> standardDeck) {
    List<Eights.Card> cards = new ArrayList();
    for(Generic.Card c : standardDeck) {
        cards.add(
            new Eights.Card(c)
        );
    }
    return new Eights.Deck(cards);
}

или, возможно,

Eights.Deck deck(Generic.Deck standardDeck) {
    return new Eights.Deck(
         standardDeck
             .stream()
             .map(Eights.Deck::new)
             .collect(Collectors.toList())
    );
}

Корень композиции будет выглядеть примерно так:

public static void main(String [] args) {
    GenericCards genericCards = new ApacheGenericCards();
    EightsCards eightsCards = MyAwesomEightsCardsV2(genericCards);
    EightsGame game = new EightsGame(eightsCards)
    game.play();
}