关于C#:在存储库模式中使用接口优于抽象类的优势?

Advantage of using Interface over abstract class for repository pattern?

本问题已经有最佳答案,请猛点这里访问。

Possible Duplicate:
Interface vs Base class

通常会看到使用接口实现的存储库模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface IFooRepository
{
   Foo GetFoo(int ID);
}

public class SQLFooRepository : IFooRepository
{
   // Call DB and get a foo
   public Foo GetFoo(int ID) {}
}

public class TestFooRepository : IFooRepository
{
   // Get foo from in-memory store for testing
   public Foo GetFoo(int ID) {}
}

但是您也可以使用抽象类来实现这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class FooRepositoryBase
{
    public abstract Foo GetFoo(int ID);
}

public class SQLFooRepository : FooRepositoryBase
{
    // Call DB and get a foo
    public override Foo GetFoo(int ID); {}
}

public class TestFooRepository : FooRepositoryBase
{
    // Get foo from in-memory store for testing
    public override Foo GetFoo(int ID); {}
}

在存储库场景中,与抽象类相比,使用接口的具体优势是什么?

(也就是说,不要只是告诉我您可以实现多个接口,我已经知道了这一点——为什么要在存储库实现中这样做)

编辑以澄清-"msdn-在类和接口之间选择"等页面可以解释为"选择类而不是接口,除非有充分的理由不这样做"-在存储库模式的特定情况下,什么是好的理由


在这个实例中,与抽象类相比,使用接口的主要优点是接口是完全透明的:这是一个更大的问题,您无法访问继承的类的源。

但是,这种透明性允许您生成一个已知范围的单元测试:如果您测试一个接受接口作为参数的类(使用依赖项注入方法),您就知道您正在用已知的数量测试该类;接口的测试实现将只包含您的测试代码。

同样,当测试存储库时,您知道您只测试存储库中的代码。这有助于限制测试中可能的变量/交互的数量。


就我个人而言,我倾向于有一个接口来保存纯"业务相关"的方法的签名,例如Foo GetFoo()void DeleteFood(Foo foo)等。我还有一个通用抽象类,它保存受保护的方法,如T Get()void Delete(T obj)

我在抽象的Repository类中保护我的方法,这样外部世界就不知道水管工程(Repository看起来像object,而只知道通过接口的业务模型。

除了拥有水管厂的另一个优势外,我还有一个优势,例如,我有一个Delete方法(protected)可供任何存储库使用,但它不是公共的,因此我不必在存储库中强制实现它,因为它没有从我的数据源中删除某些内容的业务含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public abstract class Repository<T>
{
    private IObjectSet objectSet;

    protected void Add(T obj)
    {
        this.objectSet.AddObject(obj);
    }

    protected void Delete(T obj)
    {
        this.objectSet.DeleteObject(obj);
    }

    protected IEnumerable<T>(Expression<Func<T, bool>> where)
    {
        return this.objectSet.Where(where);
    }
}

public interface IFooRepository
{
    void DeleteFoo(Foo foo);
    IEnumerable<Foo> GetItalianFoos();
}

public class FooRepository : Repository<Foo>, IFooRepository
{
    public void DeleteFoo(Foo foo)
    {
        this.Delete(foo);
    }

    public IEnumerable<Foo> GetItalianFoos()
    {
        return this.Find(foo => foo.Country =="Italy");
    }
}

使用抽象类而不是接口进行水管工程的好处是,我的具体存储库不必实现它们不需要的方法(例如,DeleteAdd,但如果需要,它们可以随时使用。在当前上下文中,某些foo没有业务原因,因此该方法在接口上不可用。

对于业务模型,使用接口比使用抽象类的优势在于,接口提供了如何从业务端操作Foo的答案(删除一些foo是否有意义?创造一些?等等)。在单元测试时,使用这个接口也更容易。我使用的抽象Repository不能进行单元测试,因为它通常与数据库紧密耦合。它只能在集成测试中进行测试。将抽象类用于存储库的业务目的将阻止我在单元测试中使用它们。


这是一个适用于任何类层次结构的一般问题,而不仅仅是存储库。从纯OO的角度来看,接口和纯抽象类是相同的。

如果您的类是公共API的一部分,那么使用抽象类的主要优点是您可以在将来添加方法,而不会有破坏现有实现的风险。

有些人还喜欢将接口定义为"类可以做的事情",而将基类定义为"类是什么",因此只将接口用于外围功能,并始终将主要功能(如存储库)定义为类。我不知道我站在哪里。

为了回答您的问题,我认为在定义类的主要函数时使用接口没有任何好处。


由于模式起源于域驱动的设计,这里有一个DDD答案:

存储库的契约通常在域层中定义。这允许域和应用程序层中的对象操作存储库的抽象,而不必关心它们的实际实现和底层存储细节——换句话说,就是说,这些对象对持久性一无所知。此外,我们通常希望在某些存储库的契约中包含特定的行为(除了您的普通add()、getbyid()等),因此我更喜欢ISomeEntityRepository形式,而不仅仅是IRepository,我们稍后将了解它们为什么需要作为接口。

另一方面,存储库的具体实现驻留在基础结构层(或测试存储库的测试模块中)。它们实现了上述存储库契约,但也有它们自己的持久性特定特性范围。例如,如果您正在使用nhibernate来持久化您的实体,那么使用nhibernate会话和其他nhibernate相关的通用管道将一个超类放到所有nhibernate存储库中就可以派上用场。

由于您不能继承几个类,所以最终的具体存储库继承的这两个类之一必须是一个接口。

域层契约是一个接口(ISomeEntityRepository)更合乎逻辑,因为它是一个纯粹的声明性抽象,不能对将要使用的底层持久性机制做任何假设,也就是说,它不能实现任何东西。

持久性特定的存储库可以是一个抽象类(基础结构层中的NHibernateRepositoryNHibernateRepository,它允许您在那里集中一些行为,这些行为对于将要存在的整个持久性存储库特定的存储库范围是通用的。

结果是:

1
2
3
4
public class SomeEntityRepository : NHibernateRepository<SomeEntity>, ISomeEntityRepository
{
  //...
}

我想关键的区别是,抽象类可以包含私有属性和方法,其中接口不能,因为它只是一个简单的约定。

作为接口的结果总是"这里没有恶作剧-你看到的就是你得到的",而抽象的基类可能会允许副作用。


虽然从纯实际的角度来看,其他框架可能需要添加更多内容,但大多数IOC框架在接口->类映射方面工作得更好。您可以在接口和类上具有不同的可见性,而对于继承,可见性必须匹配。

如果你不使用国际奥委会的框架,从我的角度来看没有什么区别。提供程序基于抽象基类。


看看TimMcCarthy的存储库框架的实现。

他使用像IRepository这样的接口来定义契约,但是他也使用像RepositoryBase或他的SqlCeRepositoryBase < T >这样的抽象类来实现IRepository。抽象基类是用来消除大量不可靠代码的代码。特定于类型的存储库只需继承抽象基类,并需要为其目的添加代码。API的用户可以通过契约对接口进行编码。

因此,您可以将这两种方法结合起来使用它们的优势。

另外,我认为大多数IOC框架都可以处理抽象类。