Jak uprościć moje złożone klasy stanowe i ich testowanie?

9

Jestem w projekcie systemu rozproszonego napisanym w Javie, w którym mamy klasy odpowiadające bardzo złożonym obiektom biznesowym w świecie rzeczywistym. Obiekty te mają wiele metod odpowiadających działaniom, które użytkownik (lub inny agent) może zastosować do tych obiektów. W rezultacie klasy te stały się bardzo złożone.

Ogólne podejście do architektury systemu doprowadziło do wielu zachowań skoncentrowanych na kilku klasach i wielu możliwych scenariuszach interakcji.

Jako przykład i dla jasności, powiedzmy, że Robot i Samochód były zajęciami w moim projekcie.

Tak więc w klasie Robot miałbym wiele metod według następującego wzoru:

  • sen(); isSleepAvaliable ();
  • obudzić(); isAwakeAvaliable ();
  • spacer (kierunek); isWalkAvaliable ();
  • strzelanie (kierunek); isShootAvaliable ();
  • turnOnAlert (); isTurnOnAlertAvailable ();
  • turnOffAlert (); isTurnOffAlertAvailable ();
  • ładowanie (); isRechargeAvailable ();
  • powerOff (); isPowerOffAvailable ();
  • stepInCar (samochód); isStepInCarAvailable ();
  • stepOutCar (samochód); isStepOutCarAvailable ();
  • samozniszczenia(); isSelfDestructAvailable ();
  • umierać(); isDieAvailable ();
  • żyje(); jest obudzony(); isAlertOn (); getBatteryLevel (); getCurrentRidingCar (); getAmmo ();
  • ...

W klasie samochodów byłoby podobnie:

  • włączyć(); isTurnOnAvaliable ();
  • wyłączyć(); isTurnOffAvaliable ();
  • spacer (kierunek); isWalkAvaliable ();
  • tankować(); isRefuelAvailable ();
  • samozniszczenia(); isSelfDestructAvailable ();
  • wypadek(); isCrashAvailable ();
  • isOperational (); isOn (); getFuelLevel (); getCurrentPassenger ();
  • ...

Każdy z nich (Robot i samochód) jest zaimplementowany jako maszyna stanu, w której niektóre działania są możliwe w niektórych stanach, a niektóre nie. Akcje zmieniają stan obiektu. Metody akcji rzucają, IllegalStateExceptiongdy są wywoływane w niepoprawnym stanie, a isXXXAvailable()metody informują, czy akcja jest możliwa w danym momencie. Chociaż niektórych można łatwo wydedukować ze stanu (np. W stanie uśpienia dostępna jest funkcja czuwania), niektóre nie są (aby strzelać, musi być przebudzona, żywa, mając amunicję i nie jeżdżąc samochodem).

Co więcej, interakcje między obiektami są również złożone. Na przykład samochód może pomieścić tylko jednego pasażera robota, więc jeśli inny spróbuje wsiąść, należy zgłosić wyjątek; W przypadku awarii samochodu pasażer powinien umrzeć; Jeśli robot nie żyje w pojeździe, nie może wyjść, nawet jeśli sam Samochód jest w porządku; Jeśli robot jest w samochodzie, nie może wejść do innego przed wysiadaniem; itp.

Wynikiem tego jest, jak już powiedziałem, zajęcia te stały się naprawdę złożone. Co gorsza, istnieją setki możliwych scenariuszy interakcji Robota i Samochodu. Ponadto znaczna część tej logiki wymaga dostępu do zdalnych danych w innych systemach. W rezultacie testy jednostkowe stały się bardzo trudne i mamy wiele problemów z testowaniem, z których jeden powoduje drugie w błędnym kole:

  • Konfiguracje przypadków testowych są bardzo złożone, ponieważ muszą stworzyć znacznie bardziej złożony świat do ćwiczeń.
  • Liczba testów jest ogromna.
  • Uruchomienie zestawu testowego zajmuje kilka godzin.
  • Nasz zasięg testów jest bardzo niski.
  • Kod testowy ma tendencję do pisania tygodni lub miesięcy później niż kod, który testuje lub wcale.
  • Wiele testów jest również zepsutych, głównie dlatego, że zmieniły się wymagania testowanego kodu.
  • Niektóre scenariusze są tak złożone, że przekroczą limit czasu podczas instalacji (skonfigurowaliśmy limit czasu w każdym teście, w najgorszych przypadkach 2 minuty, a nawet tak długo, limity czasu zapewniły, że nie jest to nieskończona pętla).
  • Błędy regularnie wpadają do środowiska produkcyjnego.

