Come semplificare le mie complesse classi di stato e i loro test?

9

Sono in un progetto di sistema distribuito scritto in java dove abbiamo alcune classi che corrispondono a oggetti di business del mondo reale molto complessi. Questi oggetti hanno molti metodi corrispondenti alle azioni che l'utente (o qualche altro agente) può applicare a quegli oggetti. Di conseguenza, queste classi sono diventate molto complesse.

L'approccio all'architettura generale del sistema ha portato a molti comportamenti concentrati su poche classi e su molti possibili scenari di interazione.

Come esempio e per mantenere le cose semplici e chiare, diciamo che Robot e Car erano classi nel mio progetto.

Quindi, nella classe Robot avrei molti metodi nel seguente schema:

  • sleep (); isSleepAvaliable ();
  • sveglio (); isAwakeAvaliable ();
  • di cammino (direzione); isWalkAvaliable ();
  • shoot (direzione); isShootAvaliable ();
  • turnOnAlert (); isTurnOnAlertAvailable ();
  • turnOffAlert (); isTurnOffAlertAvailable ();
  • di ricarica (); isRechargeAvailable ();
  • poweroff (); isPowerOffAvailable ();
  • stepInCar (Car); isStepInCarAvailable ();
  • stepOutCar (Car); isStepOutCarAvailable ();
  • Autodistruzione (); isSelfDestructAvailable ();
  • die (); isDieAvailable ();
  • isAlive (); è sveglio(); isAlertOn (); GetBatteryLevel (); getCurrentRidingCar (); getAmmo ();
  • ...

Nella classe Car, sarebbe simile:

  • turnOn (); isTurnOnAvaliable ();
  • bivio (); isTurnOffAvaliable ();
  • di cammino (direzione); isWalkAvaliable ();
  • rifornimento (); isRefuelAvailable ();
  • Autodistruzione (); isSelfDestructAvailable ();
  • incidente (); isCrashAvailable ();
  • isOperational (); Ison (); getFuelLevel (); getCurrentPassenger ();
  • ...

Ciascuno di questi (Robot e Car) è implementato come una macchina a stati, dove alcune azioni sono possibili in alcuni stati e altre no. Le azioni cambiano lo stato dell'oggetto. I metodi azioni generano IllegalStateException quando vengono chiamati in uno stato non valido e i metodi isXXXAvailable() indicano se l'azione è possibile al momento. Sebbene alcuni siano facilmente deducibili dallo stato (ad esempio, nello stato di sonno, il risveglio è disponibile), alcuni non lo sono (per sparare, deve essere sveglio, vivo, munito di munizioni e non in auto).

Inoltre, anche le interazioni tra gli oggetti sono complesse. Ad esempio, l'auto può contenere solo un passeggero Robot, quindi se un altro tenta di entrare, deve essere lanciata un'eccezione; Se l'auto si schianta, il passeggero dovrebbe morire; Se il robot è morto all'interno di un veicolo, non può uscire, anche se l'auto stessa è ok; Se il robot è all'interno di un'auto, non può entrare in un altro prima di uscire; ecc.

Il risultato di questo, è come ho già detto, queste classi sono diventate davvero complesse. Per peggiorare le cose, ci sono centinaia di possibili scenari quando il robot e l'auto interagiscono. Inoltre, gran parte di questa logica ha bisogno di accedere a dati remoti in altri sistemi. Il risultato è che il collaudo delle unità è diventato molto difficile e abbiamo molti problemi di test, uno che provoca l'altro in un circolo vizioso:

  • I setup delle serie di test sono molto complessi, perché hanno bisogno di creare un mondo significativamente complesso da esercitare.
  • Il numero di test è enorme.
  • La suite di test richiede alcune ore per essere eseguita.
  • La copertura del nostro test è molto bassa.
  • Il codice di prova tende a essere scritto settimane o mesi dopo il codice che testano, o mai del tutto.
  • Anche molti test sono stati interrotti, principalmente perché i requisiti del codice testato sono cambiati.
  • Alcuni scenari sono così complessi, che non riescono a scadere durante l'installazione (abbiamo configurato un timeout in ciascun test, nei casi peggiori di 2 minuti e anche questa volta sono scaduti i timeout, abbiamo assicurato che non si tratta di un ciclo infinito ).
  • I bug scivolano regolarmente nell'ambiente di produzione.

