【问题标题】:When should one use interfaces?什么时候应该使用接口?
【发布时间】:2010-12-13 18:11:35
【问题描述】:

我知道接口没有主体,只有方法定义。但是什么时候应该使用接口呢?如果我为某人提供一组没有主体的接口,他们为什么会觉得有必要编写函数体?他们是否最好编写自己的带有抽象方法的抽象类。

编辑:

我想当您是团队的一员时,更多地使用接口。假设团队 A 为某事编写代码,并且他们想查看是否调用了某个方法。名称为 getRecords(),完成与否。这将有助于团队 B 编写提供给他们的接口主体,团队 B 必须保持方法名称相似,以便团队 A 的代码运行。

只是一个想法。我可能错了。我认为接口对单个开发人员没有用处。

编辑:

感谢大家的回答。根据大家的回答,我认为接口在您制作 API 之类的东西时更有用?

【问题讨论】:

  • 这不一定只适用于团队。而不是团队,从你的 API 的消费者的角度来考虑它。您可能正在编写两个不同的类。您可能正在构建一个在客户端-服务器模型中工作的解决方案。您可能正在创建需要使用的 Web 服务。无论如何,接口都是你的朋友。他们将帮助您管理调用者与被调用者的关系。
  • 不是“什么时候应该”,而是总是分别制作接口和实现。
  • 团队合作并不是界面使用的驱动力。接口对于单个开发人员来说非常有用。
  • 理论上你有一个分叉的类层次结构,你需要在两个不共享祖先或共享祖先但相距甚远的类中具有类似的功能。让我们进一步说,无论如何,这两个类的功能都需要不同。繁荣,使用接口,因为它在性能、代码设计和可维护性方面会更好。 NEEDING 接口很少见,大多数事情都是用inheritance + polymorphism 解决的。
  • 界面很棒,因为它确保您拥有所需的一切。它不允许你忘记任何事情。这就像如果你去学校并且你有一个界面,你必须记得带上你的笔记本电脑和你需要的任何学校设备。

标签: oop interface


【解决方案1】:

在 Java 和 C# 等语言中,接口为类提供了一种以polymorphic 方式存在的方法。也就是说,一个类可以满足多个契约——它可以表现为多种不同的类型,一种类型的类可以替换为另一种类型。在其他语言中,这也可以由multiple inheritance 提供,但这种方法存在各种缺点。然而,让一个类表现出不止一种类型并不是使用接口的最常见动机。

通过对接口而不是类进行编程,您还可以将程序与特定实现分离。这使得用一个类实现替换另一个类实现变得更加容易。这在编写单元测试时特别有用,您可能希望用轻量级的mock object 交换一些重量级的类实现。如果你的程序只需要一个接口类型,并且重量级对象和模拟对象都实现了该接口,那么它们很容易被替代。

另外,考虑一个简单的 Java 示例,我说有一个在屏幕上显示数据页面的程序。最初我希望它从数据库或 XML 文件中获取数据。如果我编写我的程序以便它使用接口,我可以像这样定义一个接口:

public interface PageDatasource {
    public List<Page> getPages();
}

然后像这样使用它:

PageDatasource datasource = // insert concrete PageDatasource implementation here
List<Pages> pages = datasource.getPages();
display(pages);

然后我可以编写符合此接口的单独数据库和 XML 实现:

public class DatabasePageDatasource implements PageDatasource {
    public List<Page> getPages() {
        // Database specific code
    }
}

public class XmlPageDatasource implements PageDatasource {
    public List<Page> getPages() {
        // XML specific code
    }
}

因为我使用了一个接口,所以我现在可以互换地使用数据库或 XML 实现,而无需更改程序中要求页面数据的部分。 XML 和数据库实现可能会做完全不同的事情,但我的程序所关心的是提供页面数据的对象实现PageDatasource 接口。

【讨论】:

  • 如果你要走接口之路,那么定义 public IList getPages() 或 public IEnumerable getPages() :)
  • 啊,这是一个 Java 示例 - 为了清楚起见,现在更新了答案:)
  • 您好,在继承父类到不同的子类后,重写方法不能达到同样的效果吗?
  • 这很有帮助。尤其是您给出的编写单独的数据库和 XML 实现的具体示例。非常感谢!
