Java: как правильно обрабатывать много полей и их инкапсуляцию? - PullRequest
9 голосов
/ 20 апреля 2009

Допустим, мне поручено кодировать какую-то RPG. Это означает, что, например, я хочу отслеживать Character GameCharacter и их характеристики, такие как интеллект, бонусы за урон или хитпоинты.

Я очень боюсь, что к концу проекта я могу закончить обработкой с очень большим количеством полей - и для каждого мне придется убедиться, что они следуют очень похожему набору ограничений и поведения (для Например, я хочу, чтобы они были ограничены между минимальным и максимальным, я хочу иметь возможность различать «базовое значение» и «временный бонус», я хочу иметь возможность увеличивать и уменьшать оба без прохождения через сеттеры и добытчики). Неожиданно для каждого поля мне понадобится один (два?) Геттера и четыре сеттера, а может быть, и пара ретрансляторов тоже! Даже для 10 полей это означает, что множество методов одинаковы, eek.

Для DRYness я начал инкапсулировать логику работы с этими показателями в классах Field, чтобы я мог написать такой код, как intelligence.applyBonus(10) или hitpoints.get() (который заботится о том, чтобы возвращаемое значение находилось в диапазоне) и т. Д. Я даже пошел на такую ​​длину, чтобы создать классы для группировки этих полей вместе, но сейчас дело не в этом.

Теперь я столкнулся с этой проблемой при «подключении» Field к GameCharacter: в большинстве учебников по Java написано, что у каждого класса должны быть закрытые поля с открытыми методами получения и установки. Это хорошо звучит в теории, и я уже построил целый класс вокруг int; однако идея звучит не так убедительно, когда вы вызываете геттер, чтобы получить ... геттер:

thisCharacter.getIntelligence().get() //eeek

Я бы предпочел получить доступ к полю напрямую. Может быть, это мой Python / VB [1] «фон», но для меня это чище, яснее и понятнее:

thisCharacter.intelligence.get()

(Теоретическая) проблема с открытыми полями состоит в том, что я отказываюсь от всего контроля над ним; например, в какой-то другой точке кодовой базы, по несчастью, может произойти следующее:

thisCharacter.intelligence = somethingThatReallyIsNull;

Звучит как небольшая ошибка ... но ... я имею в виду, я должен действительно беспокоиться об этом? Я, например, никогда не планирую назначать Field напрямую [2], я задокументировал в Javadoc, что это не то, что должно быть сделано, но все же я новичок здесь, поэтому я немного разорван.

Так что я хотел бы услышать, что вы думаете по этой теме. Являются ли преимущества инкапсуляции настолько огромными, что я должен пойти дальше и получить геттеры, геттеры и так далее ... или я должен принять инкапсуляцию в здоровых показателях и оставить поле Field как public?


[1] Да, я знаю. Я пытался забыть. Но мы только недавно видели немного C # и человека, разве свойства не сладки. Ну хорошо.

[2] кроме конструкторов! И получатель не спасет меня от неисправного конструктора.

Ответы [ 10 ]

18 голосов
/ 20 апреля 2009

Звучит так, будто вы думаете с точки зрения if(player.dexterity > monster.dexterity) attacker = player. Вы должны думать больше как if(player.quickerThan(monster)) monster.suffersAttackFrom(player.getCurrentWeapon()). Не возитесь с голой статистикой, выражайте свое реальное намерение, а затем разрабатывайте свои классы в соответствии с тем, что они должны делать. Статистика в любом случае отговорка; что вас действительно волнует, так это то, может или нет игрок выполнять какие-либо действия, или его способность / способность в сравнении с некоторыми рекомендациями. Подумайте о том, «достаточно ли силен персонаж игрока» (player.canOpen(trapDoor)) вместо «обладает ли персонаж как минимум 50 силой».

8 голосов
/ 20 апреля 2009

Мой опыт показывает, что в ситуациях, когда вам нужно лот полей, число, природа, наименование и типы полей настолько гибки и могут изменяться в течение всего срока жизни вашего проекта, что вы могли бы скорее всего нужна какая-то карта вместо полей.

Например, есть карта атрибутов от ключей к значениям.

