Как упростить мои сложные классы с сохранением состояния и их тестирование?

9

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

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

В качестве примера и для простоты и ясности, скажем, что Robot и Car были классами в моем проекте.

Итак, в классе Robot у меня было бы много методов по следующей схеме:

  • спать(); isSleepAvaliable ();
  • бодрствования (); isAwakeAvaliable ();
  • ходить (направление); isWalkAvaliable ();
  • стрелять (направление); isShootAvaliable ();
  • turnOnAlert (); isTurnOnAlertAvailable ();
  • turnOffAlert (); isTurnOffAlertAvailable ();
  • перезарядки (); isRechargeAvailable ();
  • выключить(); isPowerOffAvailable ();
  • stepInCar (Car); isStepInCarAvailable ();
  • stepOutCar (Car); isStepOutCarAvailable ();
  • саморазрушение(); isSelfDestructAvailable ();
  • умереть(); isDieAvailable ();
  • является живым(); isAwake (); isAlertOn (); getBatteryLevel (); getCurrentRidingCar (); getAmmo ();
  • ...

В классе автомобилей это было бы похоже:

  • включить(); isTurnOnAvaliable ();
  • выключи(); isTurnOffAvaliable ();
  • ходить (направление); isWalkAvaliable ();
  • заправлять (); isRefuelAvailable ();
  • саморазрушение(); isSelfDestructAvailable ();
  • аварии (); isCrashAvailable ();
  • isOperational (); Ison (); getFuelLevel (); getCurrentPassenger ();
  • ...

Каждый из них (Робот и Автомобиль) реализован как конечный автомат, где некоторые действия возможны в некоторых состояниях, а некоторые нет. Действия изменяют состояние объекта. Методы действия выдают, IllegalStateExceptionкогда вызываются в недопустимом состоянии, и isXXXAvailable()методы сообщают, возможно ли действие в данный момент. Хотя некоторые из них легко выводятся из состояния (например, в состоянии сна, бодрствование доступно), некоторые - нет (чтобы стрелять, он должен быть бодрствующим, живым, иметь боеприпасы и не ездить на машине).

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

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

  • Установки тестовых сценариев очень сложны, потому что они должны создать значительно более сложный мир для тренировок.
  • Количество тестов огромно.
  • Тестовый набор занимает несколько часов.
  • Наш тестовый охват очень низкий.
  • Код тестирования, как правило, пишется на несколько недель или месяцев позже, чем код, который они тестируют, или никогда.
  • Многие тесты тоже не пройдены, в основном из-за того, что требования тестируемого кода изменились.
  • Некоторые сценарии настолько сложны, что во время установки они терпят неудачу по таймауту (мы настраивали тайм-аут в каждом тесте, в худшем случае - 2 минуты, и даже если этот тайм-аут был продолжительным, мы убедились, что это не бесконечный цикл).
  • Ошибки регулярно проскальзывают в производственную среду.

Этот сценарий «Робот и машина» является чрезмерным упрощением того, что мы имеем на самом деле. Очевидно, что эта ситуация не управляема. Итак, я прошу помощи и предложений: 1, уменьшить сложность занятий; 2. Упростить сценарии взаимодействия между моими объектами; 3. Уменьшите время тестирования и количество кода, который будет проверен.

РЕДАКТИРОВАТЬ:
Я думаю, что я не был ясен о государственных машинах. Робот сам по себе является конечным автоматом с состояниями «спящий», «бодрствующий», «перезарядка», «мертвый» и т. д. Автомобиль - это еще один конечный автомат.

РЕДАКТИРОВАТЬ 2: В случае, если вам интересно, что на самом деле представляет собой моя система, взаимодействующие классы - это такие вещи, как Сервер, IP-адрес, Диск, Резервное копирование, Пользователь, SoftwareLicense и т. Д. Сценарий «Робот и машина» - это как раз тот случай, который я обнаружил. это было бы достаточно просто, чтобы объяснить мою проблему.

