2013-03-16 21 views
32

Contesto/Domanda

Ho lavorato su numerosi progetti .NET che sono stati necessari per salvare i dati e che hanno di solito finito per usare uno schema Repository. Qualcuno sa di una buona strategia per rimuovere tutto il codice boilerplate senza sacrificare la scalabilità del codice?C# - composizione oggetto - Rimozione Codice Boilerplate

Inheritance strategia

Perché così gran parte del codice repository è piatto della caldaia e deve essere ripetuto io normalmente creare una classe di base per coprire i principi fondamentali come la gestione delle eccezioni, la registrazione e il supporto delle transazioni, nonché alcuni CRUD metodi:

public abstract class BaseRepository<T> where T : IEntity 
{ 
    protected void ExecuteQuery(Action query) 
    { 
     //Do Transaction Support/Error Handling/Logging 
     query(); 
    }  

    //CRUD Methods: 
    public virtual T GetByID(int id){} 
    public virtual IEnumerable<T> GetAll(int id){} 
    public virtual void Add (T Entity){} 
    public virtual void Update(T Entity){} 
    public virtual void Delete(T Entity){} 
} 

Quindi questo funziona bene quando ho un dominio semplice, posso creare rapidamente una classe di repository DRY per ogni entità. Tuttavia, questo inizia a scomparire quando il dominio diventa più complesso. Diciamo che è stata introdotta una nuova entità che non consente aggiornamenti. Posso suddividere le classi di base e spostare il metodo di aggiornamento in una classe diversa:

public abstract class BaseRepositorySimple<T> where T : IEntity 
{ 
    protected void ExecuteQuery(Action query); 

    public virtual T GetByID(int id){} 
    public virtual IEnumerable<T> GetAll(int id){} 
    public virtual void Add (T entity){} 
    public void Delete(T entity){} 
} 

public abstract class BaseRepositoryWithUpdate<T> : 
    BaseRepositorySimple<T> where T : IEntity 
{ 
    public virtual void Update(T entity){} 
} 

Questa soluzione non scala bene. Diciamo che ho diverse entità che hanno un metodo comune: archivio virtuale pubblico vuoto (entità T) {}

ma alcune Entità che possono essere archiviate possono anche essere aggiornate mentre altre no. Quindi la mia soluzione di ereditarietà si rompe, dovrei creare due nuove classi base per affrontare questo scenario.

strategia compoisition

Ho esplorato il modello Compositon, ma questo sembra lasciare un sacco di codice refusi:

public class MyEntityRepository : IGetByID<MyEntity>, IArchive<MyEntity> 
{ 
    private Archiver<MyEntity> _archiveWrapper;  
    private GetByIDRetriever<MyEntity> _getByIDWrapper; 

    public MyEntityRepository() 
    { 
     //initialize wrappers (or pull them in 
     //using Constructor Injection and DI) 
    } 

    public MyEntity GetByID(int id) 
    { 
     return _getByIDWrapper(id).GetByID(id); 
    } 

    public void Archive(MyEntity entity) 
    { 
     _archiveWrapper.Archive(entity)' 
    } 
} 

Il MyEntityRepository è ora caricato con codice standard. C'è uno strumento/modello che posso usare per generare automaticamente questo?

Se potessi girare la MyEntityRepository in qualcosa di simile, penso che sarebbe di gran lunga l'ideale:

[Implement(Interface=typeof(IGetByID<MyEntity>), 
    Using = GetByIDRetriever<MyEntity>)]  
