65.9K
CodeProject 正在变化。 阅读更多。
Home

为什么使用 Repository 模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (20投票s)

2013 年 5 月 30 日

CPOL

6分钟阅读

viewsIcon

77548

深入探讨我个人为何喜欢仓库模式。

引言

以下文章将探讨我个人认为仓库模式是一个非常有用且应该更常使用的范例的几个不同原因 - 特别是当涉及到像 Entity Framework 这样的 ORM 框架时。

在最近一篇博文 "Entity Framework 初学者指南" 引起巨大反响后,我觉得在编写更多代码来扩展基本示例之前,我应该退一步解释我对仓库模式的看法。

我被这个反响深深震撼了;原本只是一个简单的 30 分钟的博文创作,却演变成了如此不可思议的结果。最初的文章主要是关于不要将直接的数据库查询和保存与核心代码混合在一起。

请注意,这些是我基于当前和过去我遇到的痛点对该模式的看法,我非常乐意听取他人的意见,共同改进。

在深入之前,您可以在 GitHub 上找到完整的源代码:https://github.com/endyourif/RepoTest/

让我的代码更具可读性

在最基础的层面,仓库层的目标是从我的代码中移除查询内容。我个人觉得阅读像这样的函数比阅读像这样的函数更容易:

// Display all Blogs from the database 
var query = repo.GetAll(); 
Console.WriteLine("All blogs in the database:"); 
foreach (var item in query) 
{ 
    Console.WriteLine(item.Name);
}

比这样的函数更容易:

// Display all Blogs from the database 
var query = from b in db.Blogs 
orderby b.Name 
select b; 
Console.WriteLine("All blogs in the database:"); 
foreach (var item in query) 
{ 
    Console.WriteLine(item.Name); 
} 

在第一个代码示例中,我不会被阅读 LINQ 查询所困扰;我只需要处理 `GetAll` 函数的查询结果。当然,这可能是一个糟糕的命名;也许最好将函数名更新为 `GetAllBlogsOrderedByName`。虽然名字很长,但具有极高的描述性。它让读者对函数内发生的事情一目了然。

分离职责

我工作在一个规模相对较大的团队。在这个团队中,有些人擅长编写高度优化的 LINQ 或 SQL 查询;而团队中的其他人擅长执行业务逻辑;还有一些是初级开发者,需要获得我的信任!

通过添加仓库层,我可以分离这些职责。你擅长编写 LINQ - 太好了 - 你负责仓库层。如果有人需要数据访问,他们就去找你。这将有助于防止糟糕的查询,避免多个实现相似功能的函数(因为有人不知道 XYZ 函数)等等。

分层我的代码

我热爱 MVC 设计模式 - 模型-视图-控制器。其中控制器与模型交互以获取数据,然后将数据传递给视图进行输出。

我见过太多人将所有东西都塞进控制器中 - 因为它是中间人。当然,这不仅适用于 CakePHP,甚至不只适用于控制器。

我喜欢从一个干净的函数开始,然后将其分解为多个执行必要工作的小函数。例如:错误检查、业务逻辑和数据访问。当我需要调试代码时,可以非常容易地定位到可能引起问题的代码行;通常在一个只有 10 行的函数内;而不是在一个有 100 多行的函数内。

添加缓存层

这并非总是显而易见的功能,一般的网站或应用程序可能不需要缓存。但在我日常处理的世界里,它必不可少。通过创建仓库层,这真的非常简单。

让我们看看前面讨论的 `GetAllBlogsOrderedByName` 函数,并实现缓存功能。

private static IOrderedQueryable<Blog> _blogs; 
public IOrderedQueryable<Blog> GetAllBlogsOrderedByName() 
{ 
    return _blogs ?? (_blogs = from b in _bloggingContext.Blogs 
        orderby b.Name 
        select b); 
}

知道我的博客只在我保存内容时才会更改,我可以通过 C# 的静态变量在应用程序运行的整个生命周期内保留在内存中的能力来利用这一点。如果 `_blogs` 变量为 `null`,我才需要查询它。

为了确保在更改内容时重置此变量,我可以轻松地更新 `AddBlog` 或 `SaveChanges` 函数以清除该变量的结果:

public int SaveChanges() 
{ 
    int changes = _bloggingContext.SaveChanges(); 
    if (changes > 0) 
    { 
        _blogs = null; 
    } 
    return changes; 
}  

如果我足够大胆,甚至可以在清除 `_blogs` 变量的同时重新填充它。

这个例子当然只是一个起点,我在这里利用了静态变量。如果我有一个集中的缓存服务,例如 Redis 或 Memcache,我可以添加非常类似的功能,在那里我检查集中式缓存中的变量。

单元测试

我在上一篇文章中也简要提到了这一点,通过分离我的功能,可以使我的代码单元测试更加容易。我可以测试我代码的较小部分,为一些关键路径添加不同的测试场景,而对非关键路径则不必如此。例如,如果我正在利用缓存,那么彻底测试此功能并确保我的数据被正确刷新以及不会不必要地被频繁查询是非常重要的。

为了实现这一点,需要对原始代码进行一些更改。 `BlogRepo` 需要一个接口。 `BlogRepo` 的构造函数也需要细微的更改。它应该更新为接受一个接口,这样我就可以模拟 `DbContext`,这样我的单元测试就不会连接到真实的数据库。

public interface IBlogRepo : IDisposable 
{ 
    void AddBlog(Blog blog); 
    int SaveChanges(); 
    IOrderedQueryable<Blog> GetAllBlogsOrderedByName(); 
} 

`DbContext` 的接口和 `BloggingContext` 的小更新

