Pattern per costosi controlli preliminari prima di agire

2

Diciamo che ho un metodo DoTheThing() che richiede il precondizionamento-controllo CanTheThingBeDone() per restituire true. Il secondo metodo richiede molto tempo poiché accede al database.

Sto trovando difficile trovare un modo perfetto per eseguire in modo efficace il controllo di precondizione (cioè una volta ) pur mantenendo la leggibilità e l'integrità dell'API. Osserva questi esempi:

Esempio 1:

class System() {
    public void DoTheThing() {
        if (!CanTheThingBeDone()) throw new Exception(...);
        // Do it
    }

    public bool CanTheThingBeDone() {
        // Some time consuming code
    }
}

class Consumer() {
    public void SomeLargerOperation() {
        try {  
            system.DoTheThing();
        } 
        catch (Exception) { 
            // Error handling
        }
    }
}

Esempio 2:

class System() {
    public void DoTheThing() {
        // Do it
    }

    public bool CanTheThingBeDone() {
        // Some time consuming code
    }
}

class Consumer() {
    public void SomeLargerOperation() {
        if (system.CanTheThingBeDone()) { 
            system.DoIt();
        }
    }
}

Entrambi gli approcci sono ok, ma entrambi hanno degli svantaggi. In Esempio 1 , impongo al consumatore di avvolgere il codice in un try-catch per stati difettosi. Questi stati possono essere attivati dall'utente in modo da richiedere una gestione aggraziata. Non sono sicuro che try-catch per la gestione degli errori degli utenti sia un buon approccio.

In Esempio 2 mi sto affidando la responsabilità del controllo dello stato sul consumatore, aprendo per errori di runtime non descrittivi. Nell'esempio questo , CanTheThingBeDone () è selezionato, ma quando Jimmy entra nel team a 1 anno da ora per sviluppare il modulo 2, potrebbe cambiare.

Esempio 3:

class System() {
    public bool DoTheThing() {
        if (!CanTheThingBeDone()) return false;
        // Do it 
        return true;
    }

    public bool CanTheThingBeDone() {
        // Some time consuming code
    }
}

class Consumer() {
    public void SomeLargerOperation() {
        if (!system.DoTheThing()) ProduceSomeErrorMessage();
    }
}

In questo esempio, evito try-catch per gli errori degli utenti e impedisco l'esecuzione del Do-code effettivo se lo stato non è valido, assicurando che il codice di controllo venga eseguito una sola volta. Ma sento di infrangere qualche principio di denominazione / codifica quando il metodo sta eseguendo sia il controllo che la modifica dello stato della classe (sono abbastanza sicuro che abbia persino un nome).

Come quarto esempio, potrei immaginare una combinazione di # 1 e amp; # 2 dove sia il consumatore che DoTheThing chiamano CanTheThingBeDone , ma poi stiamo facendo il lavoro due volte.

Per riassumere, non riesco a trovare un approccio migliore. Qualcuno ha un'idea migliore della mia o dei suggerimenti su come modificare uno di questi approcci per ottenere un risultato migliore?

    
posta Nilzor 29.01.2014 - 14:44
fonte

3 risposte

4

C'è una regola che mi piace: in caso di dubbio, crea un oggetto. C # è un linguaggio OOP, non funzionale dopotutto. Quindi pensare negli oggetti è molto meglio che pensare nei metodi.

public abstract class ExpensiveOperation
{
    bool wasTested = false;
    bool canBeDone = false;

    protected abstract bool Precondition();
    protected abstract void DoExecute();

    public bool CanExecute()
    {
        if (!wasTested)
        {
            canBeDone = Precondition();
            wasTested = true;
        }
        return canBeDone;
    }

    public void Execute()
    {
        if (!CanExecute())
            throw new Exception("Cannot execute");

        DoExecute();
    }
}

Quindi devi solo ricavarlo per la tua operazione personalizzata con precondizione. Ha molte buone proprietà:

  • Incapsula l'operazione con la sua precondizione
  • Assicura che venga richiamata la precondizione e che l'operazione non può essere eseguita se la precondizione fallisce
  • Assicura che la precondizione venga richiamata una sola volta indipendentemente dal nome del chiamante.
  • Fornisci l'opzione del chiamante per verificare se può essere eseguita se necessario
  • Fornisci il codice solo per la precondizione e il funzionamento, non è necessario creare altro codice
  • Assicura che il chiamante non possa aggirare questa logica di precondizione
risposta data 29.01.2014 - 18:12
fonte
1

Utilizza il primo esempio quando possibile. In questo modo è quasi impossibile dimenticare l'assegno o dimenticare di gestire l'errore. Inoltre, un tale metodo può restituire valori senza ricorrere a argomenti esterni.

Perché non dovrei usare # 2 o # 3? Perché è troppo facile per un Consumer ignorare l'errore, ad es. quando il programmatore non ha familiarità con l'API di System . Il n. 2 è particolarmente negativo perché non c'è modo di scoprire se l'operazione è stata completata con successo.

Se non puoi usare il sistema di eccezione per qualche motivo (ad esempio stai scrivendo C che non ne possiede uno, o ti astieni da eccezioni per motivi di prestazioni), allora suggerirei una combinazione di # 2 e # 3: si restituisce un codice di errore, ma se un programmatore sa cosa sta facendo, può omettere qualsiasi controllo di errore. Ma dovresti anche consentire al programmatore di inserire esplicitamente i controlli in anticipo.

Non pensare troppo a come sarebbe una soluzione elegante : in primo luogo, cerca di trovare un approccio robusto .

    
risposta data 29.01.2014 - 15:03
fonte
1

Dovresti usare qualche variante del # 1. Non ci si può fidare dei consumatori per controllare le regole per vedere se qualcosa può essere eseguito prima di eseguire qualcosa. Ciò aggiunge l'accoppiamento temporale, anche se la gente lo ricorda.

Modi per migliorarlo:

  • Lazy - supponendo che CanSomethingBeDone non possa mai cambiare durante il ciclo di vita dell'oggetto, puoi inizializzarlo pigramente in modo da non controllarlo ogni chiamata; solo una volta al primo utilizzo.
  • Decoratori . Probabilmente dovresti anche separare meglio CanSomethingBeDone da DoSomething , poiché si tratta di responsabilità separate, che potrebbero non cambiare insieme. Un decoratore sembra un buon approccio qui. Quindi puoi avere un codice DoSomething e un decoratore che controlla la regola prima di chiamare la sua implementazione completata. Ciò mantiene le cose separate e componibili. Anche se questo funziona meglio quando il consumatore non dovrebbe sapere assolutamente nulla.
  • No-Ops - A seconda dell'utilizzo, potrebbe essere preferibile non fare nulla piuttosto che generare un'eccezione se il consumatore non può eseguire DoSomething . Questo semplifica il sito di chiamata, soprattutto se la capacità è esposta e ti aspetti che DoSomething non venga solitamente chiamato perché non sarà cliccabile.
  • Non esporlo : se hai una struttura di composizione / plug-in, potresti essere in grado di nascondere l'intero comando quando non è disponibile. Ciò richiede però un'architettura particolare e alcuni altri vincoli che ti consentono di sapere che il plug-in non è disponibile in anticipo e costantemente.
risposta data 29.01.2014 - 16:37
fonte

Leggi altre domande sui tag