Le classi non dovrebbero chiamare altre classi? [chiuso]

2

Ho appena finito di leggere The Art of Unit Testing , di Roy Osherove. È stato un libro interessante, ma ho problemi con qualcosa che menziona verso la fine (sezione 11.2.2):

Identifying "roles" in the application and abstracting them under interfaces is an important part of the design process. An abstract class shouldn’t call concrete classes, and concrete classes shouldn’t call concrete classes either, unless they’re data objects (objects holding data, with no behavior). This allows you to have multiple seams in the application where you could intervene and provide your own implementation.

In particolare, non capisco cosa intenda quando dice che le lezioni concrete non dovrebbero chiamare lezioni concrete. Considera i seguenti esempi in C #:

double roundedAge = Math.Floor(exactAge);

Non è considerata una chiamata a una classe perché Math.Floor è un metodo statico? O è Osherove che dice che la riga di codice sopra è cattiva progettazione? Un altro esempio:

using (StreamReader reader = new StreamReader(path))
{
    // do things
}

Questo cattivo design è dovuto all'uso di uno StreamReader? Oppure questa istanza di StreamReader è semplicemente un oggetto che contiene i dati, come descritto nella citazione sopra?

Piuttosto che iniziare una discussione sull'opportunità o meno di fare in modo che le classi chiamino altre classi, il mio obiettivo qui è cercare di capire meglio cosa significa Osherove nel brano che ho citato. Intende davvero che il codice nei due esempi sopra dovrebbe essere evitato? Grazie in anticipo per eventuali risposte.

EDIT: Di seguito è riportata un'illustrazione del libro che è probabilmente delucidante.

    
posta Brian Snow 02.05.2014 - 01:36
fonte

2 risposte

6

dichiarazione di non responsabilità : non ho verificato che il codice di questa risposta sia effettivamente compilato: è solo lì per dimostrare un punto.

Credo che il peggior anti-pattern sia Cargo Cult Programming - seguire ciecamente i modelli di design. Affermo che è molto peggio di qualsiasi altro anti-pattern, perché mentre l'altro anti-pattern indica almeno un processo di pensiero (errato) dall'applicatore, Cargo Cult Programming chiude la mente in modo che non si interrompa nell'atto di applicando modelli di progettazione inutili.

Nel nostro caso, sembra che il modello Dependency Inversion sia oggetto di culto del carico. L'utilizzo di DI per ogni classe (salvo per le classi di soli dati) è impossibile, poiché a un certo punto è necessario costruire classi concrete. Per lo meno, alcuni metodi statici e tutte le classi di fabbrica dovrebbero essere esenti da questa regola.

Questa esenzione renderà il compito possibile, ma ancora inutilmente difficile. La chiave che sta dietro all'utilizzo di Dependency Injection è capire che è necessario disaccoppiare moduli (o componenti), non classi. Le classi dello stesso modulo dovrebbero poter utilizzare altre classi concrete dello stesso modulo. Ovviamente - decidere dove vanno i confini tra i componenti - quali classi appartengono a ciascun componente - richiede il pensiero, qualcosa che è proibito nelle sette del carico.

Un'altra cosa importante è che alcune cose sono troppo piccole per poter essere usate solo per interfaccia, e alcune cose sono troppo alte per le sole interfacce di utilizzo.

Il tuo esempio di Math.Floor è ed esempio di "troppo piccolo per can-be-used-by-interface-only". L'inversione della dipendenza qui richiede qualcosa come:

interface IMath{
    public double Floor(double d);
}

public void Foo(IMath math){
    // ...
    double roundedAge=math.Floor(exactAge);
    // ...
}

Questo ci permette di fare cose come:

  • Chiama Foo con un'altra implementazione di IMath che implementa Floor in modo diverso
  • Mock IMath nei test unitari - chiama Foo con un oggetto IMath che non fa realmente la pavimentazione, fa solo finta che lo faccia.

Devi essere molto profondo nel culto del carico per ignorare il fatto che questi due benefici sono completamente inutili qui. È difficile immaginare un metodo che faccia Floor meglio di System.Math.Floor , e qualsiasi tentativo di deriderlo - a meno che non funzioni solo per casi specifici - sarà più complicato e prune di errori che semplicemente facendo il dannato pavimento.

Confronta con i costi:

  • Dovendo inviare un'implementazione IMath a Foo .
  • L'implementazione di Foo (utilizzando un oggetto concreto IMath ) è esposta dalla sua interfaccia (richiede un oggetto IMath come argomento).
  • Anche tutti i metodi che utilizzano Foo avranno questi costi, dal momento che non può utilizzare direttamente un% di% di co_de concreto.

ed è facile vedere che non vale la pena in questo caso. Un esempio di "troppo alto per le sole interfacce di utilizzo" è il tuo metodo IMath , che deve effettivamente creare oggetti concreti a cui non può essere iniettato.

Il tuo esempio di Main è in realtà un buon esempio per qualcosa che può possibilmente beneficiare di Dependency Inversion:

  • È possibile che tu voglia fornire un'implementazione diversa di StreamReader , ad esempio una che legge e decrittografa un flusso crittografato.
  • È possibile che tu voglia simulare TextReader in un test di unità, quindi non avrai bisogno di un file effettivo per esistere in un percorso specifico quando esegui il tuo test di unità e puoi invece fornire il testo che sarebbe leggere direttamente nel test dell'unità.

Ancora: a un certo punto è necessario creare un oggetto concreto. Qui dovremmo trovare un luogo "troppo alto per le sole interfacce di utilizzo", in cui possiamo creare TextReader o un oggetto factory che può creare TextReader e iniettarlo fino a quando non viene utilizzato:

interface ITextReaderFactory{
    TextReader CreateTextReader(string path);
}


public void Bar(ITextReaderFactory textReaderFactory){
    // ...
    using(TextReader reader=textReaderFactory.CreateTextReader(path)){
        // do things
    }
    // ...
}

class StreamReaderFactory : ITextReaderFactory{
    public TextReader CreateTextReader(string path){
        return new StreamReader(path);
    }
}

public static void Main(string[] args){
    Bar(new StreamReaderFactory());
}
    
risposta data 02.05.2014 - 02:57
fonte
0

Il primo esempio non si applica poiché è essenzialmente un'espressione aritmetica. Raramente si scrivono test unitari per operazioni aritmetiche; quando lo fai, il tuo approccio sarebbe molto diverso.

Nel secondo esempio, il codice sta usando la classe concreta. Ciò significa che il codice all'interno delle parentesi graffe è più difficile da testare poiché si sta vincolando il StreamReader in fase di compilazione. Quello che l'autore sta descrivendo è rimandare il binding di reader il più tardi possibile. Quando lo fai, diventa possibile testare l'uso di reader con meno dolore.

Detto questo, molti degli strumenti di derisione del codice fanno un lavoro sufficientemente buono per consentire la sostituzione dell'elemento deriso per la cosa reale. Questo è l'intento della prescrizione: mantieni quante "giunture" utili che puoi nel codice, perché fai tutti i test alle cuciture.

    
risposta data 02.05.2014 - 02:01
fonte

Leggi altre domande sui tag