public interface IBloggingContext : IDisposable 
{ 
    IDbSet<Blog> Blogs { get; set; } 
    IDbSet<Post> Posts { get; set; } 
    int SaveChanges(); 
} 
public class BloggingContext : DbContext, IBloggingContext 
{ 
    public IDbSet<Blog> Blogs { get; set; } 
    public IDbSet<Post> Posts { get; set; } 
} 

现在 `BlogRepo` 中的构造函数可以更新为期望一个 `IBloggingContext`。我们还将通过创建一个空构造函数来执行另一个增强,该构造函数将创建一个新的 `BloggingContext`。

public class BlogRepo : IBlogRepo 
{ 
    private readonly IBloggingContext _bloggingContext;
    public BlogRepo() : this(new BloggingContext()) 
    { 
    } 
    public BlogRepo(IBloggingContext bloggingContext) 
    { 
        _bloggingContext = bloggingContext; 
    } 
    public void Dispose() 
    { 
        _bloggingContext.Dispose(); 
    } 
    … 
} 

正如原始帖子中的一些评论所暗示的,我仍然在主 `Program.cs` 中创建 `DbContext`。通过修改 `BlogRepo` 和接口来实现 `IDisposable`,我可以自动创建一个新的 `BloggingContext`,或者传递一个已经创建的上下文,该上下文将在执行结束时自动被释放。

现在我们的 Main 函数可以删除创建新的 `BloggingContext` 的 using 语句。

static void Main(string[] args) 
{ 
    // Create new repo class 
    BlogRepo repo = new BlogRepo();
    …
} 

现在我已经改进了核心代码,我可以创建一个新的测试项目并将其添加到我的解决方案中。在测试项目中,我需要创建一个名为 `FakeDbSet` 和 `FakeDbContext` 的新类。这些类将在我的测试中使用,以模拟我的 `BloggingContext`:

public class FakeDbSet<T> : IDbSet<T> where T : class 
{ 
    ObservableCollection<T> _data; 
    IQueryable _query; 
    public FakeDbSet()
    { 
        _data = new ObservableCollection<T>(); 
        _query = _data.AsQueryable(); 
    } 
    public T Find(params object[] keyValues) 
    { 
        throw new NotSupportedException("FakeDbSet does not support the find operation"); 
    } 
    public T Add(T item) 
    { 
        _data.Add(item); 
        return item; 
    } 
    public T Remove(T item) 
    { 
        _data.Remove(item); 
        return item; 
    } 
    public T Attach(T item) 
    { 
        _data.Add(item); 
        return item; 
    } 
    public T Detach(T item) 
    { 
        _data.Remove(item); 
        return item; 
    } 
    public T Create() 
    { 
        return Activator.CreateInstance<T>(); 
    } 
    public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
    {  
        return Activator.CreateInstance<TDerivedEntity>(); 
    } 
    public ObservableCollection<T> Local 
    { 
        get { return _data; } 
    } 
    Type IQueryable.ElementType 
    { 
        get { return _query.ElementType; } 
    } 
    System.Linq.Expressions.Expression IQueryable.Expression 
    { 
        get { return _query.Expression; } 
    } 
    IQueryProvider IQueryable.Provider 
    { 
        get { return _query.Provider; } 
    } 
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
    { 
        return _data.GetEnumerator(); 
    } 
    IEnumerator<T> IEnumerable<T>.GetEnumerator() 
    { 
        return _data.GetEnumerator(); 
    } 
} 

以及实现 `IBloggingContext` 接口的 `FakeDbContext`:

class FakeDbContext : IBloggingContext 
{ 
    public FakeDbContext() 
    { 
        Blogs = new FakeDbSet<Blog>(); 
        Posts = new FakeDbSet<Post>(); 
    } 
    public IDbSet<Blog> Blogs { get; set; } 
    public IDbSet<Post> Posts { get; set; } 
    public int SaveChanges() 
    { 
        return 0; 
    } 
    public void Dispose() 
    { 
        throw new NotImplementedException(); 
    } 
} 

最后是 `BlogRepoTest` 类,它测试 `AddBlog` 和 `GetAllBlogsOrderedByName` 函数:

/// <summary> 
///This is a test class for BlogRepoTest and is intended 
///to contain all BlogRepoTest Unit Tests 
///</summary> 
[TestFixture] 
public class BlogRepoTest 
{ 
    private BlogRepo _target; 
    private FakeDbContext _context; 
    #region Setup / Teardown 
    [SetUp] 
    public void Setup() 
    { 
        _context = new FakeDbContext(); 
        _target = new BlogRepo(_context); 
    } 
    #endregion Setup / Teardown 
    /// <summary> 
    ///A test for AddBlog 
    ///</summary> 
    [Test] 
    public void AddBlogTest() 
    { 
        Blog expected = new Blog {Name = "Hello"}; 
        _target.AddBlog(expected); 
        Blog actual = _context.Blogs.First(); 
        Assert.AreEqual(expected, actual); 
    } 
    /// <summary> 
    ///A test for GetAllBlogsOrderedByName 
    ///</summary> 
    [Test] 
    public void GetAllBlogsOrderedByNameTest() 
    { 
        FakeDbSet<Blog> blogs = new FakeDbSet<Blog>(); 
        IOrderedQueryable<Blog> expected = blogs.OrderBy(b => b.Name); 
        IOrderedQueryable<Blog> actual = _target.GetAllBlogsOrderedByName(); 
        Assert.AreEqual(expected, actual); 
    } 
}   

可互换的数据访问点

在原始帖子中,我提到了可以更换 ORM,这当然不是一个常规场景,但未来可能会发生。

更可能发生的场景是仓库层有多个或不同的端点。可能有一个仓库需要同时连接到数据库和一些静态文件。

通过创建仓库层,使用数据的程序本身不必关心或考虑数据来自哪里,只关心它以 LINQ 对象的形式返回以供进一步使用。

© . All rights reserved.