Avvolgi tutte le chiamate esterne con flag per combattere la ricorsione e la doppia entrata?

3

La produzione di output prevedibile per ogni input possibile è responsabilità di ciascun modulo. Ad esempio (in C #):

class Logger
{
    public ITextWriter Writer { get; set; }

    private uint counter;

    /// <summary>
    /// Writes message in special format and returns the number of total messages written
    /// </summary>
    public uint Debug(string message)
    {
        if (message == null) throw new ArgumentNullException("message");

        if (Writer == null) throw new InvalidOperationException("Writer not set");

        Writer.Write(string.Format("{0:HHmmss}: [DEBUG] {1}", DateTime.Now, message));

        return ++counter;
    }
}
Il modulo

Logger è abbastanza dettagliato, ma è in output - l'eccezione, la chiamata a una depedency e il valore restituito - è prevedibile e ovvia per ogni argomento e stato.

Ma un possibile caso d'uso mi dà fastidio. Cosa succede se Writer è stato impostato su qualche strana implementazione che in qualche modo chiama il metodo Logger.Debug su quello stesso oggetto? La prima possibile conseguenza è lo stackoverflow dovuto alla ricorsione infinita. Secondo: output imprevedibile e possibili bug. È ovvio che tale situazione deve essere controllata in qualche modo. Se vogliamo un output prevedibile per il nostro logger, dovrebbe controllare le doppie voci:

    // ...
    private bool enter;

    public uint Debug(string message)
    {
        if (message == null) throw new ArgumentNullException("message");

        if (Writer == null) throw new InvalidOperationException("Writer not set");

        if (enter) throw new InvalidOperationException("Double-entry");

        enter = true;

        Writer.Write(string.Format("{0:HHmmss}: [DEBUG] {1}", DateTime.Now, message));

        enter = false;

        return ++counter;
    }
    // ...

E sembra che ogni chiamata esterna (una chiamata non ai propri componenti) debba essere avvolta con tali flag. Sembra pazzesco!

È scritto il codice? O è normale credere che il tuo sistema non abbia chiamate circolari? Mi sto perdendo qualcosa di importante? Per favore, consiglia.

Aggiorna

È ancora peggio, ragazzi:

    // ...

    private bool enter;

    public uint Debug(string message)
    {
        if (message == null) throw new ArgumentNullException("message");

        if (Writer == null) throw new InvalidOperationException("Writer not set");

        if (enter) throw new InvalidOperationException("Double-entry");

        try
        {
            enter = true;

            Writer.Write(string.Format("{0:HHmmss}: [DEBUG] {1}", DateTime.Now, message));
        }
        finally
        {
            enter = false;
        }

        return ++counter;
    }
    // ...

Credo che prendere un'eccezione sia a carico del modulo di livello superiore (quello che li ha creati tutti), quindi le eccezioni devono seguire la loro strada. Ma lo stato dopo la chiamata non riuscita deve rimanere corretto. L'eccezione non è la fine del mondo e i moduli possono essere riutilizzati.

    
posta astef 10.09.2014 - 14:38
fonte

4 risposte

3

Se utilizzi progetto per contratto , puoi specificare che le implementazioni di ITextWriter non devono chiamare Logger.Debug .

Solo se è ovvio che alcune implementazioni avrebbero un motivo per chiamare Logger.Debug , ti daresti la pena di difendersi da questo caso.

Una domanda più interessante sarebbe una situazione multi-thread. La responsabilità della gestione delle chiamate multi-thread ricade a Logger o all'implementazione di ITextWriter ? O la tua biblioteca non gestisce il caso della sicurezza del thread? (Attualmente la tua implementazione di Logger è sicuramente non thread-safe a causa dell'incremento del membro counter ).

Come puoi vedere, il tuo codice può gestire solo i "noti conosciuti" per così dire.

    
risposta data 10.09.2014 - 14:49
fonte
0

In generale fai qualcosa di sano e poi a un certo punto l'uso scorretto della tua libreria diventa il problema del programmatore.