Ten scenariusz robota i samochodu to rażące nadmierne uproszczenie tego, co mamy w rzeczywistości. Oczywiście ta sytuacja nie jest możliwa do opanowania. Proszę więc o pomoc i sugestie dotyczące: 1, Zmniejszenia złożoności zajęć; 2. Uprość scenariusze interakcji między moimi obiektami; 3. Skróć czas testu i ilość kodu do przetestowania.

EDYCJA:
Myślę, że nie miałem jasności co do automatów państwowych. Robot sam jest maszyną stanową, a stany „śpią”, „budzą się”, „ładują”, „martwe” itp. Samochód to kolejna maszyna stanowa.

EDYCJA 2: W przypadku, gdy jesteś ciekawy, czym naprawdę jest mój system, klasy, które oddziałują, to takie jak Serwer, Adres IP, Dysk, Kopia zapasowa, Użytkownik, Licencja oprogramowania, itd. Scenariusz Robot i samochód to tylko przypadek, który znalazłem to byłoby wystarczająco proste, aby wyjaśnić mój problem.

Victor Stafusa
źródło
czy zastanawiałeś się nad pytaniem w Code Review.SE ? Poza tym, przy projektach takich jak twój, zacznę myśleć o refaktoryzacji rodzaju Extract Class
gnat
Zastanawiałem się nad Code Review, ale to nie jest właściwe miejsce. Głównym problemem nie jest sam kod, problemem jest ogólne podejście do architektury systemu, które prowadzi do wielu zachowań skoncentrowanych na kilku klasach i wielu możliwych scenariuszach interakcji.
Victor Stafusa
@gnat Czy możesz podać przykład, w jaki sposób wdrożyłbym klasę wyodrębniania w danym scenariuszu Robot i samochód?
Victor Stafusa
Wyodrębnię rzeczy związane z samochodem z robota do osobnej klasy. Wyodrębnię również wszystkie metody związane ze snem i wybudzeniem do dedykowanej klasy. Innymi „kandydatami”, które wydają się zasługiwać na ekstrakcję, są moc + metody ładowania, rzeczy związane z ruchem. Itd. Uwaga: ponieważ jest to refaktoryzacja, zewnętrzny interfejs API dla robota prawdopodobnie powinien pozostać; na pierwszym etapie zmodyfikowałbym tylko elementy wewnętrzne. BTDTGTTS
komara
To nie jest pytanie o weryfikację kodu - architektura jest tam nie na temat.
Michael K,

Odpowiedzi:

8

State wzornictwo może być przydatne, jeśli nie jesteś już go używać.

Główną ideą jest to, że tworzą wewnętrzną klasę dla każdego odrębnego państwa - tak, aby kontynuować swoje przykład SleepingRobot, AwakeRobot, RechargingRoboti DeadRobotże wszystko będzie klas, wdrożenie wspólnego interfejsu.

Metody Robotklasy (jak sleep()i isSleepAvaliable()) mają proste implementacje, które delegują do bieżącej klasy wewnętrznej.

Zmiany stanu są realizowane przez zamianę bieżącej klasy wewnętrznej na inną.

