为每个对象创建一个通用存储库相对于特定存储库的优势是什么?

我们正在开发一个 ASP.NET MVC 应用程序,现在正在构建存储库/服务类。我想知道创建一个所有存储库都实现的通用 IRepository 接口相对于每个存储库都有自己独特的接口和方法集是否有什么主要的优势。

例如: 通用 IRepository 接口可能看起来像(取自 这个答案) :

public interface IRepository : IDisposable
{
T[] GetAll<T>();
T[] GetAll<T>(Expression<Func<T, bool>> filter);
T GetSingle<T>(Expression<Func<T, bool>> filter);
T GetSingle<T>(Expression<Func<T, bool>> filter, List<Expression<Func<T, object>>> subSelectors);
void Delete<T>(T entity);
void Add<T>(T entity);
int SaveChanges();
DbTransaction BeginTransaction();
}

每个 Repository 都将实现这个接口,例如:

  • 客户存储库: IRepository
  • ProductRepository: IRepository
  • 等等。

我们在以前的项目中采用的替代方案是:

public interface IInvoiceRepository : IDisposable
{
EntityCollection<InvoiceEntity> GetAllInvoices(int accountId);
EntityCollection<InvoiceEntity> GetAllInvoices(DateTime theDate);
InvoiceEntity GetSingleInvoice(int id, bool doFetchRelated);
InvoiceEntity GetSingleInvoice(DateTime invoiceDate, int accountId); //unique
InvoiceEntity CreateInvoice();
InvoiceLineEntity CreateInvoiceLine();
void SaveChanges(InvoiceEntity); //handles inserts or updates
void DeleteInvoice(InvoiceEntity);
void DeleteInvoiceLine(InvoiceLineEntity);
}

在第二种情况下,表达式(LINQ 或其他)将完全包含在 Repository 实现中,实现服务的人只需要知道要调用哪个存储库函数。

我想我没有看到在服务类中编写所有表达式语法并传递到存储库的好处。这是否意味着在许多情况下,易于操作的 LINQ 代码正在被复制?

例如,在我们的旧发票系统中,我们调用

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

从几个不同的服务(客户,发票,帐户等)。这似乎比在多个地方写下面的内容要干净得多:

rep.GetSingle(x => x.AccountId = someId && x.InvoiceDate = someDate.Date);

我认为使用这种特定方法的唯一缺点是,我们最终可能会得到许多 Get * 函数的排列,但这似乎仍然比将表达式逻辑推到 Service 类中要好。

我错过了什么?

34692 次浏览

这个问题与 Repository 模式本身一样古老。最近引入的 LINQ 的 IQueryable(查询的统一表示)引起了很多关于这个主题的讨论。

在非常努力地构建了一个通用的存储库框架之后,我自己更喜欢特定的存储库。无论我尝试什么聪明的机制,我总是以同样的问题告终: 存储库是被建模的域的一部分,而且该域不是通用的。不是每个实体都可以删除,不是每个实体都可以添加,也不是每个实体都有一个存储库。查询变化很大; 存储库 API 变得和实体本身一样唯一。

我经常使用的一种模式是具有特定的存储库接口,但是是实现的基类。例如,使用 LINQtoSQL,您可以:

public abstract class Repository<TEntity>
{
private DataContext _dataContext;


protected Repository(DataContext dataContext)
{
_dataContext = dataContext;
}


protected IQueryable<TEntity> Query
{
get { return _dataContext.GetTable<TEntity>(); }
}


protected void InsertOnCommit(TEntity entity)
{
_dataContext.GetTable<TEntity>().InsertOnCommit(entity);
}


protected void DeleteOnCommit(TEntity entity)
{
_dataContext.GetTable<TEntity>().DeleteOnCommit(entity);
}
}

DataContext替换为您选择的工作单元:

public interface IUserRepository
{
User GetById(int id);


IQueryable<User> GetLockedOutUsers();


void Insert(User user);
}


public class UserRepository : Repository<User>, IUserRepository
{
public UserRepository(DataContext dataContext) : base(dataContext)
{}


public User GetById(int id)
{
return Query.Where(user => user.Id == id).SingleOrDefault();
}


public IQueryable<User> GetLockedOutUsers()
{
return Query.Where(user => user.IsLockedOut);
}


public void Insert(User user)
{
InsertOnCommit(user);
}
}

注意,存储库的公共 API 不允许删除用户。此外,曝光 IQueryable是另一个完全不同的问题——关于这个话题的意见就像肚脐眼一样多。

我更喜欢具有可覆盖方法签名的从通用存储库(或通用存储库列表,以指定确切的行为)派生的特定存储库。

事实上,我对布莱恩的观点有点不同意。我认为他是对的,最终一切都是独一无二的等等。但与此同时,大部分都是在你设计的时候产生的,我发现在开发我的模型的时候,建立一个通用的存储库并使用它,我可以很快地建立一个应用,然后在我发现需要的时候重构到更具体的特性。

因此,在这样的情况下,我经常创建一个通用的 IRepository,它拥有完整的 CRUD 堆栈,这让我能够快速地使用 API,让人们在 UI 上玩游戏,并且并行地进行集成和用户接受测试。然后,当我发现我需要特定的查询回购等,我开始替换的依赖 w/特定的一个,如果需要,并从那里开始。一个潜在的暗示。易于创建和使用(并且可能连接到内存中的 db 或静态对象或模拟对象或其他对象)。

也就是说,我最近开始做的是打破这种行为。因此,如果你为 IDataFetcher、 IDataUpdater、 IDataInserter 和 IDataDeleter (例如)做接口,你可以通过接口混合并匹配来定义你的需求,然后有实现来处理它们中的一部分或全部,我仍然可以在构建应用程序时注入所有实现来使用。

Paul

有一个由特定存储库包装的通用存储库。通过这种方式,您可以控制公共接口,但是仍然具有来自通用存储库的代码重用的优势。

Public 类 UserRepository: Repository,IUserRepository

您是否应该注入 IUserRepository 以避免暴露接口。正如人们所说,您可能不需要完整的 CRUD 堆栈等。