[Implement(Interface=typeof(IArchive<MyEntity>), 
    Using = Archiver<MyEntity>) 
public class MyEntityRepository 
{ 
    public MyEntityRepository() 
    { 
     //initialize wrappers (or pull them in 
     //using Constructor Injection and DI) 
    } 
} 

Aspect Oriented Programming

Ho guardato in utilizzando un framework AOP per questo, in particolare PostSharp e il loro Composition Aspect, che sembra che dovrebbe fare il trucco, ma al fine di utilizzare un repository dovrò chiamare Post.Cast <>(), che aggiunge un odore molto strano al codice. Qualcuno sa se c'è un modo migliore per usare AOP per aiutare a sbarazzarsi del codice boilerplate del compositor?

Custom Code Generator

Se tutto il resto fallisce, suppongo che potrei lavorare alla creazione di una spina di Visual Studio generatore di codice personalizzato in che potrebbe generare il codice piastra della caldaia in un file di codice parziale. C'è già uno strumento là fuori che farebbe questo?

[Implement(Interface=typeof(IGetByID<MyEntity>), 
    Using = GetByIDRetriever<MyEntity>)]  
[Implement(Interface=typeof(IArchive<MyEntity>), 
    Using = Archiver<MyEntity>) 
public partial class MyEntityRepository 
{ 
    public MyEntityRepository() 
    { 
     //initialize wrappers (or pull them in 
     //using Constructor Injection and DI) 
    } 
} 

//Generated Class file 
public partial class MyEntityRepository : IGetByID<MyEntity>, IArchive<MyEntity> 
{ 
    private Archiver<MyEntity> _archiveWrapper;  
    private GetByIDRetriever<MyEntity> _getByIDWrapper; 

    public MyEntity GetByID(int id) 
    { 
     return _getByIDWrapper(id).GetByID(id); 
    } 

    public void Archive(MyEntity entity) 
    { 
     _archiveWrapper.Archive(entity)' 
    } 
} 

Metodi di estensione

dimenticato di aggiungere questo quando ho scritto inizialmente la questione (mi dispiace).Ho anche provato a sperimentare con metodi di estensione:

public static class GetByIDExtenions 
{ 
    public T GetByID<T>(this IGetByID<T> repository, int id){ }   
} 

Tuttavia, questo ha due problemi, a) che avrei dovuto ricordare lo spazio dei nomi della classe metodi di estensione e aggiungerlo ovunque e b) i metodi di estensione non può soddisfare le dipendenze di interfaccia:

public interface IMyEntityRepository : IGetByID<MyEntity>{} 
public class MyEntityRepository : IMyEntityRepository{} 

Aggiornamento: sarebbe T4 Templates essere una possibile soluzione?

+1

Cerca Codesmith per la generazione del codice, se non lo hai già fatto, consente modelli personalizzati e regole molto granulari che puoi definire per generare codice C#. Sto pensando ad una soluzione che includa attributi personalizzati e un enumerato Flags, ma non sono sicuro se sia quello che vuoi. –

+0

Grazie a @DavidKhaykin, guardando ora CodeSmith, sembra promettente. Non sai di un blog specifico o di un tutorial che risolva questo particolare problema, vero? –

+0

Purtroppo non sono a conoscenza di nulla per questo specifico caso senza fare qualche ricerca. –

risposta

11

Ho una singola interfaccia di repository generica, che viene implementata una sola volta per una particolare archiviazione di dati. Eccolo:

public interface IRepository<T> where T : class 
{ 
    IQueryable<T> GetAll(); 
    T Get(object id); 
    void Save(T item); 
    void Delete(T item); 
} 

ho implementazioni di esso per locale di deposito EntityFramework, NHibernate, RavenDB. Inoltre ho un'implementazione in memoria per il test delle unità.

Per esempio, ecco un parte del repository collezione a base di in-memory:

public class InMemoryRepository<T> : IRepository<T> where T : class 
{ 
    protected readonly List<T> _list = new List<T>(); 

    public virtual IQueryable<T> GetAll() 
    { 
     return _list.AsReadOnly().AsQueryable(); 
    } 

    public virtual T Get(object id) 
    { 
     return _list.FirstOrDefault(x => GetId(x).Equals(id)); 
    } 

