Disaccoppiamento quando i costruttori hanno parametri non contrattuali

5

Per parametri non contrattuali, intendo i parametri che non sono interfacce o dipendenze del servizio, qualcosa come class Person(string name) .

Sto scrivendo un'applicazione di scraping di una pagina web e finora l'ho scritta nell'ordine sbagliato (da qui la creazione di questa domanda). Ho creato una classe per analizzare documenti HTML (dal formato stringa) e darmi tutti gli URL e le immagini in esso contenuti.

Quella classe ha la seguente descrizione:

public class PageParser
{
    private readonly string _html;

    public PageParser(string html) {
        _html = html;
    }

    public IEnumerable<string> GetImages() {
        /* not important */
    }

    public IEnumerable<string> GetLinks() {
        /* not important */
    }   
}

Questo codice funziona alla grande, testato unitamente, copertura al 100%, ecc ... Il problema ora è come scrivo unit test per il codice che usa questa classe senza preoccuparsi del costruttore di questa classe. Ho solo bisogno che questa nuova classe funzioni con il comportamento di PageParser .

Lo pseudo codice per questa nuova funzionalità è il seguente:

public Report CreateReport(string url)
{
    var html = _webClient.DownloadString(url); 

    var parser = new PageParser(html);  

    var images = parser.GetImages();
    var links = parser.GetLinks();

    var relevantLinks = links.Where(l => l.Contains('something'));

    return _reportBuilder.Create(images, relevantLinks);
}

Il problema che ho è che non voglio avere un gruppo di test unitari che abbiano documenti HTML di dimensioni epiche per tutti i miei vari casi di test che il metodo CreateReport può avere.

Le opzioni che ho trovato sono:

1) Lascia tutto come è

Lascia la struttura della classe così com'è, vivi semplicemente con i test con variabili di configurazione grandi

2) Esporre il parser di pagina (+ interfaccia) da una Factory

Crea un'interfaccia per PageParser e crea una fabbrica con l'interfaccia corrispondente.

interface IPageParserFactory
{
    IPageParser Create(string html);
}

class PageParserFactory : IPageParserFactory
{
    IPageParser Create(string html)
    {
        return new PageParser(html);
    }
}

Quindi lo pseudo codice risultante apparirebbe come segue.

// ... download string

var pageParser = _pageParserFactory.Create(html);

// ... create report

3) Sposta il parametro html in ogni metodo e crea un'interfaccia (quindi ora la classe non ha dipendenze di costruzione)

interface IPageParser
{
    IEnumerable<string> GetImages(string html);
    IEnumerable<string> GetLinks(string html);
}

4) Qualcos'altro completamente

Se c'è un altro pattern che risolve questo problema mi piacerebbe saperlo.

    
posta Matthew 04.08.2014 - 07:42
fonte

1 risposta

11

Da una rapida occhiata, sembra che il tuo metodo CreateReport stia violando il principio della responsabilità unica, poiché comporta un sacco di cose:

  • legge HTML dall'URL tramite DownloadString
  • crea PageParser e recupera i risultati di analisi
  • filtra il risultato in base a un criterio di pertinenza
  • fa la vera creazione di un rapporto

Dato il nome del metodo, ci si aspetterebbe che quel metodo abbia solo quell'ultima responsabilità. Quello che hai dato qui invece è qualcosa che è altamente accoppiato con il recupero dei dati, la logica di analisi e filtraggio.

L'idea di creare un report si basa di solito su una sorta di dati già preparati. Quindi si inizia già a chiedersi, perché il tuo metodo accetta un URL come input?

Dalle soluzioni proposte 1) a 3), nessuno risolve veramente questi problemi. Quando si implementa uno di questi, sarà comunque necessario creare file locali con contenuti HTML più o meno completi per i propri test unitari.

Si noti che alcune persone arrivano addirittura a sostenere che nessun test unitario dovrebbe accedere al filesystem, perché generalmente è troppo lento. Certo, non si può essere d'accordo con questo, ma almeno dovrebbe farti sentire un po 'colpevole, così che lo fai solo se hai una buona ragione per farlo ... e in questo caso non ne hai uno .

Quindi andiamo con l'opzione 4) qualcos'altro interamente. Dando uno sguardo diverso al tuo problema, vediamo che si tratta davvero di gestire una sorta di dati del documento. Il tuo PageParser è il primo a creare tali dati, quindi che ne dici di cambiarli per non restituire il singolo IEnumerable s, ma invece un singolo oggetto PageData , che a sua volta è in grado di fornire IEnumerable s.

Continuando con questa linea, tuttavia, una generazione di report richiede solo un oggetto PageData . Il tuo _reportBuilder.Create(images, relevantLinks) diventa una chiamata a _reportBuilder.Create(pageData) e il tuo CreateReport prende un PageData invece di un URL come input. In realtà, CreateReport può essere abbreviato a quella riga Create . Tutto il resto non era di sua responsabilità comunque.

Al di fuori di una di queste classi ( PageParser , PageData , classe del generatore di report) è possibile definire un'interfaccia PageDataFilter indipendente e classi di implementazione. Uno di questi sarà il filtro che hai inserito in CreateReport . Si noti che l'interfaccia è un semplice metodo di filtraggio a un metodo dell'ordinamento PageData ApplyFilter(PageData pageData) . Trattare con filtri come questo offre i vantaggi di una facile testabilità dei singoli filtri, una facile composizione dei filtri e disaccoppia i filtri da qualsiasi altra cosa. Il generatore di report non deve preoccuparsi se il suo dato PageData è stato filtrato in un modo o nell'altro. (Nota che per l'implementazione devi decidere se PageData è immutabile, o può essere modificato da filtri sul posto. La prima opzione è matematicamente solida, facile da ragionare, più adatta alla parallelizzazione, ecc., Mentre il secondo è più efficiente in termini di memoria e cpu.)

A questo punto, puoi facilmente testare il tuo PageParser , il tuo generatore di rapporti e il tuo filtro link. Inoltre, è possibile eseguire tutti questi test semplicemente preparando una semplice istanza di una classe richiesta in memoria. Niente più caricamento dell'URL, niente più materiale HTML coinvolto al di fuori dei test di PageParser , niente più accessi al file system e nessun altro accoppiamento che deve essere curato nei test.

Addendum: che cosa manca il testo sopra è come questi componenti sono collegati insieme. Supponiamo ora che tu abbia un PageParser che crea PageData che puoi filtrare tramite PageDataFilter istanze e creare rapporti da ReportCreator . Ciò di cui hai ancora bisogno è l'istanziazione e la strumentazione di queste classi - da qualche parte più lontano. Dipende dal design della tua applicazione su dove sarebbe il posto adatto. Ma per semplicità, potresti assumere una classe Application con un metodo main . In questo metodo devi fare quanto segue:

  • Scarica l'URL HTML su una stringa
  • Crea un'istanza di PageParser con questa stringa come argomento del costruttore
  • Crea un'istanza degli oggetti PageDataFilter desiderati
  • Applica gli oggetti PageDataFilter a PageData erogato da PageParser
  • Crea un'istanza di ReportCreator e assegnagli il PageData filtrato per la creazione di un rapporto.

Anche in questo caso, dipende dal design generale e dall'architettura, se queste cose sono istanziate manualmente, iniettate in dipendenza o cosa-hai. Il punto principale però è: mantieni le tue classi piccole e limitate alla loro unica e sola responsabilità, quindi strumentale dall'esterno. Se il codice della strumentazione diventa più grande e fa troppe cose, allora applica in modo ricorsivo le idee precedenti a quel codice.

    
risposta data 04.08.2014 - 08:20
fonte