Виктор Стафуса
источник
Вы рассматривали вопрос на Code Review.SE ? Кроме этого, для дизайна, подобного вашему, я бы начал думать о рефакторинге класса Extract
gnat
Я рассмотрел Code Review, но это не то место. Основная проблема не в самом коде, проблема в подходе к общей архитектуре системы, который приводит к большому количеству поведения, сконцентрированному на нескольких классах, и множеству возможных сценариев взаимодействия.
Виктор Стафуса
@gnat Можете ли вы привести пример того, как я мог бы реализовать Извлечение класса в данном сценарии «Робот и машина»?
Виктор Стафуса
Я бы выделил связанные с автомобилем вещи из робота в отдельный класс. Я также извлек бы все методы, связанные со сном + бодрствованием, в отдельный класс. Другими «кандидатами», которые, похоже, заслуживают извлечения, являются методы питания + перезарядки, связанные с движением вещи. И т.д. Обратите внимание, поскольку это рефакторинг, внешний API для робота, вероятно, должен остаться; на первом этапе я бы модифицировал только внутренности. BTDTGTTS
комар
Это не вопрос Code Review - архитектура там не по теме.
Майкл К

Ответы:

8

Государственный шаблон дизайна может быть полезна, если вы не используете его.

Основная идея в том , что вы создаете внутренний класс для каждого отдельного государства - так , чтобы продолжить ваш пример, SleepingRobot, AwakeRobot, RechargingRobotи DeadRobotвсе были бы классов, реализует общий интерфейс.

Методы Robotкласса (например, sleep()and isSleepAvaliable()) имеют простые реализации, которые делегируют текущему внутреннему классу.

Изменения состояния реализуются путем замены текущего внутреннего класса на другой.

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

Беван
источник
Я использую Java.
Виктор Стафуса
Хорошее предложение. Таким образом, каждая реализация имеет четкую направленность, которую можно протестировать индивидуально, не имея класса Junit в 2000 строк, тестирующего все состояния одновременно.
OliverS
3

Я не знаю ваш код, но на примере метода «сна» я предположу, что он похож на следующий «упрощенный» код:

public void sleep() {
 if(!dead && awake) {
  sleeping = true;
  awake = false;
  this.updateState(SLEEPING);
 }
 throw new IllegalArgumentException("robot is either dead or not awake");
}

Я думаю, что вы должны делать различие между интеграционными и юнит-тестами . Написание теста, который выполняется на всей машине, - это, безусловно, большая задача. Написание меньших модульных тестов, которые проверяют, правильно ли работает ваш метод сна, проще. На этом этапе вам не нужно знать, было ли должным образом обновлено состояние машины или «машина» правильно отреагировала на тот факт, что состояние машины было обновлено «роботом» ... и т. Д., Вы получите его.

Учитывая приведенный выше код, я бы издеваться на «machineState» объект и мой первый тест был бы:

testSleep_dead() {
 robot.dead = true;
 robot.awake = false;
 robot.setState(AWAKE);
 try {
  robot.sleep();
  fail("should have got an exception");
 } catch(Exception e) {
  assertTrue(e instanceof IllegalArgumentException);
  assertEquals("robot is either dead or not awake", e.getMessage());
 }
}

Мое личное мнение таково, что написание таких маленьких модульных тестов должно быть первым делом. Вы написали, что:

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

Выполнение этих небольших тестов должно быть очень быстрым, и вам не нужно ничего инициализировать заранее, как ваш «сложный мир». Например, если это приложение на основе контейнера IOC (скажем, Spring), вам не нужно инициализировать контекст во время модульных тестов.

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

Наконец, это может быть сделано независимо от того, является ли ваш код сложным (как вы сказали, что он есть сейчас), или после того, как вы произвели рефакторинг.

Jalayn
источник
Я думаю, что я не был ясен о государственных машинах. Робот сам по себе является конечным автоматом с состояниями «спящий», «бодрствующий», «перезарядка», «мертвый» и т. д. Автомобиль - это еще один конечный автомат.
Виктор Стафуса
@Victor Хорошо, не стесняйтесь исправлять мой пример кода, если хотите. Если вы не скажете мне иначе, я думаю, что моя точка зрения на модульные тесты остается в силе, я надеюсь, по крайней мере, так.
Джалайн
Я исправил пример. У меня нет привилегий, чтобы сделать его легко видимым, поэтому он должен быть предварительно проверен. Ваш комментарий полезен.
Виктор Стафуса
2

Я читал раздел «Происхождение» статьи в Википедии о принципе разделения интерфейсов , и мне напомнили об этом вопросе.

Я процитирую статью. Проблема: «... один основной класс Job… толстый класс с множеством методов, специфичных для множества разных клиентов». Решение: «... слой интерфейсов между классом Job и всеми его клиентами ...»

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

Можете ли вы сгруппировать методы по типу взаимодействия, а затем создать интерфейсный класс для каждого типа? Например: классы RobotPowerInterface, RobotNavigationInterface, RobotAlarmInterface?

Джефф
источник