    public virtual void Save(T item) 
    { 
     if (_list.Any(x => EqualsById(x, item))) 
     { 
      Delete(item); 
     } 

     _list.Add(item); 
    } 

    public virtual void Delete(T item) 
    { 
     var itemInRepo = _list.FirstOrDefault(x => EqualsById(x, item)); 

     if (itemInRepo != null) 
     { 
      _list.Remove(itemInRepo); 
     } 
    } 
} 

interfaccia repository generico mi libera dalla creazione di una sacco di classi simili. Hai solo un'implementazione di repository generica, ma anche la libertà di interrogare.

IQueryable<T> risultato da GetAll() metodo consente di effettuare qualsiasi query desidero con i dati e separarli dal codice specifico di archiviazione. Tutti gli ORM .NET popolari hanno i propri provider LINQ, e tutti dovrebbero avere quel magico metodo GetAll() - quindi non ci sono problemi qui.

Preciso implementazione repository nella radice composizione utilizzando contenitore CIO:

ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>)); 

Nei test che sto usando il suo in-memory di sostituzione:

ioc.Bind(typeof (IRepository<>)).To(typeof (InMemoryRepository<>)); 

Se voglio aggiungere più business -specifiche specifiche per il repository, aggiungerò un metodo di estensione (simile al metodo di estensione nella risposta):

public static class ShopQueries 
{ 
    public IQueryable<Product> SelectVegetables(this IQueryable<Product> query) 
    { 
     return query.Where(x => x.Type == "Vegetable"); 
    } 

    public IQueryable<Product> FreshOnly(this IQueryable<Product> query) 
    { 
     return query.Where(x => x.PackTime >= DateTime.Now.AddDays(-1)); 
    } 
} 

Così si può utilizzare e miscelare questi metodi nelle query livello di logica di business, risparmiando verificabilità e facilità di implementazione del repository, come:

var freshVegetables = repo.GetAll().SelectVegetables().FreshOnly(); 

Se non si desidera utilizzare uno spazio dei nomi diverso per quei metodi di estensione (come me) - ok, mettili nello stesso spazio dei nomi in cui risiede l'implementazione del repository (come MyProject.Data) o, ancora meglio, in uno spazio dei nomi specifico aziendale esistente (come MyProject.Products o MyProject.Data.Products). Non c'è bisogno di ricordare spazi dei nomi aggiuntivi ora.

Se si dispone di una logica di repository specifica per un tipo di entità, creare una classe di repository derivata che sostituisce il metodo desiderato.Ad esempio, se i prodotti si possono trovare solo da ProductNumber invece di Id e non supportano la cancellazione, è possibile creare questa classe:

public class ProductRepository : RavenDbRepository<Product> 
{ 
    public override Product Get(object id) 
    { 
     return GetAll().FirstOrDefault(x => x.ProductNumber == id); 
    } 

    public override Delete(Product item) 
    { 
     throw new NotSupportedException("Products can't be deleted from db"); 
    } 
} 

E fare CIO tornare questa implementazione repository specifica per i prodotti:

ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>)); 
ioc.Bind<IRepository<Product>>().To<ProductRepository>(); 

È così che partecipo con i miei repository;)

+0

+1 non è difficile creare un repository generico. Questo mostra come. –

+0

In breve: utilizzare ereditarietà e NotSupportedExceptions, metodi di estensione come helper, nello spazio dei nomi radice per un accesso rapido. Sembra un buon compromesso. – Algirdas

+5

Sono contrario all'uso di Iqueryable in un repository perché non ha nulla a che fare con un repository. IQueryable specifica COME per creare una query. Non devi dire al repository COME fare il suo lavoro, diglielo COSA fare. Al massimo puoi passare un criterio di selezione per le domande. Il repository generico funziona meglio con DDD dove ogni radice aggregata è serializzata. Per tutti gli altri casi, fallisce un po 'ed è solo un'astrazione di usanze su un ORM (che a sua volta è un'astrazione). – MikeSW

4