Предоставляйте публичные звонки для получения и установки атрибутов, но не позволяйте всем использовать их (или убедитесь, что они не используют). Вместо этого создайте классы для представления каждого интересующего вас атрибута, и этот класс предоставляет все функции для управления этим атрибутом. Например, если у вас есть Strength, у вас может быть класс «StrengthManipulation», который инициализируется для конкретного объекта Player, а затем предоставляет методы получения, установки (все с соответствующей проверкой и исключениями) и, возможно, такие вещи, как вычисление силы с бонусами и т. Д. .

Одним из преимуществ этого является то, что вы отделяете использование своих атрибутов от класса вашего игрока. Поэтому, если вы сейчас добавите атрибут интеллекта, вам не придется разбирать и перекомпилировать все, что манипулирует только силой.

Что касается прямого доступа к полям, это плохая идея. Когда вы получаете доступ к полю в VB (по крайней мере, в старых VB), вы обычно вызываете getter и setter свойства, а VB просто скрывает для вас вызов (). Я считаю, что вы должны адаптироваться к соглашениям языка, который вы используете. В C, C ++, Java и т. П. У вас есть поля и у вас есть методы. Вызов метода должен всегда иметь (), чтобы было ясно, что это вызов и могут происходить другие вещи (например, вы можете получить исключение). В любом случае, одним из преимуществ Java является более точный синтаксис и стиль.

VB на Java или C ++ - это то же самое, что текстовые сообщения для выпускников школ по написанию научных работ.

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

4 голосов
/ 20 апреля 2009

У Стива Йегге было очень интересное (хотя и продолжительное) сообщение в блоге, в котором освещались следующие вопросы: Шаблон универсального дизайна .

2 голосов
/ 20 апреля 2009

Держите ваши поля приватными! Вы никогда не хотите показывать слишком много вашего API. В будущих выпусках вы всегда можете сделать что-то частное, но не наоборот.

Подумайте, как будто вы сделаете это в следующей MMORPG. У вас будет много возможностей для ошибок, ошибок и ненужного зла. Убедитесь, что неизменяемые свойства являются окончательными.

Подумайте о DVD-плеере с его миниамилистическим интерфейсом (воспроизведение, остановка, меню), и в то же время такой технической особенностью. Вы захотите спрятать все не жизненно важное в вашей программе.

2 голосов
/ 20 апреля 2009

Мне кажется, что у thisCharacter вполне может быть объект «интеллекта» для работы с интеллектом за кулисами, но я спрашиваю себя, должен ли он вообще быть публичным. Вы должны просто предоставить thisCharacter.applyInt и thisCharacter.getInt вместо объекта, который имеет с ним дело. Не выставляйте свою реализацию таким образом.

1 голос
/ 20 апреля 2009

Являются ли преимущества инкапсуляции настолько огромными, что я должен пойти дальше и получить геттеры, геттеры и так далее ... или я должен принять инкапсуляцию в здоровых мерах и оставить Поле как открытое поле?

IMO, инкапсуляция не имеет ничего общего с обертыванием геттера / установщика вокруг частного поля. В небольших дозах или при написании библиотек общего назначения компромисс приемлем. Но когда в системе, подобной той, которую вы описываете, не отмечено, то это antipattern .

Проблема с геттерами / сеттерами заключается в том, что они создают чрезмерно тесную связь между объектом с этими методами и остальной частью системы.

Одним из преимуществ реальной инкапсуляции является то, что она уменьшает необходимость в методах получения и установки, отделяя ваш объект от остальной части системы в процессе.

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

Например, вместо:

// pseudo-encapsulation anti-pattern
public class GameCharacter
{
  private Intelligence intelligence;

  public Intelligence getIntelligence()
  {
    return intelligence
  }

  public void setIntelligence(Intelligence intelligence)
  {
    this.intelligence = intelligence;
  }
}

почему бы не попробовать это?:

// better encapsulation
public class GameCharacter
{
  public void grabObject(GameObject object)
  {
    // TODO update intelligence, etc.
  }

  public int getIntelligence()
  {
    // TODO
  }
}

или даже лучше:

// still better
public interface GameCharacter
{
  public void grabObject(GameObject object); // might update intelligence
  public int getIntelligence();
}

public class Ogre implements GameCharacter
{
  // TODO: never increases intelligence after grabbing objects
}

Другими словами, GameCharacter может захватывать GameObjects. Эффект каждого захвата GameCharacter одного и того же GameObject может (и должен) различаться, но детали полностью инкапсулированы в каждой реализации GameCharacter.