Zaletą tego podejścia jest to, że każda z klas stanów jest znacznie prostsza (ponieważ reprezentuje tylko jeden możliwy stan) i może być niezależnie testowana. W zależności od języka implementacji (nieokreślony), nadal możesz być zmuszony do posiadania wszystkiego w tym samym pliku lub możesz być w stanie podzielić rzeczy na mniejsze pliki źródłowe.

Bevan
źródło
Korzystam z Java.
Victor Stafusa
Dobry pomysł. W ten sposób każda implementacja ma wyraźny nacisk, który może być testowany indywidualnie, bez posiadania klasy 2.000 linii junit testującej jednocześnie wszystkie stany.
OliverS
3

Nie znam twojego kodu, ale na przykładzie metody „uśpienia” założę, że jest to coś podobnego do następującego „uproszczonego” kodu:

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

Myślę, że trzeba zrobić różnicę między integracji badań i testów jednostkowych . Napisanie testu przebiegającego przez cały stan maszyny jest z pewnością dużym zadaniem. Pisanie mniejszych testów jednostkowych, które sprawdzają, czy metoda snu działa prawidłowo, jest łatwiejsze. W tym momencie nie musisz wiedzieć, czy stan maszyny został poprawnie zaktualizowany lub czy „samochód” poprawnie zareagował na fakt, że stan maszyny został zaktualizowany przez „robota” ... itd., Otrzymujesz go.

Biorąc pod uwagę powyższy kod, wyśmiewałbym obiekt „machineState”, a moim pierwszym testem byłoby:

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());
 }
}

Osobiście uważam, że pisanie tak małych testów jednostkowych powinno być pierwszą rzeczą do zrobienia. Napisałeś to:

Konfiguracje przypadków testowych są bardzo złożone, ponieważ muszą stworzyć znacznie bardziej złożony świat do ćwiczeń.

Uruchamianie tych małych testów powinno być bardzo szybkie i nie powinieneś mieć nic do zainicjowania wcześniej, jak „złożony świat”. Na przykład, jeśli jest to aplikacja oparta na kontenerze IOC (powiedzmy Spring), nie trzeba inicjalizować kontekstu podczas testów jednostkowych.

Po pokryciu znacznego odsetka złożonego kodu testami jednostkowymi możesz zacząć budować bardziej czasochłonne i bardziej złożone testy integracyjne.

Na koniec można to zrobić niezależnie od tego, czy kod jest złożony (jak powiedziałeś, że jest teraz), czy po dokonaniu refaktoryzacji.

Jalayn
źródło
Myślę, że nie miałem jasności co do machin państwowych. Robot sam jest maszyną stanową, a stany „śpią”, „budzą się”, „ładują”, „martwe” itp. Samochód to kolejna maszyna stanowa.
Victor Stafusa
@Victor OK, jeśli chcesz, możesz poprawić mój przykładowy kod. O ile nie powiesz mi inaczej, myślę, że moje zdanie na temat testów jednostkowych jest nadal aktualne, mam przynajmniej taką nadzieję.
Jalayn
Poprawiłem przykład. Nie mam przywileju uczynienia go dobrze widocznym, więc najpierw należy go przejrzeć. Twój komentarz jest pomocny.
Victor Stafusa
2

Czytałem sekcję „Pochodzenie” artykułu w Wikipedii na temat zasady segregacji interfejsów i przypomniałem sobie o tym pytaniu.

Zacytuję ten artykuł. Problem: „… jedna główna klasa Job… gruba klasa z mnóstwem metod specyficznych dla różnych klientów”. Rozwiązanie: „... warstwa interfejsów między klasą Job a wszystkimi jej klientami ...”

Twój problem wydaje się być permutacją tej, którą miał Xerox. Zamiast jednej grubej klasy masz dwie, a te dwie tłuste klasy rozmawiają ze sobą zamiast wielu klientów.

Czy możesz pogrupować metody według rodzaju interakcji, a następnie utworzyć klasę interfejsu dla każdego typu? Na przykład: RobotPowerInterface, RobotNavigationInterface, RobotAlarmInterface?

Jeff
źródło