Acquista file T4 per la generazione del codice. T4 è integrato in Visual Studio. See a tutorial here.

Ho creato file T4 per la generazione di codice entità POCO ispezionando un DBML LINQ e per i loro repository, penso che sarebbe utile qui. Se generi classi parziali con il tuo file T4, potresti semplicemente scrivere il codice per i casi speciali.

+0

Ho dato un'occhiata alla documentazione di T4 in precedenza, ma non sono riuscito a trovare un buon esempio per questo scenario, cioè dove potrei specificare in codice quale implementazione Concrete volevo per una determinata interfaccia. Qualche idea dove potrei trovare qualcosa? –

+1

Welp, non ho alcun collegamento per te, ma ... Io uso una convenzione sulla strategia di configurazione ... Quindi per le entità capaci di archivio di cui hai parlato, c'è qualcosa nell'entità che rende chiaro che è archiviabile? Ad esempio, se l'entità ha un campo IsArchived, il tuo file T4 potrebbe usare reflection per cercare quel campo e, se trovato, creare il repository con un metodo di archiviazione, ecc. Ti aiuta? – sidesinger

+0

In alternativa, potrebbe anche avere un piccolo file di configurazione che elenca tutte le entità e il tipo di repository che si desidera e il T4 lo leggerà e sputerà il codice completo. – sidesinger

2

Per me, sembra che si dividano le classi base e quindi si desideri la funzionalità da entrambe in una classe ereditaria. In tal caso, la composizione è la scelta. Anche l'ereditarietà di classi multiple sarebbe piacevole se C# l'avesse supportata. Tuttavia, poiché ritengo che l'ereditarietà sia più bella e la riusabilità sia ancora valida, la mia prima opzione potrebbe essere accettata.

Opzione 1

avrei preferito un altro classe di base al posto della composizione dei due. La riusabilità può essere risolta anche con metodi statici piuttosto che con l'ereditarietà:

La parte riutilizzabile non è visibile all'esterno. Non c'è bisogno di ricordare lo spazio dei nomi.

Ovviamente c'è qualche piccolo codice ripetuto, ma questo è solo chiamando le funzioni già pronte dalla libreria comune.

Opzione 2

mia implementazione della composizione (o imitazione della ereditarietà multipla):

public class Composite<TFirst, TSecond> 
{ 
    private TFirst _first; 
    private TSecond _second; 

    public Composite(TFirst first, TSecond second) 
    { 
     _first = first; 
     _second = second; 
    } 

    public static implicit operator TFirst(Composite<TFirst, TSecond> @this) 
    { 
     return @this._first; 
    } 

    public static implicit operator TSecond(Composite<TFirst, TSecond> @this) 
    { 
     return @this._second; 
    } 

    public bool Implements<T>() 
    { 
     var tType = typeof(T); 
     return tType == typeof(TFirst) || tType == typeof(TSecond); 
    } 
} 

eredità e la composizione (in basso):

class Basic 
{ 
    public void SelectAll() { Console.WriteLine("SelectAll"); } 
} 

class ChildWithUpdate : Basic 
{ 
    public void Update() { Console.WriteLine("Update"); } 
} 

class ChildWithArchive : Basic 
{ 
    public void Archive() { Console.WriteLine("Archive"); } 
} 

Composizione. Non sono sicuro se questo è sufficiente per dire che non esiste alcun codice di codice.

class ChildWithUpdateAndArchive : Composite<ChildWithUpdate, ChildWithArchive> 
{ 
    public ChildWithUpdateAndArchive(ChildWithUpdate cwu, ChildWithArchive cwa) 
     : base(cwu, cwa) 
    { 
    } 
} 

codice utilizzando tutto questo sembra un po 'OK, ma ancora insolito (invisibile) Tipo getta nelle assegnazioni. Si tratta di un pay off per avere il codice meno boilerplate:

ChildWithUpdate b; 
ChildWithArchive c; 
ChildWithUpdateAndArchive d; 

