Pregunta C # - Composición del objeto - Eliminación del código repetitivo


Contexto / Pregunta

He trabajado en numerosos proyectos .NET que se han requerido para persistir datos y generalmente terminaron usando un Repositorio patrón. ¿Alguien sabe de una buena estrategia para eliminar tanto código repetitivo sin sacrificar la escalabilidad de la base de código?

Estrategia de herencia

Debido a que gran parte del código del repositorio es una placa de la caldera y debe repetirse, normalmente creo una clase base para cubrir los aspectos básicos como el manejo de excepciones, el registro y el soporte de transacciones, así como algunos métodos básicos de CRUD:

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){}
}

Así que esto funciona bien cuando tengo un dominio simple, puedo crear rápidamente una clase de repositorio DRY para cada entidad. Sin embargo, esto comienza a descomponerse cuando el dominio se vuelve más complejo. Digamos que se introduce una nueva entidad que no permite actualizaciones. Puedo dividir las clases base y mover el método Update a una clase diferente:

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){}
}

Esta solución no escala bien. Digamos que tengo varias entidades que tienen un método común:     archivo de vacío virtual público (entidad T) {}

pero algunas entidades que pueden archivarse también pueden actualizarse mientras que otras no. Entonces mi solución de Herencia se rompe, tendría que crear dos nuevas clases base para enfrentar este escenario.

Estrategia de Compoisición

Exploré el patrón de Compositon, pero parece que deja mucho código de placa de caldera:

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)'
    }
} 

El MyEntityRepository ahora está cargado con un código repetitivo. ¿Hay alguna herramienta / patrón que pueda usar para generar esto automáticamente?

Si pudiera convertir el MyEntityRepository en algo así, creo que sería ideal:

[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)
    }
}

Programación Orientada a Aspectos

Intenté usar un marco AOP para esto, específicamente PostSharp y ellos Aspecto de composición, que parece que debería ser el truco, pero para usar un Repositorio tendré que llamar a Post.Cast <> (), que agrega un olor muy extraño al código. ¿Alguien sabe si hay una mejor manera de usar AOP para ayudar a deshacerse del código repetitivo del compositor?

Generador de código personalizado

Si todo lo demás falla, supongo que podría trabajar creando un plugin de Custom Code Generator Visual Studio que pueda generar el código de la placa de la caldera en un archivo de código parcial. ¿Ya hay una herramienta que haría esto?

[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)'
    }
} 

Métodos de extensión

Olvidé agregar esto cuando inicialmente escribí la pregunta (lo siento). También traté de experimentar con métodos de extensión:

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

Sin embargo, esto tiene dos problemas: a) Tendría que recordar el espacio de nombres de la clase de métodos de extensión y agregarlo en todas partes yb) los métodos de extensión no pueden satisfacer las dependencias de la interfaz:

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

Actualizar: Haría Plantillas T4 ser una posible solución?


32
2018-03-16 17:31


origen


Respuestas:


Tengo una única interfaz de repositorio genérico, que se implementa solo una vez para un almacenamiento de datos particular. Aquí está:

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

Tengo implementaciones para almacenes EntityFramework, NHibernate, RavenDB. También tengo una implementación en memoria para pruebas unitarias.

Por ejemplo, aquí hay una parte del repositorio basado en colección en memoria:

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);
        }
    }
}

La interfaz de repositorio genérico me libera de la creación de muchas clases similares. Solo tiene una implementación genérica de repositorio, pero también libertad para consultar.

IQueryable<T> el resultado de GetAll() método me permite hacer las consultas que quiero con los datos y separarlos del código específico del almacenamiento. Todos los ORM de .NET populares tienen sus propios proveedores de LINQ, y todos deberían tener esa magia GetAll() método - así que no hay problemas aquí.

Especifico la implementación del repositorio en la raíz de la composición usando el contenedor IoC:

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

En las pruebas que estoy usando es reemplazo en memoria:

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

Si deseo agregar más consultas específicas de negocios para el repositorio, agregaré un método de extensión (similar a su método de extensión en la respuesta):

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));
    }
}

Entonces puede usar y mezclar esos métodos en las consultas de la capa de lógica de negocios, salvando la capacidad de prueba y la facilidad de las implementaciones del repositorio, como:

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

