我已经处理了许多需要持久保存数据的.NET项目,并且通常最终使用Repository模式。有没有人知道在不牺牲代码库可扩展性的情况下删除尽可能多的样板代码的好策略?
因为很多存储库代码都是锅炉板并且需要重复,所以我通常会创建一个基类来涵盖异常处理,日志记录和事务支持等基础知识以及一些基本的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){}
}
因此,当我有一个简单的域时,这很有效,我可以为每个实体快速创建一个DRY存储库类。但是,当域变得更复杂时,这开始崩溃。假设引入了一个不允许更新的新实体。我可以拆分基类并将Update方法移动到另一个类中:
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){}
}
此解决方案无法很好地扩展。假设我有几个实体有一个共同的方法: public virtual void Archive(T entity){}
但是一些可以存档的实体也可以更新,而其他实体则不能。所以我的继承解决方案崩溃了,我必须创建两个新的基类来处理这种情况。
我已经探索了Compositon模式,但这似乎留下了很多锅炉板代码:
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)'
}
}
MyEntityRepository现在加载了样板代码。是否有可用于自动生成此工具/模式的工具/模式?
如果我可以将MyEntityRepository变成这样的东西,我想这到目前为止理想:
[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)
}
}
我考虑使用AOP框架,特别是PostSharp和他们的Composition Aspect,看起来应该这样做,但为了使用存储库,我必须调用Post .Cast&lt;&gt;(),它为代码添加了一种非常奇怪的气味。任何人都知道是否有更好的方法来使用AOP来帮助摆脱合成器样板代码?
如果所有其他方法都失败了,我想我可以创建一个自定义代码生成器Visual Studio插件,它可以将锅炉板代码生成为部分代码文件。是否已经有一个可以做到这一点的工具?
[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)'
}
}
忘记在我最初写这个问题时添加这个(对不起)。我也试过尝试扩展方法:
public static class GetByIDExtenions
{
public T GetByID<T>(this IGetByID<T> repository, int id){ }
}
然而,这有两个问题,a)我必须记住扩展方法类的名称空间并将其添加到任何地方b)扩展方法不能满足接口依赖性:
public interface IMyEntityRepository : IGetByID<MyEntity>{}
public class MyEntityRepository : IMyEntityRepository{}
更新:T4 Templates会成为可能的解决方案吗?
答案 0 :(得分:11)
我有一个通用的存储库接口,对于特定的数据存储只能实现一次。这是:
public interface IRepository<T> where T : class
{
IQueryable<T> GetAll();
T Get(object id);
void Save(T item);
void Delete(T item);
}
我为EntityFramework,NHibernate,RavenDB存储实现了它。我还有一个用于单元测试的内存实现。
例如,以下是基于内存集合的存储库的一部分:
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);
}
}
}
通用存储库接口使我无法创建类似类的批次。您只有一个通用的存储库实现,但也可以自由查询。
来自IQueryable<T>
方法的 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<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));
}
}
因此,您可以在业务逻辑层查询中使用和混合这些方法,从而节省可测试性和存储库实现的简便性,例如:
var freshVegetables = repo.GetAll().SelectVegetables().FreshOnly();
如果您不想为这些扩展方法使用不同的命名空间(比如我) - 好吧,将它们放在存储库实现所在的同一命名空间中(如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)
{
throw new NotSupportedException("Products can't be deleted from db");
}
}
让IoC返回产品的特定存储库实现:
ioc.Bind(typeof (IRepository<>)).To(typeof (RavenDbRepository<>));
ioc.Bind<IRepository<Product>>().To<ProductRepository>();
这就是我与我的存储库分开的方式;)
答案 1 :(得分:4)
结帐T4文件以生成代码。 T4内置于Visual Studio中。 See a tutorial here
我已经通过检查LINQ DBML及其存储库为代码生成POCO实体创建了T4文件,我认为它可以很好地为您服务。如果使用T4文件生成部分类,则可以为特殊情况编写代码。
答案 2 :(得分:2)
对我来说,似乎你划分了基类,然后在一个继承者类中想要它们的功能。在这种情况下,组合是选择。如果C#支持它,多类继承也会很好。但是,因为我觉得继承更好,并且可重用性仍然很好,我的第一个选择将适用它。
选项1
我宁愿再增加一个基类而不是两者的组合。可重用性也可以使用静态方法解决,而不是继承:
外部看不到可重复使用的部件。无需记住命名空间。
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(); }
}
当然有一些小的重复代码,但这只是从公共库中调用现成的函数。
选项2
我对组合的实现(或模仿多重继承):
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);
}
}
继承和组成(下):
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"); }
}
组合物。不确定这是否足以说明没有样板代码。
class ChildWithUpdateAndArchive : Composite<ChildWithUpdate, ChildWithArchive>
{
public ChildWithUpdateAndArchive(ChildWithUpdate cwu, ChildWithArchive cwa)
: base(cwu, cwa)
{
}
}
使用所有这些的代码看起来很好,但在分配中仍然不常见(不可见)类型。这样可以获得更少的样板代码:
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();
答案 3 :(得分:1)
这是我的版本:
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; }
}
我不喜欢通用存储库接口,因为它会增加额外的限制,并且以后更难以使用它。我改用泛型方法。
不是将header interface用于存储库,而是为每个存储库方法使用role interfaces。这使我可以向存储库方法添加其他功能,例如记录,发布对PubSub的更改等等。
我没有使用存储库进行自定义查询,因为我还没有找到适合任何数据库的任何好的和简单的查询抽象。我的版本的存储库只能通过id获取项目或获取相同类型的所有项目。其他查询在内存中完成(如果性能足够好)或者我有其他一些机制。
为方便起见,可以引入IRepository接口,这样就不必为crud控制器之类的东西不断编写4个接口
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);
}
}
我提到使用角色接口我可以添加其他行为,这里有几个使用装饰器的例子
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);
}
}
在内存存储器中实现测试并不困难
class InMemoryStore : IRepository
{
private readonly IDictionary<Type, Dictionary<object, object>> db;
public InMemoryStore(IDictionary<Type, Dictionary<object, object>> db)
{
this.db = db;
}
...
}
最后把所有人放在一起
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);
答案 4 :(得分:1)
您可以使用访问者模式,阅读实施here,这样您就只能实现必要的功能。
以下是这个想法:
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;
}
}
因此,您可以应用域逻辑和基础结构之间的关注点分离,应用访问者模式以保持持久性。 此致!