d = new ChildWithUpdateAndArchive(new ChildWithUpdate(), new ChildWithArchive()); 
//now call separated methods. 
b = d; 
b.Update(); 
c = d; 
c.Archive(); 
+1

Questa è una soluzione davvero interessante. Tuttavia, vedo un problema con la classe Composite che utilizza solo due parametri generici. Puoi nidificarli in modo simile a un decoratore 'classe FooBarFiz: Composite >' o dovresti avere una catena di classi di base composite simili a Action e Func, ovvero 'Composite : Composite {}'? Inoltre, conosci qualche framework che possa aiutare a generare o fornire le classi composite? –

+0

O nidificare o concatenare funzionerebbe con la classe fornita. Ho visto T4 e Codesmith al lavoro e entrambi possono aiutarti. Non ho idea della generazione del codice specifico del composito. – Tengiz

1

Ecco la mia versione:

interface IGetById 
{ 
    T GetById<T>(object id); 
} 

interface IGetAll 
{ 
    IEnumerable<T> GetAll<T>(); 
} 

interface ISave 
{ 
    void Save<T>(T item) where T : IHasId; //you can go with Save<T>(object id, T item) if you want pure pure POCOs 
} 

interface IDelete 
{ 
    void Delete<T>(object id); 
} 

interface IHasId 
{ 
    object Id { get; set; } 
} 

non mi piace l'interfaccia repository generica in quanto mette ulteriori restrizioni e rende più difficile con cui lavorare più tardi. Io invece uso metodi generici.

Invece di utilizzare header interface per il repository, utilizzo role interfaces per ciascun metodo di repository.Ciò mi consente di aggiungere funzionalità aggiuntive ai metodi del repository, come la registrazione, la pubblicazione delle modifiche su PubSub e così via.

Non utilizzo il repository per le query personalizzate poiché non ho ancora trovato alcuna astrazione di query valida e semplice adatta a qualsiasi database. La mia versione del repository può ottenere solo oggetto per id o ottenere tutti gli elementi dello stesso tipo. Altre query sono fatte in memoria (se le prestazioni sono abbastanza buone) o ho qualche altro meccanismo.

Per interfaccia convenienza IRepository potrebbe essere introdotta in modo da non dover scrivere costantemente 4 interfacce per qualcosa come controllori CRUD

interface IRepository : IGetById, IGetAll, ISave, IDelete { } 

class Repository : IRepository 
{ 
    private readonly IGetById getter; 

    private readonly IGetAll allGetter; 

    private readonly ISave saver; 

    private readonly IDelete deleter; 

    public Repository(IGetById getter, IGetAll allGetter, ISave saver, IDelete deleter) 
    { 
     this.getter = getter; 
     this.allGetter = allGetter; 
     this.saver = saver; 
     this.deleter = deleter; 
    } 

    public T GetById<T>(object id) 
    { 
     return getter.GetById<T>(id); 
    } 

    public IEnumerable<T> GetAll<T>() 
    { 
     return allGetter.GetAll<T>(); 
    } 

    public void Save<T>(T item) where T : IHasId 
    { 
     saver.Save(item); 
    } 

    public void Delete<T>(object id) 
    { 
     deleter.Delete<T>(id); 
    } 
} 

ho citato che con le interfacce di ruolo posso aggiungere comportamento aggiuntivo, ecco paio di esempi che utilizzano decoratori

class LogSaving : ISave 
{ 
    private readonly ILog logger; 

    private readonly ISave next; 

    public LogSaving(ILog logger, ISave next) 
    { 
     this.logger = logger; 
     this.next = next; 
    } 

    public void Save<T>(T item) where T : IHasId 
    { 
     this.logger.Info(string.Format("Start saving {0} : {1}", item.ToJson())); 
     next.Save(item); 
     this.logger.Info(string.Format("Finished saving {0}", item.Id)); 
    } 
} 

