C# - 对象组成 - 去除锅炉代码 [英] C# - Object Composition - Removing Boilerplate Code

查看:152
本文介绍了C# - 对象组成 - 去除锅炉代码的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

上下文/问题



我已经处理了许多.NET项目,需要保留数据,通常最终使用 Repository 模式。有没有人知道在不牺牲代码库可扩展性的情况下删除尽可能多的样板代码的好策略?



继承策略



因为这么多的Repository代码是锅炉板,需要重复,我通常会创建一个基类来覆盖基础知识,如异常处理,日志记录和事务支持以及一些基本的CRUD方法:

  public abstract class BaseRepository< T>其中T:IEntity 
{
protected void ExecuteQuery(Action query)
{
//执行事务支持/错误处理/记录
query();
}

// CRUD方法:
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){}
}

所以这样做很好,当我有一个简单的域名,我可以快速创建一个干每个实体的存储库类。但是,当领域变得越来越复杂时,它就开始分解了。让我们说一个不允许更新的新实体被引入。我可以分解基类并将Update方法移动到不同的类中:

  public abstract class BaseRepositorySimple< T>其中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>其中T:IEntity
{
public virtual void Update(T entity){}
}

此解决方案不能很好地扩展。假设我有几个实体有一个常见的方法:
public virtual void Archive(T entity){}



但是一些可以归档的实体也可以在其他人不能更新。所以我的继承解决方案崩溃了,我必须创建两个新的基础类来处理这种情况。



收购策略



我已经探讨了Compositon模式,但这似乎留下了很多锅炉版代码:

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

public MyEntityRepository()
{
//初始化包装器(或使用构造函数注入和DI拉入
//)
}

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

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

MyEntityRepository现在加载了样板代码。有没有一个可以用来自动生成这个的工具/模式?



如果我可以将MyEntityRepository变成这样的东西,我认为这样做是非常理想的:

  [Implement(Interface = typeof(IGetByID< MyEntity>),
Using = GetByIDRetriever&MyEntity>)]
[实现(Interface = typeof(IArchive< MyEntity>))
使用= Archiver< MyEntity>)
public class MyEntityRepository
{
public MyEntityRepository()
{
//初始化包装器(或使用构造函数注入和DI拉入
//)
}
}
pre>

面向方面的编程



我研究了使用AOP框架,特别是 PostSharp 及其组合方面,它看起来应该是诀窍,但为了使用一个存储库,我必须调用Post.Cast<>(),它添加一个非常奇怪的气味到代码。任何人都知道是否有更好的方式使用AOP来帮助摆脱合成器样板代码?



自定义代码生成器



如果一切都失败,我想我可以在创建一个自定义代码生成器Visual Studio插件,可以将锅炉板代码生成为部分代码文件。是否有一个工具可以这样做?

  [实现(Interface = typeof(IGetByID< MyEntity>),
使用= GetByIDRetriever< MyEntity>)]
[实现(Interface = typeof(IArchive< MyEntity>),
使用= Archiver< MyEntity>)
public partial class MyEntityRepository
{
public MyEntityRepository()
{
//初始化包装器(或者使用构造函数注入和DI拉入
//)
}
}

//生成的类文件
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)'
}
}



扩展方法



这个问题(对不起)。我也尝试使用扩展方法:

  public static class GetByIDExtenions 
{
public T GetByID< T> ;(这个IGetByID< T>存储库,int id){}
}

这有两个问题,a)我必须记住扩展方法类的命名空间,并将其添加到任何地方,并且b)扩展方法不能满足接口依赖性:

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

更新 T4模板是一个可能的解决方案?

解决方案

我有一个通用存储库接口,仅对特定数据存储器实现一次。这里是:

  public interface IRepository< T>其中T:class 
{
IQueryable< T>得到所有();
T Get(object id);
void Save(T item);
void Delete(T item);
}

我有实现EntityFramework,NHibernate,RavenDB存储。另外我有一个单元测试的内存中的实现。



例如,这里是基于内存集合的存储库的一部分:

  public class InMemoryRepository< T> :IRepository< T>其中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)))
{
删除(item);
}

_list.Add(item);
}

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

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

通用存储库界面使我免于创建类似类的许多。您只有一个通用存储库实现,而且还有查询的自由。



IQueryable< T> c $ c> GetAll()方法允许我使用数据进行任何查询,并将它们与存储特定代码分开。所有流行的.NET ORM都有自己的LINQ提供程序,他们都应该有这样的魔术 GetAll()方法 - 所以没有问题。



我使用IoC容器在组合根中指定存储库实现:

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

在测试中,我使用的是内存替换:

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

