Come implementare il principio DRY quando si usa la parola chiave "using"?


23

Considera questi metodi:

public List<Employee> GetAllEmployees()
{
    using (Entities entities = new Entities())
    {
        return entities.Employees.ToList();
    }
}

public List<Job> GetAllJobs()
{
    using (Entities entities = new Entities())
    {
        return entities.Jobs.ToList();
    }
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    using (Entities entities = new Entities())
    {
        return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
    }
}

L'uso del blocco è lo stesso ed è stato ripetuto 3 volte qui (ovviamente, più di 100 volte nella vera applicazione). Come è possibile implementare l'entità DRY (Don't Repeat Yourself) per il usingblocco? È considerata una violazione del principio DRY?

Aggiornamento: non sto parlando di ciò che è stato implementato all'interno del usingblocco. Quello che in realtà intendo qui, è il using (Entities entities = new Entities()). Questa riga viene ripetuta 100 volte o più.


2
è questo C #? La risposta alla tua domanda potrebbe dipendere dalla lingua
David,

Sì @ David, mi dispiace di non aver menzionato la mia lingua. In che modo può influire sulla risposta?
Saeed Neamati,

alcune lingue possono avere una sintassi particolare che può aiutarti a considerare un po 'del tuo codice. Non conosco C #, ma in Ruby penso che potresti usare i blocchi per fattorizzare la parte che usa.
David,

L' istruzione using in realtà fornisce il supporto del linguaggio C # per applicare il principio DRY per aiutare a evitare la codifica ripetitiva mentre gestisce lo smaltimento delle risorse con il modello di progettazione Dispose . Ciò non significa che non possiamo trovare il modo di rendere le cose ASCIUGABILI! Personalmente, penso a DRY come a un processo ricorsivo.
John Tobler,

Risposte:


24

Un'idea sarebbe quella di avvolgerlo con una funzione che accetta a Func.

Qualcosa come questo

public K UsingT<T,K>(Func<T,K> f) where T:IDisposable,new()
{
    using (T t = new T())
    {
        return f(t);
    }
}

Quindi il codice sopra diventa

public List<Employee> GetAllEmployees()
{
    return UsingT<Entities,List<Employee>>(e=>e.Employees.ToList());
}

public List<Job> GetAllJobs()
{
    return UsingT<Entities,List<Job>>(e=>e.Jobs.ToList());
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    return UsingT<Entities,List<Task>>(e=>e.Tasks.Where(t => t.JobId == job.Id).ToList());
}

Ho anche creato Entitiesun parametro tipo, perché presumo che tu abbia più di un tipo con cui lo stai facendo. In caso contrario, è possibile rimuoverlo e utilizzare semplicemente il tipo param per il tipo restituito.

Ad essere onesti, anche se questo tipo di codice non aiuta affatto la leggibilità. Nella mia esperienza, più i colleghi Jr hanno avuto un momento davvero difficile.

Aggiorna Alcune variazioni aggiuntive sugli helper che potresti prendere in considerazione

//forget the Entities type param
public T UsingEntities<T>(Func<Entities,T> f)
{
    using (Entities e = new Entities())
    {
        return f(e);
    }
}
//forget the Entities type param, and return an IList
public IList<T> ListFromEntities<T>(Func<Entities,IEnumerable<T>> f)
{
    using (Entities e = new Entities())
    {
        return f(e).ToList();
    }
}
//doing the .ToList() forces the results to enumerate before `e` gets disposed.

1
+1 Bella soluzione, sebbene non affronti il ​​problema reale (non incluso nella domanda originale), ovvero le numerose occorrenze di Entities.
back2dos,

@Doc Brown: penso di averti battuto con il nome della funzione. Ho lasciato IEnumerablefuori la funzione nel caso in cui ci fossero delle non IEnumerableproprietà di T che il chiamante voleva restituire, ma hai ragione, lo avrebbe ripulito un po '. Forse avere un aiuto sia per Single che per i IEnumerablerisultati sarebbe buono. Detto questo, penso ancora che rallenta il riconoscimento di ciò che fa il codice, specialmente per qualcuno che non è abituato a usare molti generici e lambda (ad esempio i tuoi colleghi che non sono su SO :))
Brook,

+1 Penso che questo approccio vada bene. E la leggibilità può essere migliorata. Ad esempio, inserisci "ToList" in WithEntities, usa Func<T,IEnumerable<K>>invece di Func<T,K>e dai a "WithEntities" un nome migliore (come SelectEntities). E non credo che "Entità" debba essere un parametro generico qui.
Doc Brown,

1
Per far funzionare tutto ciò, i vincoli dovrebbero essere where T : IDisposable, new(), come usingrichiesto IDisposableper funzionare.
Anthony Pegram,

1
@Anthony Pegram: risolto, grazie! (Questo è quello che ottengo per la codifica C # in una finestra del browser)
Brook,

23

Per me questo sarebbe come preoccuparsi di cercare più volte nella stessa collezione: è solo qualcosa che devi fare. Qualsiasi tentativo di astrarlo ulteriormente renderebbe il codice molto meno leggibile.


Grande analogia @Ben. +1
Saeed Neamati,

3
Non sono d'accordo, scusa. Leggi i commenti del PO sull'ambito della transazione e pensa a cosa devi fare quando hai scritto 500 volte quel tipo di codice, e poi nota che dovrai cambiare la stessa cosa 500 volte. Questo tipo di ripetizione del codice può essere ok solo se hai <10 di quelle funzioni.
Doc Brown,

Oh, e non dimenticare, se per ognuno la stessa raccolta più di 10 volte in un modo molto simile, con un codice simile all'interno del tuo per-ciascuno, dovresti assolutamente pensare a una generalizzazione.
Doc Brown,

1
A me sembra solo che tu stia trasformando una tre linee in una oneliner ... ti stai ancora ripetendo.
Jim Wolff,

Dipende dal contesto, se per esempio foreachè su una raccolta molto grande o la logica all'interno del foreachciclo richiede tempo. Un motto che ho imparato ad adottare: non ossessionarti ma fai sempre attenzione al tuo approccio
Coops,

9

Sembra che tu stia confondendo il principio "Once and Only Once" con il principio DRY. Il principio DRY afferma:

Ogni pezzo di conoscenza deve avere un'unica, inequivocabile, autorevole rappresentazione all'interno di un sistema.

Tuttavia, il principio Once and Only Once è leggermente diverso.

Il principio [DRY] è simile a OnceAndOnlyOnce, ma con un obiettivo diverso. Con OnceAndOnlyOnce, sei incoraggiato a eseguire il refactoring per eliminare codice e funzionalità duplicati. Con DRY, si tenta di identificare la fonte unica e definitiva di ogni conoscenza utilizzata nel proprio sistema, quindi utilizzare tale fonte per generare istanze applicabili di tale conoscenza (codice, documentazione, test, ecc.).

Il principio DRY è di solito usato nel contesto della logica attuale, non tanto ridondante usando le istruzioni:

Mantenere la struttura di un programma DRY è più difficile e ha un valore inferiore. Sono le regole aziendali, le dichiarazioni if, le formule matematiche e i metadati che dovrebbero apparire in un solo posto. Le cose WET - pagine HTML, dati di test ridondanti, virgole e {} delimitatori - sono tutte facili da ignorare, quindi asciugarle è meno importante.

fonte


7

Non riesco a vedere l'uso di usingqui:

Che ne dite di:

public List<Employee> GetAllEmployees() {
    return (new Entities()).Employees.ToList();
}
public List<Job> GetAllJobs() {
    return (new Entities()).Jobs.ToList();
}
public List<Task> GetAllTasksOfTheJob(Job job) {
    return (new Entities()).Tasks.Where(t => t.JobId == job.Id).ToList();
}

O ancora meglio, poiché non credo che sia necessario creare un nuovo oggetto ogni volta.

private Entities entities = new Entities();//not sure C# allows for that kind of initialization, but you can do it in the constructor if needed

public List<Employee> GetAllEmployees() {
    return entities.Employees.ToList();
}
public List<Job> GetAllJobs() {
    return entities.Jobs.ToList();
}
public List<Task> GetAllTasksOfTheJob(Job job) {
    return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
}

Per quanto riguarda la violazione di DRY: DRY non si applica a questo livello. In realtà nessun principio lo fa davvero, tranne quello della leggibilità. Cercare di applicare DRY a quel livello è in realtà solo una micro-ottimizzazione architettonica, che come tutta la micro-ottimizzazione è solo una perdita di bici e non risolve alcun problema, ma rischia persino di introdurne di nuovi.
Dalla mia esperienza, so che se si tenta di ridurre la ridondanza del codice a quel livello, si crea un impatto negativo sulla qualità del codice, offuscando ciò che era veramente chiaro e semplice.

Modifica:
Ok. Quindi il problema non è in realtà l'istruzione using, il problema è la dipendenza dall'oggetto che si crea ogni volta. Suggerirei di iniettare un costruttore:

private delegate Entities query();//this should be injected from the outside (upon construction for example)
public List<Employee> GetAllEmployees() {
    using (var entities = query()) {//AFAIK C# can infer the type here
        return entities.Employees.ToList();
    }
}
//... and so on

2
Ma @ back2dos, ci sono molti posti in cui utilizziamo il using (CustomTransaction transaction = new CustomTransaction())blocco di codice nel nostro codice per definire l'ambito di una transazione. Che non può essere raggruppato in un singolo oggetto e in ogni posto in cui desideri utilizzare una transazione, dovresti scrivere un blocco. Ora, cosa succede se si desidera modificare il tipo di tale operazione da CustomTransactiona BuiltInTransactionin più di 500 metodi? Questo mi sembra essere un compito ricorrente e un esempio di violazione del principio DRY.
Saeed Neamati,

3
La presenza di "utilizzo" in questo caso chiude il contesto dei dati, pertanto non è possibile eseguire il caricamento lento al di fuori di questi metodi.
Steven Striga,

@Saeed: Questo è quando guardi all'iniezione di dipendenza. Ma questo sembra essere abbastanza diverso dal caso, come affermato nella domanda.
un CVn il

@Saeed: post aggiornato.
back2dos,

@WeekendWarrior using(in questo contesto) è ancora una "sintassi conveniente" più sconosciuta? Perché è così bello usare =)
Coops il

4

Non solo l'utilizzo è un codice duplicato (tra l'altro è un codice duplicato e in realtà confronta con un'istruzione try..catch..finally) ma anche toList. Rifiuterei il tuo codice in questo modo:

 public List<T> GetAll(Func<Entities, IEnumerable<T>> getter) {
    using (Entities entities = new Entities())
    {
        return getter().ToList();
    }
 }

public List<Employee> GetAllEmployees()
{
    return GetAll(e => e.Employees);
}

public List<Job> GetAllJobs()
{
    return GetAll(e => e.Jobs);
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    return GetAll(e => e.Tasks.Where(t => t.JobId == job.Id));
}

3

Dal momento che qui non esiste alcuna logica aziendale di alcun tipo tranne l'ultima. Non è davvero ASCIUTTO, a mio avviso.

L'ultimo non ha alcun DRY nel blocco using ma immagino che la clausola where dovrebbe cambiare dove mai usata.

Questo è un lavoro tipico per i generatori di codice. Scrivi e copri il generatore di codice e lascialo generare per ogni tipo.


No @arunmur. C'è stato un malinteso qui. Per DRY intendevo using (Entities entities = new Entities())blocco. Voglio dire, questa riga di codice viene ripetuta 100 volte e viene ripetuta sempre di più.
Saeed Neamati,

DRY deriva principalmente dal fatto che è necessario scrivere casi di test che molte volte e un bug in un punto indicano che è necessario risolverlo in 100 punti. L'utilizzo (Entità ...) è un codice troppo semplice da infrangere. Non ha bisogno di essere diviso o inserito in un'altra classe. Se insisti ancora a semplificarlo. Vorrei suggerire una funzione di richiamata Yeild da ruby.
Arunmur,

1
@arnumur: l'utilizzo non è sempre troppo semplice da interrompere. Spesso ho una buona logica che determina quali opzioni utilizzare nel contesto dei dati. È molto probabile che possa essere passata una stringa di connessione errata.
Morgan Herlocker,

1
@ironcode - In quelle situazioni ha senso spingerlo verso una funzione. Ma in questi esempi è piuttosto difficile da rompere. Anche se si interrompe la correzione è spesso nella definizione della stessa classe Entities, che dovrebbe essere coperta con un caso di test separato.
arunmur,

+1 @arunmur - Sono d'accordo. Di solito lo faccio da solo. Scrivo test per quella funzione, ma sembra un po 'esagerato scrivere un test per la tua dichiarazione using.
Morgan Herlocker,

2

Dal momento che stai creando e distruggendo lo stesso oggetto usa e getta più volte, la tua classe è essa stessa un buon candidato per l'implementazione del modello IDisposable.

class ThisClass : IDisposable
{
    protected virtual Entities Context { get; set; }

    protected virtual void Dispose( bool disposing )
    {
        if ( disposing && Context != null )
            Context.Dispose();
    }

    public void Dispose()
    {
        Dispose( true );
    }

    public ThisClass()
    {
        Context = new Entities();
    }

    public List<Employee> GetAllEmployees()
    {
        return Context.Employees.ToList();
    }

    public List<Job> GetAllJobs()
    {
        return Context.Jobs.ToList();
    }

    public List<Task> GetAllTasksOfTheJob(Job job)
    {
        return Context.Tasks.Where(t => t.JobId == job.Id).ToList();
    }
}

Questo ti lascia solo bisogno di "usare" quando crei un'istanza della tua classe. Se non si desidera che la classe sia responsabile dello smaltimento degli oggetti, è possibile fare in modo che i metodi accettino la dipendenza come argomento:

public static List<Employee> GetAllEmployees( Entities entities )
{
    return entities.Employees.ToList();
}

public static List<Job> GetAllJobs( Entities entities )
{
    return entities.Jobs.ToList();
}

public static List<Task> GetAllTasksOfTheJob( Entities entities, Job job )
{
    return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
}

1

Il mio pezzo preferito di magia incomprensibile!

public class Blah
{
  IEnumerable<T> Wrap(Func<Entities, IEnumerable<T>> act)
  {
    using(var entities = new Entities()) { return act(entities); }
  }

  public List<Employee> GetAllEmployees()
  {
    return Wrap(e => e.Employees.ToList());
  }

  public List<Job> GetAllJobs()
  {
    return Wrap(e => e.Jobs.ToList());
  }

  public List<Task> GetAllTasksOfTheJob(Job job)
  {
    return Wrap(e => e.Tasks.Where(x ....).ToList());
  }
}

Wrapesiste solo per sottrarre ciò o qualsiasi magia di cui hai bisogno. Non sono sicuro che lo consiglierei sempre, ma è possibile utilizzarlo. L'idea "migliore" sarebbe quella di utilizzare un contenitore DI, come StructureMap, e limitare l'ambito della classe Entities al contesto della richiesta, iniettarlo nel controller e quindi lasciare che si occupi del ciclo di vita senza che sia necessario il controller.


I parametri di tipo per Func <IEnumerable <T>, Entità> sono nell'ordine sbagliato. (vedi la mia risposta che ha sostanzialmente la stessa cosa)
Brook,

Bene, abbastanza facile da correggere. Non ricordo mai quale sia il giusto ordine. Uso Funcabbastanza s dovrei.
Travis,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.