복잡한 상태 저장 클래스와 테스트를 단순화하는 방법은 무엇입니까?


9

매우 복잡한 실제 비즈니스 객체에 해당하는 클래스가있는 Java로 작성된 분산 시스템 프로젝트에 있습니다. 이러한 객체에는 사용자 (또는 다른 에이전트)가 해당 객체에 적용 할 수있는 작업에 해당하는 많은 방법이 있습니다. 결과적으로 이러한 클래스는 매우 복잡해졌습니다.

시스템 일반 아키텍처 접근 방식은 몇 가지 클래스와 가능한 많은 상호 작용 시나리오에 집중된 많은 동작으로 이어졌습니다.

예를 들어 물건을 쉽고 명확하게 유지하기 위해 로봇과 자동차가 내 프로젝트의 수업이라고 가정 해 봅시다.

따라서 Robot 클래스에는 다음과 같은 패턴으로 많은 메소드가 있습니다.

  • 자다(); isSleepAvaliable ();
  • 깨다(); isAwakeAvaliable ();
  • 도보 (방향); isWalkAvaliable ();
  • 촬영 (방향); isShootAvaliable ();
  • turnOnAlert (); isTurnOnAlertAvailable ();
  • turnOffAlert (); isTurnOffAlertAvailable ();
  • 재충전 (); isRechargeAvailable ();
  • powerOff (); isPowerOffAvailable ();
  • stepInCar (자동차); isStepInCarAvailable ();
  • stepOutCar (자동차); isStepOutCarAvailable ();
  • selfDestruct (); isSelfDestructAvailable ();
  • 주사위(); isDieAvailable ();
  • 살아있다(); isAwake (); isAlertOn (); getBatteryLevel (); getCurrentRidingCar (); getAmmo ();
  • ...

Car 클래스에서는 다음과 유사합니다.

  • 켜다(); isTurnOnAvaliable ();
  • 끄다(); isTurnOffAvaliable ();
  • 도보 (방향); isWalkAvaliable ();
  • 연료 보급 (); isRefuelAvailable ();
  • selfDestruct (); isSelfDestructAvailable ();
  • 크래시(); isCrashAvailable ();
  • isOperational (); isOn (); getFuelLevel (); getCurrentPassenger ();
  • ...

