Як спростити мої складні уроки та їх тестування?


9

Я в проекті розподіленої системи, написаному на Java, де у нас є кілька класів, що відповідають дуже складним об'єктам реального бізнесу. Ці об'єкти мають безліч методів, що відповідають діям, які користувач (або якийсь інший агент) може застосувати до цих об'єктів. В результаті ці заняття стали дуже складними.

Загальний підхід до архітектури системи спричинив багато поведінки, сконцентрованої на кількох класах, і безліч можливих сценаріїв взаємодії.

Як приклад, щоб зробити речі легкими та зрозумілими, скажімо, що Робот і Авто були моїми проектами.

Отже, у класі Robot у мене було б багато методів за такою схемою:

  • сон (); isSleepAvaliable ();
  • прокидатися (); isAwakeAvaliable ();
  • прогулянка (напрямок); isWalkAvaliable ();
  • стріляти (Напрям); isShootAvaliable ();
  • turnOnAlert (); isTurnOnAlertAvailable ();
  • turnOffAlert (); isTurnOffAlertAvailable ();
  • заряджати (); isRechargeAvailable ();
  • powerOff (); isPowerOffAvailable ();
  • stepInCar (Автомобіль); isStepInCarAvailable ();
  • stepOutCar (Автомобіль); isStepOutCarAvailable ();
  • самознищення(); isSelfDestructAvailable ();
  • die (); isDieAvailable ();
  • живий(); isAwake (); isAlertOn (); getBatteryLevel (); getCurrentRidingCar (); getAmmo ();
  • ...

У класі Car це було б так:

  • ввімкнути(); isTurnOnAvaliable ();
  • вимкнути(); isTurnOffAvaliable ();
  • прогулянка (напрямок); isWalkAvaliable ();
  • заправка (); isRefuelAvailable ();
  • самознищення(); isSelfDestructAvailable ();
  • крах (); isCrashAvailable ();
  • isOperational (); isOn (); getFuelLevel (); getCurrentPassenger ();
  • ...

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

Крім того, взаємодія між об'єктами теж є складною. Наприклад, автомобіль може утримувати лише одного пасажира-робота, тому якщо інший спробує в'їхати, слід викинути виняток; Якщо автомобіль врізається, пасажир повинен загинути; Якщо робот мертвий всередині транспортного засобу, він не може вийти, навіть якщо сама машина в порядку; Якщо Робот знаходиться в автомобілі, він не може увійти до іншого, перш ніж вийти; тощо.

Результатом цього, як я вже говорив, ці заняття стали справді складними. Що ще гірше, існує сотні можливих сценаріїв взаємодії Робота та Автомобіля. Крім того, велика частина цієї логіки дійсно потребує доступу до віддалених даних в інших системах. Результат полягає в тому, що тестування приладів стало дуже важким, і у нас багато проблем з тестуванням, одна викликала іншу в порочному колі:

  • Налаштування тестів дуже складні, тому що їм потрібно створити значно складний світ для здійснення.
  • Кількість тестів величезна.
  • Набір тестів займає кілька годин.
  • Покриття нашого тесту дуже низьке.
  • Код тестування, як правило, пишеться на тижні чи місяці пізніше, ніж код, який вони перевіряють, або взагалі ніколи.
  • Багато тестів теж порушено, головним чином через те, що вимоги до тестового коду змінилися.
  • Деякі сценарії настільки складні, що вони не спрацьовують під час встановлення часу (ми налаштовували тайм-аут у кожному тесті, в гірших випадках - 2 хвилини і навіть цей час тривалий час, ми переконалися, що це не нескінченний цикл).
  • Клопи регулярно ковзають у виробничі умови.

Цей сценарій роботи і автомобіля - це грубе надмірне спрощення того, що ми маємо насправді. Зрозуміло, що ця ситуація не піддається управлінню. Отже, я прошу допомогу та пропозиції щодо: 1, зменшення складності занять; 2. Спростіть сценарії взаємодії між моїми об'єктами; 3. Скоротіть час тестування та кількість коду для тестування.

EDIT:
Я думаю, що я не був зрозумілий про стан машин. Робот сам по собі є державною машиною, зі штатами «сплячий», «неспаний», «підзарядний», «мертвий» тощо. Автомобіль - ще одна державна машина.

EDIT 2: Якщо вам цікаво, що насправді є моєю системою, класи, які взаємодіють, - це такі як сервер, IPAddress, диск, резервне копіювання, користувач, SoftwareLicense та ін. це було б досить просто, щоб пояснити мою проблему.


Ви розглядали питання про Code Review.SE ? Окрім цього, для дизайну, як ваш, я б почав думати про рефакторинг типу Extract Class
gnat

Я розглядав Code Review, але це не правильне місце. Основна проблема полягає не в самому коді, проблема полягає в загальному підході до архітектури системи, що призводить до великої кількості поведінки, сконцентрованої на декількох класах, і безліч можливих сценаріїв взаємодії.
Віктор Стафуса

@gnat Чи можете ви навести приклад того, як я би реалізував клас Extract для даного сценарію робота і машини?
Віктор Стафуса

Я витяг би матеріали, що стосуються автомобілів, від роботи в окремий клас. Я також витягну всі методи, пов’язані зі сном + пробудженням, у спеціалізований клас. Інші "кандидати", які, здається, заслуговують видобутку, - це влада + методи підзарядки, пов'язані з рухом речі. Зауважте, оскільки це рефакторинг, зовнішній API для роботи, ймовірно, повинен залишатися; на першому етапі я міняв би лише внутрішні. BTDTGTTS
комар

Це не питання перегляду коду - архітектура там поза темою.
Майкл К

Відповіді:


8

Модель державного дизайну може бути корисною, якщо ви її ще не використовуєте.

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

Методи Robotкласу (як sleep()і isSleepAvaliable()) мають прості реалізації, які делегуються поточному внутрішньому класу.

Зміни стану реалізуються шляхом заміни поточного внутрішнього класу на інший.

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


Я використовую Java.
Віктор Стафуса

Гарна пропозиція. Таким чином, кожна реалізація має чіткий фокус, який можна протестувати індивідуально, не маючи тестування класу 2.000 рядків для тестування всіх станів одночасно.
ОліверS

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), вам не потрібно буде ініціалізувати контекст під час тестування одиниць.

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

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


Думаю, мені не було зрозуміло про державні машини. Робот сам по собі є державною машиною, зі штатами «сплячий», «неспаний», «підзарядний», «мертвий» тощо. Автомобіль - ще одна державна машина.
Віктор Стафуса

@Victor Гаразд, сміливо виправте мій прикладний код, якщо хочете. Якщо ви не скажете мені інше, я вважаю, що мій погляд на одиничні тести все ще справедливий, я принаймні так сподіваюся.
Джалайн

Я виправив приклад. У мене немає прихильності, щоб зробити його легко видимим, тому його потрібно спочатку перевірити. Ваш коментар корисний.
Віктор Стафуса

2

Я читав розділ "Походження" статті у Вікіпедії про Принцип поділу інтерфейсу , і мені нагадали це питання.

Я цитую статтю. Проблема: "... один основний клас роботи .... клас жирів з безліччю методів, характерних для різних клієнтів." Рішення: "... шар інтерфейсів між класом Job та всіма його клієнтами ..."

Ваша проблема здається перестановкою тієї, яку мав Xerox. Замість одного жирового класу у вас є два, і ці два класи жиру розмовляють один з одним замість багатьох клієнтів.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.