Si no desea utilizar un espacio de nombres diferente para esos métodos de extensión (como yo) - ok, colóquelos en el mismo espacio de nombres donde reside la implementación del repositorio (como MyProject.Data), o, mejor aún, a algún espacio de nombres específico de negocios existente (como MyProject.Products o MyProject.Data.Products) No es necesario recordar espacios de nombres adicionales ahora.

Si tiene alguna lógica de repositorio específica para algún tipo de entidad, cree una clase de repositorio derivado anulando el método que desee. Por ejemplo, si los productos solo pueden ser encontrados por ProductNumber en lugar de Id y no admite la eliminación, puede crear esta clase:

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");
    }
}

Y haga que IoC devuelva esta implementación de repositorio específica para productos:

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

Así es como me voy en pedazos con mis repositorios;)


11
2018-03-16 22:42



Pagar archivos T4 para la generación de código. T4 está integrado en Visual Studio. Vea un tutorial aquí.

He creado archivos T4 para entidades de POCO que generan código inspeccionando un LINQ DBML y sus repositorios, creo que le serviría bien aquí. Si genera clases parciales con su archivo T4, podría simplemente escribir el código para los casos especiales.


4
2018-04-11 19:32



Para mí, parece que divides las clases base y luego quieres la funcionalidad de ambas en una sola clase heredera. En tal caso, la composición es la elección. La herencia de clases múltiples también sería agradable si C # lo admitiera. Sin embargo, debido a que siento que la herencia es más agradable y que la reutilización aún está bien, mi primera elección de opción iría bien.

Opción 1

Preferiría tener una clase base más en lugar de la composición de los dos. La reutilización se puede resolver con métodos estáticos y no con la herencia:

La parte reutilizable no es visible afuera. No es necesario recordar el espacio de nombres.

static class Commons
{
    internal static void Update(/*receive all necessary params*/) 
    { 
        /*execute and return result*/
    }

    internal static void Archive(/*receive all necessary params*/) 
    { 
        /*execute and return result*/
    }
}

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

class ChildWithUpdate : Basic
{
    public void Update() { Commons.Update(); }
}

class ChildWithArchive : Basic
{
    public void Archive() { Commons.Archive(); }
}

class ChildWithUpdateAndArchive: Basic
{
    public void Update() { Commons.Update(); }
    public void Archive() { Commons.Archive(); }
}

Por supuesto, hay algún código menor repetido, pero eso es simplemente llamar a las funciones preconfiguradas de la biblioteca común.

opcion 2

Mi implementación de la composición (o imitación de la herencia múltiple):

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);
    }
}

Herencia y composición (abajo):

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"); }
}

Composición. No estoy seguro si esto es suficiente para decir que no existe un código repetitivo.

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

Código usando todo esto parece estar bien, pero aún inusual (invisible) escribe moldes en asignaciones. Esto es una recompensa por tener menos código repetitivo:

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();

2
2018-04-17 19:59



Aquí está mi versión:

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; }
}

No me gusta la interfaz genérica de repositorio, ya que impone restricciones adicionales y hace que sea más difícil trabajar con ella más adelante. Yo uso métodos genéricos en su lugar.

En lugar de usar interfaz de encabezado para el repositorio que uso interfaces de rol para cada método de repositorio. Esto me permite agregar funcionalidad adicional a los métodos de repositorio, como el registro, la publicación de cambios en PubSub, etc.

No utilizo el repositorio para consultas personalizadas, ya que aún no encontré ninguna abstracción de consulta buena y simple que se ajuste a cualquier base de datos. Mi versión del repositorio solo puede obtener elementos por identificación u obtener todos los artículos del mismo tipo. Otras consultas se realizan en la memoria (si el rendimiento es lo suficientemente bueno) o tengo algún otro mecanismo.

Por conveniencia, se podría introducir la interfaz IRepository para que no tenga que escribir constantemente 4 interfaces para algo así como controladores 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);
    }
}

Mencioné que con las interfaces de roles puedo agregar un comportamiento adicional, aquí hay ejemplos de pareja usando decoradores

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);
    }
}

No es difícil de implementar en la memoria de la tienda para probar

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

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

    ...
}

Finalmente poner todo junto

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
2017-11-25 11:21



Puede usar el patrón de visitante, leer una implementación aquí por lo que solo puede implementar la funcionalidad necesaria.

Aquí está la 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;
    }
}

Por lo tanto, puede aplicar la separación de preocupaciones entre la lógica de dominio y la infraestructura aplicando el patrón de visitante para persistencia. ¡Saludos!


1
2017-12-05 18:32