이러한 각 (로봇 및 자동차)은 상태 머신으로 구현되며 일부 상태에서는 일부 작업이 가능하고 일부는 불가능합니다. 동작은 객체의 상태를 변경합니다. IllegalStateException유효하지 않은 상태에서 호출되면 조치 메소드가 발생하고 isXXXAvailable()메소드는 해당 시점에 조치가 가능한지 알려줍니다. 일부는 상태에서 쉽게 추론 할 수 있지만 (예 : 수면 상태에서 깨어 있음), 그렇지 않은 경우도 있습니다 (사격하려면 깨어 있어야합니다.

또한 객체 간의 상호 작용도 복잡합니다. 예를 들어, 자동차는 한 명의 승객 만 탑승 할 수 있으므로 다른 승객이 입장하려고하면 예외가 발생합니다. 차가 추락하면 승객은 사망해야합니다. 로봇이 차량 안에서 죽었다면, 차 자체가 괜찮아도 밖으로 나갈 수 없습니다. 로봇이 차 안에 있다면, 밖으로 나가기 전에 다른 로봇에 들어갈 수 없습니다. 기타

이것의 결과는 이미 말했듯이이 클래스는 정말 복잡해졌습니다. 설상가상으로, 로봇과 자동차가 상호 작용할 때 수백 가지의 가능한 시나리오가 있습니다. 또한 그 논리의 대부분은 다른 시스템의 원격 데이터에 액세스해야합니다. 결과적으로 단위 테스트는 매우 어려워졌으며 많은 테스트 문제가 발생하여 하나는 악순환으로 이어졌습니다.

  • 테스트 케이스 설정은 실행하기 위해 상당히 복잡한 세계를 만들어야하기 때문에 매우 복잡합니다.
  • 테스트 횟수는 엄청납니다.
  • 테스트 스위트를 실행하는 데 몇 시간이 걸립니다.
  • 우리의 테스트 범위는 매우 낮습니다.
  • 테스트 코드는 테스트하는 코드보다 몇 주 또는 몇 개월 후에 작성되거나 전혀 작성되지 않는 경향이 있습니다.
  • 주로 테스트 된 코드의 요구 사항이 변경 되었기 때문에 많은 테스트가 중단되었습니다.
  • 일부 시나리오는 너무 복잡하여 설정 중에 시간 초과시 실패합니다 (각 테스트에서 시간 초과를 구성했습니다. 최악의 경우 2 분, 시간이 너무 길어도 무한 루프가 아님).
  • 버그는 정기적으로 프로덕션 환경으로 미끄러 져 들어갑니다.

로봇과 자동차 시나리오는 우리가 실제로 가지고있는 것을 지나치게 단순화 한 것입니다. 분명히이 상황은 관리 할 수 ​​없습니다. 그래서 저는 다음과 같은 도움과 제안을 요청합니다. 1, 수업의 복잡성을 줄입니다. 2. 내 객체 간의 상호 작용 시나리오를 단순화합니다. 3. 테스트 시간과 테스트 할 코드의 양을 줄입니다.

편집 :
나는 상태 머신에 대해 명확하지 않다고 생각합니다. 로봇 자체는 "잠자기", "깨어나 기", "충전", "죽음"등의 상태 머신입니다. 자동차는 다른 상태 머신입니다.

편집 2 : 내 시스템이 실제로 무엇인지 궁금한 경우 상호 작용하는 클래스는 서버, IP 주소, 디스크, 백업, 사용자, SoftwareLicense 등입니다. 로봇 및 자동차 시나리오는 내가 찾은 사례입니다. 그것은 내 문제를 설명하기에 충분히 간단합니다.


Code Review.SE에 문의 해 보셨습니까 ? 그 외에, 당신과 같은 디자인을 위해 Extract Class kind refactoring 에 대해 생각하기 시작할 것입니다
gnat

Code Review를 고려했지만 올바른 위치는 아닙니다. 주요 문제는 코드 자체가 아니며, 시스템 일반 아키텍처 접근 방식으로 문제가 발생하여 몇 가지 클래스와 많은 상호 작용 시나리오에 집중되는 많은 동작이 발생합니다.
Victor Stafusa

@gnat 주어진 로봇 및 자동차 시나리오에서 추출 클래스를 구현하는 방법의 예를 제공 할 수 있습니까?
Victor Stafusa

로봇에서 자동차 관련 자료를 별도의 클래스로 추출했습니다. 또한 sleep + awake와 관련된 모든 메소드를 전용 클래스로 추출합니다. 추출이 필요한 다른 "후보자"는 전력 + 재충전 방법, 운동 관련 물건입니다. 기타 이것은 리팩토링이므로 로봇의 외부 API는 그대로 유지되어야합니다. 첫 단계에서는 내부 만 수정했습니다. BTDTGTTS
gnat

이것은 코드 검토 질문이 아닙니다. 아키텍처는 주제가 맞지 않습니다.
Michael K

답변:


8

이미 사용하지 않을 경우 디자인 패턴은 사용이 될 수 있습니다.

그래서 당신의 예, 계속 - 핵심 아이디어는 각각 별개의 상태에 대해 내부 클래스를 만들 수 있다는 것입니다 SleepingRobot, AwakeRobot, RechargingRobotDeadRobot공통 인터페이스를 구현하는 모든 것이 클래스를.

온 방법 Robot(같은 클래스 sleep()isSleepAvaliable()) 간단한 구현 내부 클래스 현재까지 그 대리자가있다.

상태 변경은 현재 내부 클래스를 다른 클래스로 교체하여 구현됩니다.

이 접근법의 장점은 각 상태 클래스가 (단 하나의 가능한 상태를 나타 내기 때문에) 극적으로 단순하며 독립적으로 테스트 할 수 있다는 것입니다. 구현 언어 (지정되지 않음)에 따라 모든 파일이 동일한 파일에 포함되도록 제한되거나 사물을 더 작은 소스 파일로 분할 할 수 있습니다.


Java를 사용하고 있습니다.
Victor Stafusa

좋은 제안. 이런 식으로 각 구현은 모든 상태를 동시에 테스트하는 2.000 라인 junit 클래스없이 개별적으로 테스트 할 수있는 명확한 초점을 가지고 있습니다.
OliverS

3

코드를 모르지만 "sleep"메서드의 예를 들어 다음 "간단한"코드와 비슷한 것으로 가정합니다.

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 Stafusa

@Victor 좋아, 원하는 경우 예제 코드를 수정하십시오. 달리 말하지 않으면 단위 테스트에 대한 나의 요점이 여전히 유효하다고 생각합니다.
Jalayn 2012

예제를 수정했습니다. 나는 그것을 쉽게 볼 수있는 특권이 없으므로 먼저 동료의 검토를 받아야합니다. 귀하의 의견은 도움이됩니다.
Victor Stafusa

2

인터페이스 분리 원칙에 대한 Wikipedia 기사"Origin"섹션을 읽었 으며이 질문에 대해 생각 나게했습니다.

나는 기사를 인용 할 것이다. 문제 : "... 하나의 주요 작업 클래스 .... 다양한 클라이언트에 다양한 방법을 가진 뚱뚱한 클래스." 해결책 : "... Job 클래스와 모든 클라이언트 사이의 인터페이스 계층 ..."

당신의 문제는 Xerox가 가지고 있었던 것의 순열처럼 보입니다. 하나의 뚱뚱한 클래스 대신에 두 가지가 있으며이 두 가지 뚱뚱한 클래스는 많은 고객 대신 서로 이야기하고 있습니다.

상호 작용 유형별로 메소드를 그룹화 한 후 각 유형에 대한 인터페이스 클래스를 작성할 수 있습니까? 예 : RobotPowerInterface, RobotNavigationInterface, RobotAlarmInterface 클래스?

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.