Dobbiamo convalidare l'intero utilizzo del modulo o solo argomenti di metodi pubblici?

9

Ho sentito che si consiglia di convalidare gli argomenti dei metodi pubblici:

La motivazione è comprensibile. Se un modulo viene utilizzato in modo errato, vogliamo generare un'eccezione immediata anziché un comportamento imprevedibile.

Ciò che mi infastidisce, è che gli argomenti sbagliati non sono l'unico errore che può essere fatto durante l'uso di un modulo. Ecco alcuni scenari di errore in cui dobbiamo aggiungere la logica di controllo se seguiamo i consigli e non vogliamo l'escalation degli errori:

  • Chiamata in arrivo - argomenti inattesi
  • Chiamata in arrivo - il modulo si trova in uno stato errato
  • Chiamata esterna: risultati inattesi restituiti
  • Chiamata esterna - effetti collaterali inattesi (doppia entrata in una chiamata modulo, rompendo gli altri stati di dipendenza)

Ho cercato di tenere conto di tutte queste condizioni e di scrivere un modulo semplice con un metodo (mi dispiace, non-C # ragazzi):

public sealed class Room
{
    private readonly IDoorFactory _doorFactory;
    private bool _entered;
    private IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        if (doorFactory == null)
            throw new ArgumentNullException("doorFactory");
        _doorFactory = doorFactory;
    }
    public void Open()
    {
        if (_door != null)
            throw new InvalidOperationException("Room is already opened");
        if (_entered)
            throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;
        _door = _doorFactory.Create();
        if (_door == null)
            throw new IncompatibleDependencyException("doorFactory");
        _door.Open();
        _entered = false;
    }
}

Ora è sicuro =)

È piuttosto inquietante. Ma immagina quanto possa essere raccapricciante in un vero modulo con dozzine di metodi, stato complesso e molte chiamate esterne (ciao, amanti dell'iniezione di dipendenza!). Nota che se stai chiamando un modulo il cui comportamento può essere sovrascritto (classe non sigillata in C #), allora stai facendo una chiamata esterna e le conseguenze non sono prevedibili nell'ambito del chiamante.

Riassumendo, qual è la strada giusta e perché? Se è possibile scegliere tra le opzioni di seguito, rispondere a ulteriori domande, per favore.

Verifica l'intero utilizzo del modulo. Abbiamo bisogno di test unitari? Esistono esempi di tale codice? L'iniezione di dipendenza dovrebbe essere limitata nell'uso (poiché causerà più logica di controllo)? Non è pratico spostare questi controlli in debug-time (non includere nel rilascio)?

Controlla solo argomenti. Dalla mia esperienza, il controllo degli argomenti, in particolare il controllo nullo, è il controllo meno efficace, poiché l'errore degli argomenti raramente causa errori complessi e escalation degli errori. La maggior parte delle volte otterrai un NullReferenceException alla riga successiva. Allora perché i controlli degli argomenti sono così speciali?

Non controllare l'utilizzo del modulo. È un'opinione impopolare, puoi spiegare perché?

    
posta astef 16.07.2015 - 00:29
fonte

2 risposte

2

TL; DR: convalida la modifica dello stato, fa affidamento sullo stato corrente [validità].

Qui di seguito considero solo le verifiche abilitate alla versione. Le asserzioni attive solo per il debug sono una forma di documentazione, che è utile a suo modo e non rientra nell'ambito di questa domanda.

Considera i seguenti principi:

  • buon senso
  • Errore veloce
  • DRY
  • SRP

Definizioni

  • Componente - un'unità che fornisce API
  • Client - utente dell'API del componente

Stato mutevole

Problema

Nelle lingue imperative, il sintomo di errore e la sua causa possono essere separati da ore di sollevamento. La corruzione dello stato può nascondersi e mutare per provocare insuccessi inspiegabili, poiché l'ispezione dello stato attuale non può rivelare il processo completo di corruzione e, quindi, l'origine dell'errore.

Soluzione

Ogni modifica dello stato dovrebbe essere attentamente elaborata e verificata. Un modo per gestire lo stato mutabile è di mantenerlo al minimo. Questo è ottenuto da:

  • sistema di tipo (dichiarazioni di membri costanti e finali)
  • introduzione di invarianti
  • verifica di ogni modifica dello stato del componente tramite API pubbliche

Quando estendi lo stato di un componente, considera di farlo lasciando che il compilatore imponga l'immutabilità dei nuovi dati. Inoltre, applica ogni vincolo di runtime ragionevole, limitando i potenziali stati risultanti a un set ben definito più piccolo possibile.

Esempio

// Wrong
class Natural {
    private int number;
    public Natural(int number) {
        this.number = number;
    }
    public int getInt() {
      if (number < 1)
          throw new InvalidOperationException();
      return number;
    }
}

// Right
class Natural {
    private readonly int number;
    /**
     * @param number - positive number
     */
    public Natural(int number) {
      // Going to modify state, verification is required
      if (number < 1)
        throw new ArgumentException("Natural number should be  positive: " + number);
      this.number = number;
    }
    public int getInt() {
      // State is guaranteed by construction and compiler
      return number;
    }
}

Coerenza di ripetizioni e responsabilità

Problema

Il controllo delle precondizioni e delle post-condizioni delle operazioni porta alla duplicazione del codice di verifica sia nel client che nel componente. Convalidare la chiamata del componente spesso costringe il cliente a prendere alcune delle responsabilità del componente.

Soluzione

Affidati al componente per eseguire la verifica dello stato, quando possibile. I componenti devono fornire un'API che non richiede una verifica dell'utilizzo speciale (verifica degli argomenti o applicazione della sequenza operativa, ad esempio) per mantenere lo stato dei componenti ben definito. Obbligano a verificare gli argomenti di chiamata dell'API come richiesto, segnalano gli errori con i mezzi necessari e si sforzano di prevenire il loro danneggiamento dello stato.

I client devono fare affidamento sui componenti per verificare l'utilizzo della loro API. Non si evita solo la ripetizione, il cliente non dipende più dai dettagli di implementazione extra del componente. Considera il framework come un componente. Scrivi solo codice di verifica personalizzato quando gli invarianti del componente non sono abbastanza rigorosi o incapsulano le eccezioni dei componenti come dettagli di implementazione.

Se un'operazione non cambia stato e non è coperta da verifiche di modifica dello stato, verifica ogni argomento al livello più profondo possibile.

Esempio

class Store {
  private readonly List<int> slots = new List<int>();
  public void putToSlot(int slot, int data) {
    if (slot < 0 || slot >= slots.Count) // Unnecessary, validated by List, only needed for custom error message
      throw new ArgumentException("data");
    slots[slot] = data;
  }
}

class Natural {
   int _number;
   public Natural(int number) {
       if (number < 1)
          number = 1;  //Wrong: client can't rely on argument verification, additional state uncertainity is introduced.  Right: throw new ArgumentException(number);
       _number = number;
   }
}

risposta

Quando i principi descritti vengono applicati all'esempio in questione, otteniamo:

public sealed class Room
{
    private bool _entered = false;
    // Do not use lazy instantiation if not absolutely necessary, this introduces additional mutable state
    private readonly IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        // Rely on system null check
        IDoor door = _doorFactory.Create();
        // Modifying own state, verification is required
        if (door == null)
           throw new ArgumentNullException("Door");
        _door = door;
    }
    public void Enter()
    {
        // Room invariants do not guarantee _entered value. Door state is indirectly a part of our state. Verification is required to prevent second door state change below.
        if (_entered)
           throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;     
        // rely on immutability for _door field to be non-null
        // rely on door implementation to control resulting door state       
        _door.Open();            
    }
}