Lo scenario di Robot e auto è una grossolana semplificazione eccessiva di ciò che abbiamo nella realtà. Chiaramente, questa situazione non è gestibile. Quindi, sto chiedendo aiuto e suggerimenti per: 1, ridurre la complessità delle classi; 2. Semplificare gli scenari di interazione tra i miei oggetti; 3. Ridurre il tempo di test e il codice del test da testare.

EDIT:
Penso di non essere stato chiaro sulle macchine di stato. il robot è a sua volta una macchina a stati, con stati "dormendo", "sveglio", "ricarica", "morto", ecc. La macchina è un'altra macchina a stati.

EDIT 2: Nel caso in cui sei curioso di sapere qual'è il mio sistema, le classi che interagiscono sono cose come Server, IPAddress, Disk, Backup, User, SoftwareLicense, ecc. Lo scenario Robot and Car è solo un caso che ho scoperto che sarebbe stato abbastanza semplice da spiegare il mio problema.

    
posta Victor Stafusa 27.02.2012 - 08:29
fonte

3 risposte

8

Il modello di design Stato potrebbe essere utile, se non lo stai già utilizzando.

L'idea di base è che tu crei una classe interna per ogni stato distinto - così per continuare il tuo esempio, SleepingRobot , AwakeRobot , RechargingRobot e DeadRobot sarebbero tutte classi, implementando un'interfaccia comune.

I metodi sulla classe Robot (come sleep() e isSleepAvaliable() ) hanno implementazioni semplici che delegano alla classe interna corrente.

I cambiamenti di stato sono implementati scambiando l'attuale classe interna con un'altra differente.

Il vantaggio di questo approccio è che ciascuna delle classi di stato è estremamente più semplice (in quanto rappresenta un solo stato possibile) e può essere testata indipendentemente. A seconda del tuo linguaggio di implementazione (non specificato), potresti essere ancora costretto ad avere tutto nello stesso file, oppure potresti essere in grado di suddividerlo in file sorgente più piccoli.

    
risposta data 27.02.2012 - 09:28
fonte
3

Non conosco il tuo codice, ma prendendo l'esempio del metodo "sleep", presumo che si tratti di qualcosa di simile al seguente codice "semplicistico":

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

Penso che devi fare la differenza tra test di integrazione e test di unità . Scrivere un test che viene eseguito in tutto lo stato della macchina è certamente un grande compito. Scrivere test di unità più piccoli che testano se il metodo di sonno funziona correttamente è più facile. A questo punto non è necessario sapere se lo stato della macchina è stato aggiornato correttamente o se la "macchina" ha risposto correttamente al fatto che lo stato della macchina è stato aggiornato dal "robot" ... ecc.

Dato il codice sopra, vorrei simulare l'oggetto "machineState" e il mio primo test sarebbe:

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

La mia opinione personale è che scrivere test di unità così piccoli dovrebbe essere la prima cosa da fare. Hai scritto questo:

The testcases setups are very complex, because they need to create a significantly complex world to exercize.

Eseguire questi piccoli test dovrebbe essere molto veloce e non dovresti avere nulla da inizializzare in anticipo come il tuo "mondo complesso". Ad esempio, se si tratta di un'applicazione basata su un contenitore IOC (ad esempio, Spring), non è necessario inizializzare il contesto durante i test unitari.

Dopo aver coperto una buona percentuale del tuo codice complesso con i test unitari, puoi iniziare a costruire test di integrazione più lunghi e più complessi.

Infine, questo può essere fatto se il tuo codice è complesso (come hai detto che è ora), o dopo averlo refactored.

    
risposta data 27.02.2012 - 08:57
fonte
2

Stavo leggendo la sezione "Origin" dell'articolo di Wikipedia sul principio di segregazione dell'interfaccia , e io ero ha ricordato questa domanda.

Citerò l'articolo. Il problema: "... una classe di lavoro principale ... una classe grassa con moltitudini di metodi specifici per una varietà di clienti diversi". La soluzione: "... uno strato di interfacce tra la classe Job e tutti i suoi client ..."

Il tuo problema sembra una permutazione di quello che aveva Xerox. Invece di una lezione di grassi ne hai due, e queste due classi grasse parlano tra loro invece di molti clienti.

Puoi raggruppare i metodi per tipo di interazione e quindi creare una classe di interfaccia per ciascun tipo? Ad esempio: RobotPowerInterface, RobotNavigationInterface, RobotAlarmInterface classes?

    
risposta data 11.05.2013 - 22:14
fonte

Leggi altre domande sui tag