【问题标题】:Should I use an interface or abstract class in this scenario?在这种情况下我应该使用接口还是抽象类?
【发布时间】:2014-08-12 19:43:19
【问题描述】:

我有一个包含多个方法的类,其中一个是 Save() 方法。我想有不同的保存实现:保存到数据库、保存到文件等。所以,说这是类:

MyClass 
  Add()
  Remove()
  Save()

添加和删除实现对所有人都是通用的。只有 Save 会有所不同。在这种情况下,使用 Save 作为抽象方法创建一个抽象类会更合适吗?或者使用接口有什么好处?如果我在这里使用接口,我需要将 Add 和 Remove 的实现复制到所有实现该接口的类,对吗?在这里只使用一个带有 Save 方法的 ISave 接口是没有意义的,因为我所有的实现类仍然需要定义 Add 和 Remove?

只是想更好地理解接口和抽象类的用法......

不确定编程语言是否会有所不同,但我使用的是 C#。

【问题讨论】:

  • 您可以设计程序以非常有效地使用其中任何一个。
  • @Servy 如果我走接口路线,我怎么能不复制添加和删除的实现呢?
  • 虽然两者都可以,但 C# 并不真正支持接口上的具体方法。您可以在需要时使用扩展方法解决这个问题,但如果您有一个具体的实现,通常是抽象类的情况。话虽如此,@dbc 提出的让抽象类实现接口的建议还不错。
  • @emodendroket 所以抽象类具有 Add 和 Remove 实现,并实现 Save 接口,但具体类将是实现和履行 ISave 接口合同的类?这可能吗?
  • @Prabhu 正如我从一开始就说的那样,both are just fine。我的全部前提是两者都不是特别好。

标签: c# oop interface abstract-class


【解决方案1】:

查看策略设计模式:http://en.wikipedia.org/wiki/Strategy_pattern

您可以创建一个 ISave 接口,并为您需要使用的每个保存方法创建此接口的一个实现。 FileSave、DBSave 等。

您可以将 ISave 参数添加到 MyClass 的构造函数,然后根据您要使用的保存方法将正确的实现传递给您的类。

【讨论】:

    【解决方案2】:

    在您的特定场景中,我建议您使用 Abstract 类。根据 MSDN Recommendations for Abstract Classes vs. Interfaces,适用于您的方案的建议是:

    • 如果您想在组件的所有实现中提供通用的已实现功能,请使用抽象类。抽象类允许您部分实现您的类,而接口不包含任何成员的实现。

    在您的 Abstract 类中,您可以为“添加”和“删除”方法提供功能,并让实现者要求使用抽象(或 VB 中的 MustOverride)覆盖 Save 方法。

    查看完整文章了解更多信息。

    【讨论】:

      【解决方案3】:

      我建议两者都做。由于两个派生类之间有很多共同的代码,继承层次结构是有意义的。但是,如果您为您的类提取一个接口并始终通过它访问它们,那么如果您发现您需要一个具有不同添加、保存和删除方法的新类,它将为您提供未来的灵活性。

      【讨论】:

      • 在这种情况下你会如何使用两者?如果我有一个 DBMyClass 和 FileMyClass 类,它们将继承自 MyClass 抽象类,然后还实现一个只有 Save 方法的 ISave 接口?
      • @Prabhu 让你的抽象类实现接口。
      【解决方案4】:

      两者都用总是好的。

      首先,定义一个接口

      public interface IMyInterface
      {
          bool Add();
          bool Remove();
          bool Save();
      }
      

      然后定义一个抽象类

      public abstract MyBaseClass : IMyInterface
      {
          public bool Add() { ... }
      
          public bool Remove() { ... }
      
          public abstract bool Save();
      }
      

      并在那些实现 MyBaseClass 的类中实现您的 Save 变体。

      现在,当您需要传递对象时,在代码中的每个位置都使用 IMyInterface。

      这也使您能够测试事物。 当“添加”和Remove 访问数据库以从表中添加/删除内容时,测试变得困难。 然后您需要设置一个测试数据库环境,将您的测试指向该环境,并且您必须确保在运行测试时始终在干净的表上进行操作。 但是,如果您只想测试调用 Add 函数的某个事物的行为,而该函数在 Add 返回 truefalse 时表现不同,该怎么办?

      public class Foo
      {
          public int AddRange(MyBaseClass my, int count)
          {
              int retval = 0;
              while (count > 0)
              {
                  if (my.Add())
                      ++retval;
                  --count;
              }
              return retval;
          }
      }
      

      这现在很难测试,因为Add 绑定到MyBaseClass。它会命中数据库和所有在您的测试期间可能可靠的东西。但幸运的是,您使用的接口。所以把AddRange的签名改成

      public int AddRange(IMyInterface my, int count);
      

      并定义您的测试

      [TestMethod]
      void AddRangeReturns0ForFailedAdd()
      {
          var foo = new Foo();
          Assert.AreEqual(0, foo.AddRange(new Fail(), 0));
          Assert.AreEqual(0, foo.AddRange(new Fail(), 1));
          Assert.AreEqual(0, foo.AddRange(new Fail(), 100));
      }
      
      class Fail : IMyInterface
      {
           public bool Add() { return false; }
      
           public bool Remove() { return false; }
      
           public bool Save() { return false; }
      }
      

      【讨论】:

      • 在这种情况下,当您使用它时,接口会向代码本身添加零功能值。这实际上只是说明了一种完全不相关的做法,即让您编写的所有内容都通过一个接口实现它的整个公共 API 以进行测试。取决于应用程序的测试方式,这可能是合适的,但与此处描述的问题无关。
      • @esskar 我最终选择了 Joel 的回答(以及 Servy 的评论),但感谢您的回答,因为它确实帮助我了解了其他方法。
      猜你喜欢
      • 2011-12-23
      • 2018-04-02
      • 1970-01-01
      • 2019-01-20
      • 1970-01-01
      • 1970-01-01
      • 2014-03-25
      • 2017-03-05
      • 1970-01-01
      相关资源
      最近更新 更多