Обратите внимание, как теперь GameCharacter отвечает за собственное обновление интеллекта (проверка диапазона и т. Д.), Что может произойти, например, при захвате GameObjects. Сеттер (и осложнения, которые вы отмечаете при его наличии) исчезли. Возможно, вам удастся вообще отказаться от метода getIntelligence, в зависимости от ситуации. Аллен Голуб приводит эту идею к логическому заключению , но, похоже, такой подход не слишком распространен.

1 голос
/ 20 апреля 2009

Здесь вы видите один слой составной модели.

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

Поля должны быть окончательными, поэтому, даже если вы сделали их общедоступными, вы не можете случайно присвоить им null.

Префикс "get" присутствует для всех получателей, так что, вероятно, это скорее начальный вид, чем новая проблема как таковая.

1 голос
/ 20 апреля 2009

Похоже, ваша основная жалоба связана не столько с абстракцией методов сеттера / геттера, сколько с синтаксисом языка для их использования. То есть вы бы предпочли что-то вроде свойств стиля C #.

Если это так, то язык Java может предложить вам относительно немного. Прямой доступ к полям - это нормально, пока вам не нужно переключиться на геттер или сеттер, а затем вам нужно будет выполнить рефакторинг (возможно, хорошо, если вы контролируете всю кодовую базу).

Конечно, если платформа Java является требованием, а язык - нет, то есть другие альтернативы. Например, у Scala очень хороший синтаксис свойств, а также множество других функций, которые могут быть полезны для такого проекта. И, что самое приятное, он работает на JVM, поэтому вы все равно получите такую ​​же мобильность, какую получите, написав его на языке Java. :)

0 голосов
/ 22 апреля 2009

Подумайте об использовании "моделирующей" инфраструктуры, такой как EMF Eclipse. Это включает в себя определение ваших объектов, используя что-то вроде редактора Eclipse EMF, или в простом XML, или в Rational Rose. Это не так сложно, как кажется. Вы определяете атрибуты, ограничения и т. Д. Затем каркас генерирует код для вас. Добавляет теги @generated к частям, таким как методы получения и установки. Вы можете настроить код, а затем редактировать или вручную, или через некоторый графический интерфейс, и восстановите файлы Java.

0 голосов
/ 20 апреля 2009

В дополнение к ответу Ури (который я полностью поддерживаю) я хотел бы предложить вам рассмотреть возможность определения карты атрибутов в данных. Это сделает вашу программу ЧРЕЗВЫЧАЙНО гибкой и исключит много кода, который вы даже не понимаете, что вам не нужен.

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

Делая это таким образом, у вас НЕТ кода для каждого атрибута, чтобы записать класс в БД или отобразить его на экране. Вы просто перебираете атрибуты и используете информацию, хранящуюся внутри.

То же самое может относиться к навыкам - фактически, атрибуты и навыки, вероятно, были бы получены из того же базового класса.

После того, как вы пойдете по этому пути, вы, вероятно, найдете довольно серьезный текстовый файл для определения атрибутов и навыков, но добавить новый навык будет так же просто, как:

Skill: Weaving
  DBName: BasketWeavingSkill
  DisplayLocation: 102, 20  #using coordinates probably isn't the best idea.
  Requires: Int=8
  Requires: Dex=12
  MaxLevel=50

В какой-то момент добавление навыка, подобного этому, не потребует никакого изменения кода, все это может быть сделано довольно легко в данных, и ВСЕ данные хранятся в одном объекте навыка, прикрепленном к классу. Конечно, вы можете определить действия таким же образом:

Action: Weave Basket
  text: You attempt to weave a basket from straw
  materials: Straw
  case Weaving < 1
    test: You don't have the skill!
  case Weaving < 10
    text: You make a lame basket
    subtract 10 straw
    create basket value 8
    improve skill weaving 1%
  case Weaving < 40
    text: You make a decent basket
    subtract 10 straw
    create basket value 30
    improve skill weaving 0.1%
  case Weaving < 50
    text: You make an awesome basket!
    subtract 10 straw
    create basket value 100
    improve skill weaving 0.01%
  case Weaving = 50
    text: OMG, you made the basket of the gods!
    subtract 10 straw
    create basket value 1000

Хотя этот пример довольно продвинут, вы должны иметь возможность визуализировать, как это будет сделано без какого-либо кода. Представьте себе, как трудно было бы сделать что-то подобное без кода, если бы ваши «атрибуты / навыки» были на самом деле переменными-членами.

...