Ad esempio per la registrazione è possibile consultare il collegamento . Lo scrittore è personalizzabile in base al livello di scrittura del registro. E poi semplicemente non hai il tuo output di debug usando uno scrittore che sta usando il framework di logging per fare il debugging. A meno che non lo si voglia, nel qual caso lo scrittore dovrebbe guardarsi da una ricorsione infinita guardando il livello di registrazione e quindi decidendo se desidera rilasciare una dichiarazione di debug potenzialmente problematica.

Questa è una soluzione abbastanza buona. Qualsiasi scrittore dovrebbe documentare se stesso come non adatto per l'uso con il livello di debug, o dovrebbe avere quella guardia per non provare a eseguire il debug di se stesso quando usato così.

    
risposta data 10.09.2014 - 19:47
fonte
0

And it seems that every external call (a call not to it's own components) must be wrapped with such flags. That looks crazy! Is it how good code written? Or is it normal to beleive that your system has no circular > calls? Am I missing something important? Please, advise.

Levelization.

La livellizzazione sarà un concetto chiave per molto tempo a venire. L'idea è che pezzi di codice o componenti siano separati in livelli a seconda di cosa si concentrano sul fare. Suggerisco all'OP di pensare a livelli di metodi: i metodi più alti chiameranno solo quelli più bassi. Viceversa, se gli autori prendono una scorciatoia scrivendo meno metodi che sono tutti pari, hanno bisogno di logica di guardia ad alta manutenzione e iniziano a diventare ridicolmente simili agli spaghetti. La livellizzazione si adatta bene. Utenti futuri: cercalo

Ecco un risultato recente di Google:

link

    
risposta data 10.09.2014 - 19:17
fonte
0

Se questa è una cosa di cui sei veramente preoccupato, allora è chiaramente una preoccupazione trasversale, quindi è una cosa che potresti affrontare con un approccio di programmazione orientata agli aspetti.

Ad esempio, è possibile utilizzare DynamicProxy di Castle e scrivere un intercettore che tenga traccia della variabile entry e genera un'eccezione se lo stesso metodo viene immesso una seconda volta. Se hai bisogno di un comportamento più granulare, come se alcuni metodi potessero recitare o avere un numero massimo di voci consentite superiore a uno, puoi controllarlo con gli attributi. Ovviamente questo approccio ti darà comunque delle eccezioni piuttosto che la sicurezza in fase di progettazione.

In generale, tuttavia, direi che questa preoccupazione è al di fuori di ciò di cui un modulo dovrebbe preoccuparsi. Ad esempio, supponiamo di avere il seguente metodo:

public void CreateItem()
{
    _itemStorer.Create();
}

Non diventa molto più semplice di così. Ma cosa succede se quando viene chiamato questo, lo stack è già quasi pieno (non necessariamente a causa di una ricorsione) e qualsiasi chiamata in ulteriori metodi causerà un overflow dello stack? O cosa succede se lo stack è pieno a metà, ma _itemStorer mette abbastanza elementi su di esso con Create per sovrasfruttarlo? Questo semplice metodo dovrebbe assumersi la responsabilità di conoscere l'utilizzo dello stack di tutto quanto sopra e sotto di esso? Sicuramente in questo modo giace la follia.

Oppure, una versione ancora più semplice, e se il giorno dopo hai scritto questo pezzo di codice, qualcuno entra e sostituisce tutta l'implementazione di Create con throw new NotImplementedException() ? Dovresti essere in grado di gestire anche quello?

Un modulo di codice dovrebbe assumersi la responsabilità di se stesso, il che significa comportarsi e interagire con le sue dipendenze in modo sicuro e sano. Ci si dovrebbe aspettare che non si assuma la responsabilità delle sue dipendenze. Ricorda che se A prende B come dipendenza, allora B viene utilizzato per soddisfare un'esigenza che A ha, ed è responsabilità di B farlo correttamente.

    
risposta data 17.09.2014 - 19:19
fonte

Leggi altre domande sui tag