file di elaborazione SRP riga per riga

3

Sto scrivendo una classe che legge le righe da un file, li elabora, li memorizza in un accumulatore e quando raggiunge una soglia di inserimenti di massa in un database SqlServer. In questo modo:

class FooImporterToSqlServer
{
    private string ConnectionString { get;} //initilized via constuctor
    private AccumulatorCollection Accumulators { get;} //initilized via constuctor
    private FooProcessor Processor { get;} //initilized via constuctor
    private int LastHierarchyId { get; set;} //initilized via constuctor, starts at 0

    public void Import(string path)
    {
        var lines = File.ReadAllLines(path);
        foreach(var line in lines)
        {
            var processedLine = Processor.Process(line, ref LastHierarchyId);
            Accumulators.Add(processedLine);
            if(Accumulators.HaveAnyFull())
                UnloadAccumulators();
        }
    }

    private void UnloadAccumulator()
    {
        var fullAccumulators = Accumulators.GetFullAccumulators();
        foreach(var fullAccumulator in fullAccumulators)
        {
            //bulk insert black magic
        }
    }
}

Ma non posso fare a meno di fallire. Sto violando una singola responsabilità con la classe FooImporterToSqlServer . Fa troppo: legge il file, usa l'accumulatore e poi inserisce nel database. Ma anche io non so come separare queste preoccupazioni in questo caso in cui ho bisogno di smettere di elaborare a metà strada. Mantenimento speciale dello stato tra diversi inserimenti. Normalmente ci sarebbe qualcosa come FooReaderToMemory poi FooInsertToSqlServer ma in questo caso sono uno nella stessa cosa. Qualche idea su come posso eseguire il refact in questo modo per non rompere SRP?

EDIT: per chiarimenti, non riesco a leggere, elaborare e quindi importare il file perché le righe elaborate sono troppo da tenere in memoria tutte in una volta.

Non esitare a notare qualsiasi altra cosa che non va nell'esempio che ho fornito!

    
posta manoftheyear 11.10.2017 - 15:42
fonte

3 risposte

2

Ecco il mio suggerimento nel seguente codice. Sto cercando di risolvere i seguenti problemi:

  • Se ho bisogno di cambiare il modo di estrarre i dati (attualmente proviene da un file), voglio cambiare solo una classe (nessun problema se è necessario creare classi aggiuntive);
  • Se devo cambiare il database o il modo di importare i dati, voglio cambiare solo una classe (nessun problema se è necessario creare classi aggiuntive);
  • Se voglio cambiare il processo generale (aggiungendo o modificando i passaggi) voglio cambiare solo una classe;

    class FooImporter
    {
        private IFooImporter Importer { get;} //no more connection string; now the importer is injected (doesn't matter how it imports)
    
        private AccumulatorCollection Accumulators { get;} //initilized via constuctor
        private FooProcessor Processor { get;} //initilized via constuctor
        private IFooExtractor Extractor {get;} //this class does not open files anymore, this is done by the extractor injected
        private int LastHierarchyId { get; set;} //initilized via constuctor, starts at 0
    
        public void Import() //now the path is private detail of the Extractor injected
        {
            using (Extractor.BeginExtract())
            {
                while (Extractor.HasNext())
                {
                    var line = Extractor.GetNext();
                    var processedLine = Processor.Process(line, ref LastHierarchyId);
                    Accumulators.Add(processedLine);
                    if(Accumulators.HaveAnyFull())
                        UnloadAccumulators();
                }
            }
        }
    
        private void UnloadAccumulator()
        {
            var fullAccumulators = Accumulators.GetFullAccumulators();
            foreach(var fullAccumulator in fullAccumulators)
            {
                Importer.Import(fullAcumulator);
            }
        }
    }
    
    class SqlServerFooImporter : IFooImporter  //You could have different importers
    {
        private string ConnectionString { get;} //initilized via constuctor
    
        private void Import(Accumulator fullAcumulator)
        {
            //bulk insert black magic
        }
    }
    

Per riassumere: il codice di esempio mostra una classe che potrebbe avere più motivi per cambiare. Pertanto ho suggerito un disegno su cui ogni classe ha una ragione per cui il suo codice è cambiato; anche per le nuove funzionalità si implementano nuove classi e le si iniettano nell'importatore (proprio come si sta già facendo, ma non per tutti i passaggi del processo).

    
risposta data 11.10.2017 - 20:00
fonte
5

But I can't help but fell I'm breaking encapsulation with the FooImporterToSqlServer class. It does too much: it reads the file, uses the accumulator and then inserts in the database.

incapsulamento? Questa non è una preoccupazione di incapsulamento. Fare molto è una preoccupazione del Principio della singola responsabilità. La preoccupazione di incapsulamento viene da getter accessibili pubblicamente. Quelli potrebbero esporre il tuo stato. Ciò mi lascerebbe a chiedermi quale codice, che non è nemmeno menzionato da questa classe, ha bisogno di usare ConnectionString, Accumulators, Processor e LastHierarchyId?

Per quanto riguarda il principio di responsabilità unica, lo sei. La sola responsabilità di FooImporterToSqlServer è di importare i file. Non mi aspetto di trovare nulla qui che non aiuti a farlo.

    
risposta data 11.10.2017 - 16:44
fonte
1

Come sottolineato da @CandiedOrange, non vedo che stai violando SRP qui. Si dispone di una classe per l'elaborazione dei dati, la classe per l'accumulo di dati e una classe per l'archiviazione dei dati nel database. Ogni classe fa il proprio lavoro. L'unico modo per separarlo di più sarebbe avere una classe che esegua l'analisi dei file e che la classe incapsuli le quattro classi precedenti, ma in questo caso, penso che sarebbe eccessivo.

Tuttavia, parlerò del paragrafo seguente:

EDIT: for clarification, I can't read, process and then import the file because the processed lines are too much to hold in memory all at once.

Un modo per aggirare questo problema è quello che hai implementato. L'altro sarebbe quello di implementare un modello consumatore-produttore, in cui un thread popolerebbe la raccolta di linee e l'altro lo svuoterebbe mentre importerebbe quei dati nel database. In questo modo, il buffer "respirerebbe" e il consumo di memoria si ridurrebbe.

L'approccio che hai scelto potrebbe avere prestazioni migliori, tuttavia, se stai utilizzando alcune tecniche di importazione di massa. Con il tuo approccio, ci sarebbero meno round trip al database.

    
risposta data 11.10.2017 - 19:00
fonte

Leggi altre domande sui tag