Come può un metodo gestire la convalida e la creazione di entità senza parametri di output?

3

Ho 3 classi semplici. A Reference , a Parent e a Child . Child conosce le istanze Reference e Parent a cui è associata. Eccoli, inizializzazione e altri dati / metodi omessi:

class Reference
{
    public int ID { get; private set; }
}

class Parent
{
    public int ID { get; private set; }
}

class Child
{
    public int ID { get; private set; }
    public Reference Reference {get; private set; }
    public Parent Parent {get; private set; }
}

Ecco una classe che ne mantiene le raccolte:

class SetOfThings
{
    private List<Reference> _refs;
    private List<Parent> _parents;
    private List<Child> _children;

    // ...
    // initialization, etc
    // ...
}

Applica vincoli come

  • non può aggiungere Child a meno che non esista già Parent e Reference
  • non può aggiungere Child se esiste un'altra Child con lo stesso Parent e Reference già esistente

e così via. Attualmente, il metodo AddChild impone i vincoli con ArgumentExceptions e restituisce il nuovo Child con un nuovo ID univoco:

class SetOfThings  // continued
{
    public AddChild(Reference ref, Parent parent)
    {
        if (!_refs.Contains(ref))
            throw new ArgumentException($"{ref} not yet known");
        if (_children.Any(c => c.Parent == parent && c.Reference == ref))
            throw new ArgumentException($"{ref} and {parent} already referenced by a child");

        int newID = MakeNewChildID();
        var newChild = new Child(newID, ref, parent);

        _children.Add(newChild);
        return newChild;
    }
}

Tutte le mie chiamate AddChild hanno un aspetto simile a questo:

Child newChild;
try
{
    newChild = AddChild(someReference, someParent);
}
catch (ArgumentException ex)
{
    UI.Alert(ex.Message);
    return;
}

// ...
// Trigger state change, update UI with new child, etc.
// ...

Tuttavia, sembra un abuso del meccanismo di eccezione. Non è un comportamento veramente "eccezionale" quando vengono lanciati quei ArgumentExceptions - è solo un normale flusso di controllo. Ma non so come evitarlo:

  • Potrei restituire un bool che indica successo / fallimento e utilizzare un parametro di output per "restituire" il nuovo figlio,

    bool AddChild(Reference r, Parent p, out Child c)
    

    ma non ho alcuna indicazione di perché si è verificato un errore e i parametri di output sacrificano la leggibilità.

  • Potrei restituire null per indicare un errore. La firma del metodo non cambierebbe, ma non so ancora perché si verificano errori. Inoltre, un riferimento null probabilmente non dovrebbe essere previsto comportamento più di quanto dovrebbero essere le eccezioni.

  • Potrei restituire un oggetto Notification personalizzato (o un suo elenco) che indica errori con severità e stringhe di messaggi.

    Notification AddChild(Reference r, Parent p, out Child c)
    

    Molte informazioni, evviva! Ma ho ancora bisogno di un parametro di output. D'oh.

  • Potrei restituire Notification , ma costruire Child prima di passarlo in AddChild ed esporre la proprietà Child.ID come settabile pubblico in modo che SetOfThings possa cambiarlo in modo appropriato.

    var newChild = new Child(someRef, someParent);
    var notification = set.AddChild(newChild);
    if (/* notification is failure */)
        // change state, discard newChild, display message
    else
        // change state, use newChild, etc.
    

    Ma la proprietà Child.ID impostabile dal pubblico per rendere questo lavoro è al massimo confusa e pericolosa, nel peggiore dei casi.

Mi manca qualche alternativa ovvia o comune? Non sto aprendo nuovi orizzonti, ma non riesco a trovare un modo chiaro e leggibile per una classe di raccolta di avere la creazione e la convalida nello stesso metodo.

    
posta kdbanman 17.11.2015 - 00:50
fonte

2 risposte

3

L'alternativa che mi viene in mente immediatamente è semplicemente usare più di un metodo.

Child newChild(someReference, someParent);
if(HasChild(newChild)) {
    // UI.Alert and whatnot
} else {
    AddChild(someReference, someParent);
}

Ora non stai usando le eccezioni per il controllo del flusso, e se il metodo AddChild genera, significa che qualcosa è andato davvero male.

Si noti che l'esempio che ho appena dato non controlla che alcuniReference e someParent esistano già in SetOfThings. È deliberato. Forse già so per certo che esistono e sono solo i bambini duplicati di cui sono preoccupato. Forse in qualche altro frammento di codice sarà il contrario e lì scriverò:

if(!HasReference(someReference) || !HasParent(someParent)) {
    // UI.Alert and whatnot
} else {
    Child newChild = AddChild(someReference, someParent);
}

In altre parole, avere metodi dedicati (al contrario delle eccezioni) per i tuoi controlli di convalida ti consente di essere molto più esplicito su quali controlli ritieni siano necessari. Se viene generata un'eccezione da uno dei due snippet sopra riportati, allora sai di aver fatto un errore da qualche parte.

    
risposta data 17.11.2015 - 01:03
fonte
2

Non c'è niente di sbagliato nell'usare eccezioni per questo. Anche se potrebbe sembrare uno scenario comune, non deve essere tanto comune quanto non avere errori di convalida.

Anche se la validazione fallisce il più delle volte, le eccezioni sono ancora una soluzione desiderabile per il tuo problema. Se sei preoccupato per le prestazioni, non farlo perché ogni sviluppatore sa che l'ottimizzazione prematura è la radice di tutti i mali .

Ci sono diversi modi per migliorarlo:

  • Dividi il tuo metodo in due, uno per convalidarne un altro per eseguire effettivamente l'operazione che poi trasferisce la responsabilità al chiamante
  • hanno eccezioni più specifiche (esempio: InputValidationException , FormatException ) in modo che permetta al chiamante di rilevare solo errori rilevanti
  • Genera un'eccezione che contiene più del semplice messaggio di errore
  • Raccogli tutti gli errori di convalida e gettali come elenco all'interno di un oggetto ValidationException che consente al chiamante di visualizzare più errori contemporaneamente

Sulla base della mia esperienza che non utilizza eccezioni per gli errori di convalida, molto probabilmente trasformerà il tuo codice in spaghetti nel lungo periodo. Specialmente se devi gestire gli errori di convalida che vengono generati diversi livelli nello stack delle chiamate.

Eccezioni è lo strumento linguistico migliore che consente di trasmettere le informazioni attraverso l'intero stack senza trasformare la progettazione dell'API in un gran caos.

    
risposta data 17.11.2015 - 02:57
fonte

Leggi altre domande sui tag