如果我想为存储库添加更多的业务特定查询,我将添加一个扩展方法类似于你的扩展方法在答案):

  public static class ShopQueries 
{
public IQueryable&产品与GT; SelectVegetables(这个IQueryable< Product>查询)
{
return query.Where(x => x.Type ==Vegetable);
}

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

所以你可以在业务中使用和混合这些方法逻辑层查询,保存可靠性和存储库实现的容易性,如:

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

如果您不想为这些扩展方法使用不同的命名空间(像我一样) - ok ,将它们放在存储库实现所在的同一个命名空间中(如 MyProject.Data ),或者更好的是,一些现有的业务特定命名空间(如 MyProject.Products MyProject.Data.Products )。不需要记住其他命名空间。



如果您对某些实体有一些特定的存储库逻辑,请创建一个派生的存储库类来覆盖所需的方法。例如,如果产品只能通过 ProductNumber 而不是 Id 找到,不支持删除,则可以创建这个类:

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

public override Delete(Product item)
{
抛出新的NotSupportedException(产品无法从db中删除);
}
}

并使IoC返回产品的特定存储库实现:

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

这是我如何离开我的仓库;)


Context / Question

I've worked on numerous .NET projects that have been required to persist data and have usually ended up using a Repository pattern. Does anyone know of a good strategy for removing as much boilerplate code without sacrificing code base scalability?

Inheritance Strategy

Because so much of the Repository code is boiler plate and needs to be repeated I normally create a base class to cover the basics like exception handling, logging and transaction support as well as a few basic CRUD methods:

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

So this works well when I have a simple domain, I can quickly create a DRY repository class for each entity. However, this starts to break down when the domain gets more complex. Lets say a new entity is introduced that does not allow updates. I can break up base classes and move the Update method into a different class:

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

This solution does not scale well. Let's say I have several Entities that have a common method: public virtual void Archive(T entity){}

but some Entities that can be Archived can also be Updated while others can't. So my Inheritance solution breaks down, I'd have to create two new base classes to deal with this scenario.

Compoisition Strategy

I've explored the Compositon pattern, but this seems to leave a lot of boiler plate code:

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

The MyEntityRepository is now loaded with boilerplate code. Is there a tool / pattern that I can use to automatically generate this?

If I could turn the MyEntityRepository into something like this, I think that would by far be 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)
    }
}

Aspect Oriented Programming

I looked into using an AOP framework for this, specifically PostSharp and their Composition Aspect, which looks like it should do the trick, but in order to use a Repository I'll have to call Post.Cast<>(), which adds a very odd smell to the code. Anyone know if there's a better way to use AOP to help get rid of the compositor boilerplate code?

Custom Code Generator

If all else fails, I suppose I could work at creating a Custom Code Generator Visual Studio plug in that could generate the boiler plate code into a partial code file. Is there already a tool out there that would do this?

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

Extension Methods

Forgot to add this when I initially wrote the question (sorry). I also tried experimenting with extension methods:

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

However, this has two problems, a) I'd have to remember the namespace of the extension methods class and add it everywhere and b) the extension methods can't satisfy interface dependencies:

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

Update: Would T4 Templates be a possible solution?

解决方案

I have a single generic repository interface, which is implemented only once for a particular data storage. Here it is:

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

I have implementations of it for EntityFramework, NHibernate, RavenDB storages. Also I have an in-memory implementation for unit testing.

For example, here is a part of the in-memory collection-based repository:

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

Generic repository interface frees me from creating lot's of similar classes. You have only one generic repository implementation, but also freedom in querying.

IQueryable<T> result from GetAll() method allows me to make any queries I want with the data, and separate them from the storage-specific code. All popular .NET ORMs have their own LINQ providers, and they all should have that magic GetAll() method - so no problems here.

I specify repository implementation in the composition root using IoC container:

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

In the tests I'm using it's in-memory replacement:

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

If I want to add more business-specific queries for the repository, I will add an extension method (similar to your extension method in the answer):

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

So you can use and mix those methods in the business logic layer queries, saving testability and easiness of repository implementations, like:

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

If you don't want to use a different namespace for those extension methods (like me) - ok, put them in the same namespace where repository implementation resides (like MyProject.Data), or, even better, to some existing business specific namespace (like MyProject.Products or MyProject.Data.Products). No need to remember additional namespaces now.

If you have some specific repository logic for some kind of entities, create a derived repository class overriding the method you want. For example, if products can only be found by ProductNumber instead of Id and don't support deleting, you can create this class:

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

And make IoC return this specific repository implementation for products:

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

That's how I leave in piece with my repositories ;)

这篇关于C# - 对象组成 - 去除锅炉代码的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