同时继承一个基class和一个接口

Inherit a base class and an interface same time

在这个课程项目中,老师创建了一个抽象基础class (EfEntityRepositoryBase)用于数据访问,一个具体class对于继承抽象基础 class 并实现 接口 (IEntityRepository) 的每个实体 (ProductDal)。 ProductDal 也有其 接口 (IProductDal),它也实现了 IEntityRepository。

这样做的用例是什么? 我无法理解 IProductDal 实现 IEntityRepository 的意义,因为 ProductDal 已经继承了实现相同接口的抽象基础 class。 因此,如果 IEntityRepository 中的任何函数更新,应该是没问题。如果有人可以解释,那就太好了。下面是抽象 class 和接口代码。

public class ProductDal : EfEntityRepositoryBase<Product>, IProductDal{ }

public interface IEntityRepository<T>
{
    void Add(T entity);
    void Delete(T entity);
    void Update(T entity);
    List<T> GetAll(Expression<Func<T, bool>> expression = null);
    T GetById(Expression<Func<T, bool>> expression);
}

public interface IProductDal: IEntityRepository<Product>
{
}

public class EfEntityRepositoryBase<TEntity> : IEntityRepository<TEntity> where TEntity : class, IEntity, new()
{
    public void Add(TEntity entity)
    {
        using (BookStoreTrackerDBContext context = new BookStoreTrackerDBContext())
        {
            var addedEntity = context.Entry(entity);
            addedEntity.State = EntityState.Added;
            context.SaveChanges();
        }
    }
}

这个问题的根源是如何在依赖注入和模拟/单元测试中使用接口。

让我们看看每个 classes 和接口给你什么...

ProductDal

此 class 包含与数据存储中保存的 Product 个实例进行交互的逻辑。

IProductDal

如果您想使用 ProductDal 与数据存储中的 Product 实例进行交互,将其声明为 IProductDal 可以让您对依赖于ProductDal 来创建 IProductDal 的模拟实例,但是您的单元测试需要它的行为,因此您对使用 ProductDal 的代码的单元测试不依赖于实际的数据库。在您的生产代码中,您可以使用依赖注入在声明 IProductDal 的地方注入真正的 ProductDal

IEntityRepository

这看起来像是一个相当通用的接口,它指定了一些基本的 CRUD 操作,这些操作可能对您可能保存在数据存储中的任何实体都有用,并且它不知道数据存储的类型是什么(例如 SQL server / MongoDB / Cassandra / imp 和一个装满笔记本的文件柜)或者你可能用来访问数据存储的对象关系映射器(例如 Entity Framework / NHibernate)。

EfEntityRepositoryBase

此摘要 class 包含 IEntityRepository 中基本 CRUD 方法的实现,它特定于 Entity Framework,以避免您在 ProductDal 等中重复这些方法classes。你可以有另一个抽象class,其中包含另一个 ORM(例如 NHibernate)的这些方法的实现,如果你要从使用 Entity Framework 切换到使用 NHibernate,那么你只需要更改继承您的每个 ProductDal 等 classes,以便它们派生自 NHibernateEntityRepository<T> 而不是 EfEntityRepositoryBase<T>.

但回到问题...

I can't understand the point of IProductDal implementing IEntityRepository, since ProductDal already inherits the abstract base class that implements the same interface

如果您使用 ProductDal 而不是 IProductDal 来与数据存储中的 Product 实例进行交互,那么它对您的生产代码,但是你的单元测试真的很难写,而且可能会慢到 运行 而且相当不稳定,因为它们将依赖于一个真正的数据存储,它在开始时处于完全正确的状态每个测试用例。

如果您使用 IProductDal 与数据存储中的 Product 实例交互,并使用依赖注入来使用 ProductDal,其中 IProductDal 是您的生产代码需要(以便该代码的单元测试不依赖于真实的数据存储),您会遇到一个不同的问题。考虑这个单元测试(它使用 Moq 模拟框架,但无论您使用什么模拟框架,都存在相同的问题):

[Fact]
public void Test1()
{
    var mockProductDal = new Mock<IProductDal>();
    mockProductDal.Setup(m => m.GetAll(null))
                  .Returns(new List<Product> { new Product { } });
}

在上面的测试中,编译器只理解 m => m.GetAll 如果 IProductDal 是从 EfEntityRepositoryBase 派生的(这是定义 GetAll 方法的地方)。

类似的问题也会出现在任何使用 IProductDal 的生产代码中 - 毕竟,该代码是 IProductDal / ProductDal 的消费者,其方式与上面的单元测试是.

简而言之...

在生产代码中引用接口而不是实现。在生产代码中使用依赖注入来注入真正的实现,并在单元测试中模拟接口以消除对数据存储等外部资源的依赖。如果你这样做,那么你还需要做一些明显奇怪的事情,比如当实现 Interface1 的 class 派生自 class 时,使 Interface1 实现 Interface2实现 Interface2。但它会让你的单元测试更容易编写,更好,更好的单元测试只能是一件好事。