【解决方案2】:

界面现实生活类比:

假设您想从图书馆发行一本书。 你会做什么:
1) 去图书馆
2) 找到你感兴趣的书
3)选择图书
4) 前往图书管理员服务台请求他们发行图书。

现在这里的图书管理员是一个界面,这意味着您对图书管理员到底是谁不感兴趣,您只对坐在图书管理员办公桌上的那个人感兴趣(是同意担任图书管理员的合同的人,这意味着该人同意执行馆员的所有行为)

这么说:图书馆员的行为是:
1) 发行书
2) 再发行书3) 还书。
被指定为馆员(即同意适应以上三种行为)的人,必须以自己的方式实施这些行为。

假设 PersonA 想扮演 Librarian 的角色,那么他需要适应上述 3 种行为。作为客户,我们不在乎他如何执行他的图书管理员行为,我们只对那个人感兴趣,他是图书管理员并且他遵守执行图书管理员的任务。
因此,您要做的是通过界面参考[转到图书管理员服务台(这将引导您找到担任图书管理员的人)],然后说未来一个人离开了图书管理员职位,然后 作为客户,如果您接近图书管理员服务台,而不是充当图书管理员的特定人员,则不会影响您。因此,使用接口而不是具体的代码是有益的。

class PersonA implements Librarian {
    public void IssueBook(){...}
    public void ReIssueBook(){...}
    public void ReturnBook(){...}

    //Other person related tasks...
}   

