【问题标题】:Inherit a base class and an interface same time同时继承基类和接口
【发布时间】:2021-11-18 23:33:51
【问题描述】:

在本课程项目中,老师为数​​据访问创建了一个抽象基类(EfEntityRepositoryBase),继承抽象基类的每个实体的具体类(ProductDal)并实现一个接口(IEntityRepository)。 ProductDal 也有它的接口 (IProductDal),它也实现了 IEntityRepository。

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

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();
        }
    }
}

【问题讨论】:

  • 你能不能也显示 EfEntityRepositoryBase
  • 您好,在文本中也添加了 EfEntityRepositoryBase 类代码。
  • 能否请您提供IEntity 的代码?
  • (抽象类没有标记abstract。)你是正确的,写在顶部的类,public class ProductDal : EfEntityRepositoryBase&lt;Product&gt;, IProductDal,通过两种方式变成IEntityRepository&lt;Product&gt;(一种通过它的基类,和另一个通过它实现的接口的基本接口)。至少这是允许的。我不确定这是否有任何问题。也许是的。至少看起来没有任何接口成员有多个实现。
  • 这里是a simpler example (C# source encoded in URL),一个类可以“重新实现”一个接口(如果你取消注释, I)。这也是允许的,也可能不是一个好主意。我知道这不是您要问的,但在我的示例中,最好在基类 B 中有 virtual 成员并在继承类 C 中覆盖它。

标签: c# interface base-class


【解决方案1】:

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


作为旁注,您应该知道,如果类 EfEntityRepositoryBase&lt;TEntity&gt; 将使用与 Product 不同的泛型参数类型定义,例如,intProductDal 将有两个实现/成员重载IEntityRepository&lt;T&gt; 接口。 例如:

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&lt;TEntity&gt; 已经提供了 IEntityRepository&lt;Product&gt; 和 IProductDal 接口的实现。


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

Given 是您的 ProductDal 类型,具有以下类签名

public class ProductDal : EfEntityRepositoryBase<int>, IProductDal

您现在有多种类型可用于访问IEntityRepository&lt;Product&gt; 的实现

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&lt;Product&gt; 已经实现了IEntityRepository&lt;Product&gt;,但仍然让ProductDal 额外实现IProductDal 接口是非常有意义的,以便在只有IProductDal 接口已知的情况下启用ProductDal


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

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();
}

Given 是您的 ProductDal 类型,具有以下类签名

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();
}

【讨论】:

    【解决方案2】:

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

    让我们看看这些类和接口中的每一个都为您提供了什么......

    ProductDal

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

    IProductDal

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

    IEntityRepository

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

    EfEntityRepositoryBase

    此抽象类包含 IEntityRepository 中特定于实体框架的基本 CRUD 方法的实现,以节省您在 ProductDal 等类中重复这些方法。您可以拥有另一个抽象类,其中实现了另一个 ORM(例如 NHibernate)的这些方法,如果您要从使用实体框架切换到使用 NHibernate,那么您只需要更改每个 ProductDal 等的继承类,以便它们派生自 NHibernateEntityRepository&lt;T&gt; 而不是 EfEntityRepositoryBase&lt;T&gt;

    但回到问题...

    我无法理解IProductDal 实现IEntityRepository 的意义,因为ProductDal 已经继承了实现相同接口的抽象基类

    如果您使用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 { } });
    }
    

    在上述测试中,如果IProductDal 派生自EfEntityRepositoryBase(这是定义GetAll 方法的地方),编译器只理解m =&gt; m.GetAll

    类似的问题也会出现在任何使用 IProductDal 的生产代码中 - 毕竟,该代码是 IProductDal / ProductDal 的消费者,就像上面的单元测试一样。

    总之...

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

    【讨论】:

    • 但是 why IProductDal 实现 IEntityRepository&lt;Product&gt; 的原因是为了在单元测试中启用依赖注入和模拟。对于这个特定的问题,关于 how 进行依赖注入和模拟的讨论可能有点先进,但我觉得对 why 这是一个很好的模式的解释是按顺序。
    • 由于依赖注入或模拟,接口不存在。实际上,您不是模拟接口而是模拟类,即接口的实现。接口是面向对象语言中的一个基本概念。他们提供合同。接口允许在不知道实际实现的情况下编写代码。接口有助于实现多态性、IoC、类型层次结构,同时避免多重继承、应用程序或架构接口、可扩展性、封装和数据隐藏。
    • 依赖注入只是一种使用抽象工厂、模板、策略、存储库等接口的设计模式。针对接口进行编程并不意味着依赖注入。依赖注入是一种实现可扩展性的设计模式。虽然针对接口进行编程可以实现灵活的代码和“通用”操作行为:对具体 ProductDal 进行操作的方法只能用于这种类型,而对接口 IProductDal 进行操作的方法可以用于每种类型,只要它实现界面。
    • 这才是真正的魔力。例如,IDisposable 不存在以启用依赖注入或模拟。它启用了一个框架:一个实现完整逻辑的库,该逻辑由实现预期接口类型的客户端代码扩展。你不知道为什么 IProductDal 实现了一个接口——作者知道。
    • @sbridewell 也许这就是老师建立这样的结构的原因。我现在不能确定,但​​是当项目进展时我会在这里看看。
    猜你喜欢
    • 1970-01-01
    • 2017-07-18
    • 2013-02-25
    • 1970-01-01
    • 1970-01-01
    • 2011-11-03
    • 2017-12-29
    • 2014-12-24
    • 1970-01-01
    相关资源
    最近更新 更多