我认为很容易理解您的感受,在查看您提供的示例时,很想调用 IProductDal 接口是多余的。事实上,它不会向类型 ProductDal 添加任何额外的成员,因为接口 IProductDal 和泛型 class EfEntityRepositoryBase 是使用相同的泛型参数类型定义的 Product。由于这些教学示例并非设置在实际应用代码的上下文中,因此其真正意图或背后的想法并不容易理解。


作为旁注,您应该知道如果 class EfEntityRepositoryBase<TEntity> 将使用与 Product 不同的通用参数类型定义,例如 intProductDal 将有两个 implementations/member 重载 IEntityRepository<T> 接口。 例如:

public class ProductDal : EfEntityRepositoryBase<int>, IProductDal
{ 
  // Implementation of IProductDal. The EfEntityRepositoryBase type provides another 'int' overload
  public void Add(Product entity) {}
}

void Main()
{
  var productDal = new ProductDal();
  
  // Implementation of IEntityRepository<int> provided by class EfEntityRepositoryBase<int>
  productDal.Add(6);

  // Implementation of 'IProductDal' (provided by class 'ProductDal')  
  productDal.Add(new Product());
}

您可以看到您提供的示例显示了一种特殊情况,其中 EfEntityRepositoryBase<TEntity> 已经提供了 IEntityRepository<Product> 和 IProductDal 接口的实现。


回到你的例子:如果你使用类型转换,你会发现另一个具有所谓冗余类型定义的用例:

给出的是您的 ProductDal 类型,具有以下 class 签名

public class ProductDal : EfEntityRepositoryBase<int>, IProductDal

您现在有多种类型可用于访问 IEntityRepository<Product>

的实现
void Main()
{
  // Create an instance of ProducDal
  ProductDal productDal = new ProductDal();

  /* Use the instance of ProductDal with multiple overloads 
     to show implicit type casting */
  UseProductDal(productDal);
  UseIProductDal(productDal);
  UseIEntityRepository(productDal);
  UseEntityRepository(productDal);
}

void UseProductDal(ProductDal productDal)
{
  // Instantiate the argument
  var product = new Product(); 
  productDal.Add(product);
}

void UseIProductDal(IProductDal productDal)
{
  // Instantiate the argument
  var product = new Product(); 
  productDal.Add(product);
}

void UseIEntityRepository(IEntityRepository<Product> productDal)
{
  // Instantiate the argument
  var product = new Product(); 

  productDal.Add(product);
}

void UseEntityRepositoryBase(EntityRepositoryBase<Product> productDal)
{
  // Instantiate the argument
  var product = new Product(); 
  productDal.Add(product);
}

这展示了如何使用隐式类型转换以及如何使用接口。
您现在看到,尽管 EntityRepositoryBase<Product> 已经实现了 IEntityRepository<Product>,但仍然让 ProductDal 额外实现 IProductDal 接口非常有意义,以便使 ProductDal 可以在什么地方使用只有 IProductDal 接口是已知的。


您可以使用接口转换来隐藏成员。例如,如果您向每个接口添加独占成员,则只有在将实现者转换为相应接口时才能访问此成员:

public interface IEntityRepository<T>
{
  void Add(T entity);  
}

public interface IProductDal: IEntityRepository<Product>
{
  // Exclusive member. Will be only visible when accessed through this interface. 
  int GetProductCount();
}

给出的是您的 ProductDal 类型,具有以下 class 签名

public class ProductDal : IEfEntityRepository<int>, IProductDal

void Main()
{
  // Create an instance of ProducDal
  ProductDal productDal = new ProductDal();

  /* Use the instance of ProductDal with multiple overloads 
     to show implicit type casting */
  UseProductDal(productDal);
  UseIProductDal(productDal);
  UseIEntityRepository(productDal);
  UseEntityRepository(productDal);
}

// All implemented interfaces are visible since there is no casting involved.
// All members are referenced via the implementor type ProductDal.
void UseProductDal(ProductDal productDal)
{
  // Instantiate the argument
  var product = new Product(); 
  
  productDal.Add(product);
  int productCount = productDal.getProductCount();
}

// Only 'IProductDal' is visible since there is an implicit cast to an interface type involved
void UseIProductDal(IProductDal productDal)
{
  // Instantiate the argument
  var product = new Product(); 

  // 'Add()' is provided by 'IEntityRepository<T>', 
  // which is implemented by 'IProductDal' and therefore "visible"
  productDal.Add(product); 

  // 'GetProductCount()' is provided by 'IProductDal'
  int productCount = productDal.GetProductCount();
}

// Only 'IEntityRepository<T>' is visible since there is an implicit cast to the interface type 
void UseIEntityRepository(IEntityRepository<Product> productDal)
{
  // Instantiate the argument
  var product = new Product(); 

  productDal.Add(product);

  // 'GetProductCount()' is only available via the 'IProductDal' interface. 
  // It's not visible here.
  //int productCount = productDal.GetProductCount();
}