【讨论】:

    【解决方案3】:

    即使作为单一的开发者界面也可以是一个非常有用的工具。让我试着用一个例子来说明。

    假设您正在开发一个系统来管理图书馆目录,而图书馆将借出书籍和 DVD。您决定创建类 BookDvd 来为借出的物品建模,但现在您想要实现多态性以便您可以处理物品而不是书籍或 DVD。问题是 Item 应该是抽象类还是接口?

    在这种情况下,您可能希望使用抽象类,因为 BookDvd 都可以通过父类提供共同的功能,例如检查外出或退货。

    现在假设您要为您的图书馆目录实现一个持久性机制。您已经决定要将一些数据存储在数据库中,一些数据存储在 XML 文件中,一些数据存储在逗号分隔的文件中。所以现在的问题是如何以允许您处理通用持久性 API 的多态方式执行此操作?

    在这种情况下,您可能应该定义一个接口,该接口可以由您的每个提供数据库、XML 和逗号分隔的持久性的类实现,因为这些持久性机制中的每一个都提供类似的功能,即存储和检索数据,但每个都将被实现非常不同。这将允许您轻松更改正在使用的持久性机制,而无需对使用持久性机制的代码进行大量更改。

    【讨论】:

    • @Tony:说明得很好。谢谢。
    【解决方案4】:

    接口存在的原因是由于 OOP 的两个基本概念,即“身份”和“功能”

    类具有功能和标识。通过继承,实例化的对象可以具有很多功能和多个身份。

    接口是没有功能的身份。该功能将由实例化的类提供。

    第三种形式,“mixin”是没有身份的功能。像 ruby​​ 这样的编程语言提供了第三种继承形式。

    您使用接口的方式因编程语言的上下文和您的环境而异,但请记住,接口用于定义强制到对象上的身份。

    【讨论】:

      【解决方案5】:

      接口有助于阐明不同功能单元之间的区别。一个单元依赖另一个单元某事,而不是成为某事。只要对方能界面规定的事情(想想契约),那么它就可以 em> 幕后的一切。

      例如,我有一个条目处理器,它从一个地方读取条目,然后将它们写入另一个地方。它不在乎从什么/在哪里,或从什么/在哪里。它所关心的是它从某种类型的阅读器(使用 IReader 接口)获取条目,并将它们交给某种类型的写入器(使用 IWriter 界面)。

      在我的第一个实现中,IReader 实现者从 SQL 数据库中获取内容,而 IWriter 实现者通过 Web 服务客户端发布它。但是,我们最终会在任一端创建其他实现者来访问其他存储库(FTP 站点、本地网络驱动器上的文件目录等)。

      一直以来,中间的处理器都不关心也不改变。它只是通过那些标准接口进行对话。

      理论上,您可以使用基类(最好是抽象)而不是接口,但这会开始将您的模块更紧密地锁定在一起,从而使您的系统更难维护。松耦合确实让你的生活更轻松,即使你不在一个程序员团队中。每次在系统的不同部分工作时,都将自己视为不同的程序员。每次您重新访问给定部分时,您都必须重新学习它如何维护它。如果您的系统的每一部分都与其他部分紧密耦合,那么您必须保持对整个系统的持续、深入的了解,而不仅仅是您正在处理的部分。

      单个类实现多个接口也有好处,这有点像多重继承。在这种情况下,一个班级可以执行多项工作(可以说这不是很明智,但至少是可能的)。如果您选择使用抽象基类而不是上面的接口,那么您的灵活性就会受到限制。

      【讨论】:

        【解决方案6】:

        乍一看,抽象类和接口似乎很简单。当您还可以提供一些基本实现时,为什么只提供一个接口?经过调查,您会发现还有更多。

        也就是说,使用接口的原因有很多。您可以在 here 找到一篇不错的博客文章。

        也就是说,考虑到您可以创建一个接口(表明您的类绝对支持某些调用/方法签名调用的“合同”),但您只能提供一个抽象类。还要考虑这样一个事实,即您可以创建一个也实现一个或多个接口的抽象类,并从中继承。这不是极端情况。这实际上在用于高可扩展性的 API 中非常频繁地完成。

        查看我指向您的博客文章,您应该彻底了解何时使用它们以及为什么要使用它们。我还强烈推荐一本好书,例如 Microsoft Press 的“CLR via C#”。你会学到很多东西!

        【讨论】:

        • 链接断开.....
        【解决方案7】:

        添加到以前的答案中,接口通过允许您将基于接口的模拟对象注入到代码中来帮助您在单元测试期间,允许您模拟特定场景并将单元测试隔离到特定组件而无需依赖在外部组件上。

        例如,假设您有一个业务逻辑类,它使用数据逻辑类从数据源检索数据,然后对其进行处理。通过为其继承的数据逻辑类创建一个接口,您可以基于该接口创建该类的模拟/假实例,并将其注入您正在单元测试的业务逻辑类中。可以定义模拟实例以期望某些方法/属性调用、在某些点抛出异常、返回有效输出等。这意味着,您的单元测试可以更快/可能更可靠地运行,因为它们不依赖于基础数据源可用/实际上不必连接到它。而且您将单元测试隔离到特定的代码单元。

        【讨论】:

          【解决方案8】:

          接口比抽象类好,因为你可以实现多个接口,而你只能从一个抽象类继承。

          所以你可以这样做:

          class MyRow extends AbstractRow implements ISearchable, ISortable
          {
          
          }
          

          另外,在 StackOverflow 上搜索其他类似问题,例如 Need of interfaces in c#

          【讨论】:

          • 请阅读this博客,了解何时使用抽象类,接口与抽象类类似,但接口支持多重继承。
          【解决方案9】:

          接口和抽象类有不同的用途。 首先,抽象类只适用于继承——它不能直接实例化。您可以将实现代码放在抽象类中,但该代码只能从扩展它的类或通过扩展它的类调用。

          示例:

          public abstract class A {
              public void myFunc() {
                  //do some work
              }
          }
          
          public class B : A {
              private void someOtherFunc() {
                  base.myFunc();
              }
          }
          
          public class Z {
              public void exampleFunc() {
                  //call public base class function exposed through extending class:
                  B myB = new B();
                  myB.myFunc();
          
                  //explicitly cast B to A:
                  ((A) myB).myFunc();
          
                  //'A' cannot be created directly, do implicit cast down to base class:
                  A myA = new B();
                  myA.myFunc();
              }
          }
          

          接口的目的是提供一个契约——这意味着实现它的类必须为接口中声明的属性或函数提供实现,并且它必须使用完全相同的函数签名。当我编写一些调用实现接口的类的代码时,这提供了保证,与这些类的派生源无关,甚至它们是用什么语言编写的,或者我从哪里得到它们都无关紧要。另一个很酷的功能是,我可以为不同的接口对相同的函数签名进行几种不同的实现。检查这个例子,它使用了上一个例子中的一些相同的类:

          public class Z {
              public void exampleFunc() {
                  C myC = new C();
                  //these two calls both call the same function:
                  myC.myFunc();
                  ((IExampleInterface1)myC).myFunc();
          
                  D myD = new D();
                  //hmmm... try and work out what happens here, which myFunc() gets called?
                  //(clue: check the base class)
                  myD.myFunc();
          
                  //call different myFunc() in same class with identical signatures:
                  ((IExampleInterface1)myD).myFunc();
                  ((IExampleInterface2)myD).myFunc();
              }
          }
          
          interface IExampleInterface1
          {
              void myFunc();
          }
          
          interface IExampleInterface2
          {
              void myFunc();
          }
          
          public class C : IExampleInterface1
          {
              public void myFunc()
              {
                  //do stuff
              }
          }
          
          public class D : A, IExampleInterface1, IExampleInterface2
          {
              void IExampleInterface1.myFunc()
              {
                  //this is called when D is cast as IExampleInterface1
              }
          
              void IExampleInterface2.myFunc()
              {
                  //this is called when D is cast as IExampleInterface2
              }
          }
          

          【讨论】:

            【解决方案10】:

            这是一个设计问题(我说的是java世界)。

            该接口允许您定义软件组件(类)的行为和结构,而不受运行时限制。

            相反,抽象类在默认情况下为您提供方法的行为:如果您确定此代码可能不会在应用程序的生命周期内发生变化,则很有用。

            例子:

            您有一个商业系统的 Web 应用程序,它在某些情况下发送电子邮件(注册新用户)。

            如果您确定行为不会经常改变,您可以使用抽象类 SenderCommunication 和方法 boolean sendWithSuccefull(...)。

            如果您不确定行为不会经常改变,您可以使用接口 InterfaceSenderCommunication 和方法 boolean sendWithSuccefull(...)。

            当然,“经常 - 不经常”的判断取决于两个要素:

            • 我必须花多少时间进行同步 旧代码与新规范?
            • 客户支付了多少? ;)

            【讨论】:

              【解决方案11】:

              假设我有一个 FoodEstablishment 类,现在我想做简单的 CRUD 操作,我该怎么做呢? 我为服务定义了一个接口,但是为什么呢?

              public interface IFoodEstablishmentService
              {
                  Task<int> AddAsync(FoodEstablishment oFoodEstablishment);
                  FoodEstablishment SelectByFoodEstablishmentId(long id);
                  Task<int> UpdateAsync(FoodEstablishment oFoodEstablishment);
                  Task<int> DeleteAsync(FoodEstablishment oFoodEstablishment);
              }
              

              然后我将为该特定服务实现该合同或接口

              public class FoodEstablishmentService : IFoodEstablishmentService
              {
                  public async Task<int> AddAsync(FoodEstablishment oFoodEstablishment)
                  {
                     // Insert Operation
                      return result;
                  }
              
                  public FoodEstablishment SelectByFoodEstablishmentId(long id)
                  {
                      // Select Logic
                      return oFoodEstablishment;
                  }
              
                  public async Task<int> UpdateAsync(FoodEstablishment oFoodEstablishment)
                  {
                      // Update Logic
                      return result;
                  }
              
                  public async Task<int> DeleteAsync(FoodEstablishment oFoodEstablishment)
                  {
                      // Delete Logic
                      return result;
                  }
              
              }
              

              所以在我的主程序或我希望使用服务的地方,我会这样做

              IFoodEstablishmentService oFoodEstablishmentService =  new FoodEstablishmentService();
              FoodEstablishment oFoodEstablishment = // Input might be from views;
              oFoodEstablishmentService.AddAsync(oFoodEstablishment);
              

              所以到目前为止,这似乎是一个额外的步骤,我们可以直接完成

              FoodEstablishmentService oFoodEstablishmentService =  new FoodEstablishmentService();
              FoodEstablishment oFoodEstablishment = // Input might be from views;
              oFoodEstablishmentService.AddAsync(oFoodEstablishment);
              

              但是,如果我可能需要通过队列而不是直接将插入逻辑传递给服务器,等待插入操作完成然后返回结果,而不是传递队列然后队列工作者处理这些操作,可能不会排队插入的最佳主意,但对于接口示例来说绝对是好的:-)。所以现在我要做的是,创建另一个类 FoodEstablishment 实现相同的合同 IFoodEstablishment。

              public class FoodEstablishmentQueueService : IFoodEstablishmentService
              {
                  public async Task<int> AddAsync(FoodEstablishment oFoodEstablishment)
                  {
                     // Insert Queue Operation
                      return result;
                  }
              
                  public FoodEstablishment SelectByFoodEstablishmentId(long id)
                  {
                      // Select Queue Logic
                      return oFoodEstablishment;
                  }
              
                  public async Task<int> UpdateAsync(FoodEstablishment oFoodEstablishment)
                  {
                      // Update Queue Logic
                      return result;
                  }
              
                  public async Task<int> DeleteAsync(FoodEstablishment oFoodEstablishment)
                  {
                      // Delete Queue Logic
                      return result;
                  }
              }
              

              所以现在如果我想使用队列版本,我会这样做

              IFoodEstablishmentService oFoodEstablishmentService =  new FoodEstablishmentQueueService();
              FoodEstablishment oFoodEstablishment = // Input might be from views;
              oFoodEstablishmentService.AddAsync(oFoodEstablishment);
              

              我们可以通过使用类的旧方法来做到这一点,但是这会将类实例化与特定类绑定,这对于扩展来说有点僵硬,现在 FoodEstablishmentQueueService 也可以做其他事情,创建另一个方法直到合同有效,所以接口是联系以确保一致性,想象一个人在做普通版本,另一个人在做队列版本,或者有人在做缓存版本,除非它预先指定了工作合同并且人们最终不会交叉检查所有内容,否则签名可能会出现问题。

              同样,让我们​​考虑其他使用预定义类型(如 IEnumerable)的简单示例。假设我传递了一个 FoodEstablishment 集合列表并返回自定义排序列表

              public FoodEstablishment[] SortFoodEstablishment(FoodEstablishment[] list)
              {
                  foreach(var oFoodEstablishment in list)
                  {
                  // some logic
                  }
                  return sortedList;
              }
              

              所以我们会这样使用它

              FoodEstablishment[] list = new FoodEstablishment[]{ //some values }
              var listSorted = oFoodEstablishmentService.SortFoodEstablishment(list);
              

              但是如果我们发送列表而不是数组呢

              List<FoodEstablishment> list = //some values;
              var listSorted = oFoodEstablishmentService.SortFoodEstablishment(list);
              

              我们会得到错误,因为它使用类的严格实现,所以我们使用由 List 实现的 IEnumerable,它基本上消除了从 List 到接口的依赖

              public IEnumerable<FoodEstablishment> SortFoodEstablishment(IEnumerable<FoodEstablishment> list)
              {
                  foreach(var oFoodEstablishment in list)
                  {
                  // some logic
                  }
                  return sortedList;
              }
              

              所以通常实施很大程度上取决于情况

              【讨论】:

                【解决方案12】:

                使用接口的一个原因是一个类将实现多个接口。抽象类无法做到这一点。

                一个例子是处理鼠标移动和按键的类将实现(虚构的)IMouseMove 和 IKeyPress 接口。

                【讨论】:

                  【解决方案13】:

                  此外,使用接口可以简化单元测试。为了测试依赖于接口的类,只要你使用某种依赖注入,你可以创建实现依赖接口的存根类,或者你可以使用模拟引擎。

                  【讨论】:

                    【解决方案14】:

                    Java 语言不支持多重继承,因为接口是用来实现目标的。

                    对于一个抽象的类,只有一个方法必须是抽象的;而如果 接口所有方法都是抽象的。

                    【讨论】:

                      【解决方案15】:

                      抽象类v/s接口一直是开发人员讨论的焦点。我会加我的 5 美分。 当您想要扩展一个命令库并且想要为抽象方法提供默认实现时,请使用抽象类。

                      当您想完全实现实现接口的类的所有抽象方法并且不能提供方法的默认主体时,请使用接口。

                      【讨论】:

                        【解决方案16】:

                        在 Set 类型的变量中存储对 HashSet 或 TreeSet 的引用被认为是一种很好的风格。

                        Set&lt;String&gt; names = new HashSet&lt;String&gt;();

                        这样,如果您决定改用TreeSet,则只需更改一行。

                        此外,对集合进行操作的方法应指定 Set 类型的参数:

                        public static void print(Set&lt;String&gt; s)

                        那么该方法可以用于所有集合实现。

                        理论上,我们应该对链表提出同样的建议,即保存 List 类型的变量中的 LinkedList 引用。但是,在 Java 库中,List 接口对ArrayListLinkedList 类都是通用的。特别是,它具有用于随机访问的 get 和 set 方法,尽管这些方法对于链表来说效率非常低。

                        如果您不知道随机访问是否有效,您就无法编写有效的代码。 这显然是标准库中的严重设计错误,我不建议使用 出于这个原因的 List 界面。

                        (要了解该错误有多尴尬,请查看 Collections 类的 binarySearch 方法的源代码。该方法需要一个 列表参数,但二分查找对链表没有意义。然后代码笨拙 尝试发现列表是否为链表,然后切换到线性搜索!)

                        Set 接口和Map 接口设计得很好,你应该使用它们。

                        【讨论】:

                          【解决方案17】:

                          接口将定义和实现分开。因此,如果您想要供应商特定的实现,那么这是最好的方法。

                          现实生活中的例子是 JDBC 如果你看到 API Statemnet ,PreparedStatemnet 一切都是接口,但实现是特定于供应商的,比如 Oracle ojdbc jar 有一些其他的实现,mysql 有一些其他的。

                          如果您想更改数据库,您只需要更改连接类名称中的驱动程序。这就是使用接口的好处

                          【讨论】:

                            【解决方案18】:

                            属于一个类别的对象可以使用接口。只要对象之间存在 IS-A 关系。

                            例如Text,Image,MP3,FLV,MOV 属于FileType分类

                            如果我们的应用程序允许用户上传 4 种文件类型,它们都属于一个名为 FILE TYPE

                            的类别
                            1. 文字
                            2. 图片
                            3. 音频
                            4. 视频

                            在代码内部,我们将拥有上述每种类型的对象。

                            假设我们需要发送一个文件

                            带外接口     

                            带界面

                            所以我们可以假设所有文件类型(文本、音频等)都是 FileFormat 类型。所以我们形成了 IS-A 关系。

                            这有助于在返回单一数据类型而不是从函数返回特定类型时,我们可以发送通用类型,即 FileFormat。

                            【讨论】:

                              【解决方案19】:

                              接口的用途:

                              1. 定义合同
                              2. 将不相关的类与具有功能的类联系起来(例如,实现 Serializable 接口的类之间可能有也可能没有任何关系,除了实现该接口
                              3. 提供可互换的实现,例如Strategy_pattern

                              如果您正在寻找将 java 作为编程语言,interface 的功能很少,自 Java-8 版本以来添加了 defaultstatic 方法。

                              更多详情请参考这个问题:

                              Why to use Interfaces, Multiple Inheritance vs Interfaces, Benefits of Interfaces?

                              【讨论】:

                                猜你喜欢
                                • 2022-01-02
                                • 1970-01-01
                                • 2023-04-02
                                • 2011-04-15
                                • 2017-04-10
                                • 2012-03-19
                                • 2018-05-12
                                • 2018-12-11
                                • 1970-01-01
                                相关资源
                                最近更新 更多