class PublishChanges : ISave, IDelete 
{ 
    private readonly IPublish publisher; 

    private readonly ISave nextSave; 

    private readonly IDelete nextDelete; 

    private readonly IGetById getter; 

    public PublishChanges(IPublish publisher, ISave nextSave, IDelete nextDelete, IGetById getter) 
    { 
     this.publisher = publisher; 
     this.nextSave = nextSave; 
     this.nextDelete = nextDelete; 
     this.getter = getter; 
    } 

    public void Save<T>(T item) where T : IHasId 
    { 
     nextSave.Save(item); 
     publisher.PublishSave(item); 
    } 

    public void Delete<T>(object id) 
    { 
     var item = getter.GetById<T>(id); 
     nextDelete.Delete<T>(id); 
     publisher.PublishDelete(item); 
    } 
} 

non è difficile da implementare in serbo memoria per testare

class InMemoryStore : IRepository 
{ 
    private readonly IDictionary<Type, Dictionary<object, object>> db; 

    public InMemoryStore(IDictionary<Type, Dictionary<object, object>> db) 
    { 
     this.db = db; 
    } 

    ... 
} 

finalmente mettere tutto insieme

var db = new Dictionary<Type, Dictionary<object, object>>(); 
var store = new InMemoryStore(db); 
var storePublish = new PublishChanges(new Publisher(...), store, store, store); 
var logSavePublish = new LogSaving(new Logger(), storePublish); 
var repo = new Repository(store, store, logSavePublish, storePublish); 
1

È possibile utilizzare il modello visitatore, leggere un implementazione here in modo da poter implementare solo le funzionalità controlo.

Here's l'idea:

public class Customer : IAcceptVisitor 
{ 
    private readonly string _id; 
    private readonly List<string> _items = new List<string>(); 
    public Customer(string id) 
    { 
     _id = id; 
    } 

    public void AddItems(string item) 
    { 
     if (item == null) throw new ArgumentNullException(nameof(item)); 
     if(_items.Contains(item)) throw new InvalidOperationException(); 
     _items.Add(item); 
    } 

    public void Accept(ICustomerVisitor visitor) 
    { 
     if (visitor == null) throw new ArgumentNullException(nameof(visitor)); 
     visitor.VisitCustomer(_items); 
    } 
} 
public interface IAcceptVisitor 
{ 
    void Accept(ICustomerVisitor visitor); 
} 

public interface ICustomerVisitor 
{ 
    void VisitCustomer(List<string> items); 
} 

public class PersistanceCustomerItemsVisitor : ICustomerVisitor 
{ 
    public int Count { get; set; } 
    public List<string> Items { get; set; } 
    public void VisitCustomer(List<string> items) 
    { 
     if (items == null) throw new ArgumentNullException(nameof(items)); 
     Count = items.Count; 
     Items = items; 
    } 
} 

Quindi, è possibile applicare la separazione degli interessi tra la logica di dominio e infraestructure applicando lo scalpiccio dei visitatori per la persistenza. Saluti!

+0

Non sono sicuro di seguirlo. In che modo il pattern del visitatore può aiutare a risolvere i problemi trasversali nel livello dati? Se ho un 'Cliente',' Ordine', ecc., E voglio dire di avere tutta la durata della query del registro di persistenza sulle operazioni di I/O del database? –

+0

Trovo utile implementare il visitatore in una business class che gli consenta solo di leggere le sue proprietà, in modo da avere un oggetto completamente incapsulato con un comportamento puro, quindi alla fine esegui l'implementazione come richiesto (puoi persino decorare un visitatore per la registrazione, ecc.) in questo caso smetti di usare repository per accedere agli oggetti business perché a volte non hai bisogno di implementare il crud completo. Per leggere da db un oggetto bussiness è sufficiente creare un IReader , in questo caso il codice diventa estremamente semplice e si evitano i metodi che lanciano NotImplementingException (codice non SOLID) –