Sommario

Lo stato del client è costituito da valori di campi propri e parti dello stato del componente che non sono coperti dai relativi invarianti. La verifica deve essere eseguita solo prima del cambio di stato effettivo di un cliente.

    
risposta data 09.01.2016 - 05:39
fonte
1

Una classe è responsabile del proprio stato. Quindi convalidare nella misura in cui mantiene o mette le cose in uno stato accettabile.

If a module will be used in a wrong way, we want to throw exception immediately instead of any unpredictable behavior.

No, non lanciare un'eccezione, ma fornire un comportamento prevedibile. Corollario allo stato di responsabilità è rendere la classe / applicazione tollerante quanto più pratica. Ad esempio, passando null a aCollection.Add() ? Basta non aggiungere e andare avanti. Ottieni null di input per la creazione di un oggetto? Crea un oggetto nullo o un oggetto predefinito. Sopra, door è già open ? Quindi, continua. L'argomento DoorFactory è nullo? Creane uno nuovo Quando creo un enum , ho sempre un membro Undefined . Faccio un uso liberale di Dictionary s e enums per definire le cose esplicitamente; e questo è molto importante per fornire un comportamento prevedibile.

(hi, dependency injection lovers!)

Sì, sebbene io cammini attraverso l'ombra della valle dei parametri non temerò argomenti. Ai precedenti uso anche i parametri di default e opzionali il più possibile.

Tutto quanto sopra consente all'elaborazione interna di andare avanti. In una particolare applicazione ho dozzine di metodi su più classi con un solo posto dove viene lanciata un'eccezione. Anche in questo caso, non è a causa di argomenti nulli o non è possibile continuare l'elaborazione perché il codice ha finito per creare un oggetto "non funzionale" / "null".

modifica

citando il mio commento nella sua interezza. Penso che il design non debba semplicemente "arrendersi" quando si incontra "null". Soprattutto usando un oggetto composito.

We're forgetting key concepts/assumptions here - encapsulation & single responsibility. There is virtually no null checking after the first, client-interacting layer. The code is tolerant robust. Classes are designed with default states and so work without being written as if interacting code is bug-ridden, rogue junk. A composite parent does not have to reach down the child layers to evaluate validity (and by implication, check for null in all the nooks and crannies). The parent knows what a child's default state means

modifica fine

    
risposta data 18.07.2015 - 08:18
fonte