一般来说,在面向对象编程中,接口是一个描述一组方法签名的实体,这些方法签名的实现可以由多个本不一定彼此关联的类提供。因此,接口是一个契约,必须被实现它的类遵守。
接口允许你实现多重继承,并将抽象概念应用于软件类和模块的设计中。
但现在让我们来谈谈LabVIEW。在LabVIEW领域,接口是一个相对较新的概念。事实上,这些是从2020年版本开始的。
在LabVIEW中,接口可以被视为一个没有数据的类,即没有私有数据集群。接口声明对象可以扮演的角色,但不定义如何执行该角色。该角色的声明是通过在接口内部定义一组空动态方法来实现的。
继承自接口时,类声明其对象扮演该角色,并负责指定该角色的实现方式。该类必须覆盖其实现接口中定义的所有动态方法。
在LabVIEW中,类还可以继承多个接口,从而实现多重继承。当一个类继承多个接口时,该类必须实现它实现的所有接口的所有方法。
抽象类与接口
首先,让我们定义一下抽象类的含义。一般来说,抽象类是指不能实例化的类,因此必须通过派生类进行扩展。抽象类定义了所有或部分也是抽象(空)的方法和属性。
鉴于LabVIEW中不存在抽象(纯)类,我们可以大致将抽象类视为基类(超类),其动态方法和属性为空,动态方法则用“后代必须覆盖”选项标记。我说的是抽象类,而非基类或超类,仅仅是为了获得更理论化的理解。注意,本子章中的所有考虑也可以应用于混合类(即非纯抽象)基类。
LabVIEW |要求派生类覆盖的选项。
话虽如此,让我们试着理解抽象类和接口之间的类比和差异。
两者都代表一种契约,派生类必须通过实现基类型的方法(即其扩展的类或实现的接口)来遵守。然而,抽象类因代表衍生类的基类型,定义了与派生类更强的联系。而接口则定义了一个通用模型(角色),代表实现这些接口的类的共同行为。这些类可以有多种类型和性质。举例来说,我们可以考虑可序列化接口的Serialize方法。该接口代表实现它的类能够以字节序列形式序列化自身的能力。该接口可以由完全无关的类实现:例如,建模数字万用表的DMM类、包含生成测试序列报告设置的TestReportSettings类,以及包含用户数据的用户类。
那么什么时候用抽象类,什么时候用接口?
我唯一想给出的答案是…这取决于具体情况,也取决于开发者的意图。事实上,虽然两者有相似之处,但从软件设计的角度来看,它们的作用非常不同。抽象类定义了一种带有衍生类的垂直契约,其使用限制了使用该类的软件组件的抽象层次。这是因为使用抽象类会对类的层级施加限制。而接口则更像是与实现它的对象之间的水平契约。这种契约比抽象类更通用。然而,正因如此,接口比抽象类创造了更强的抽象层次,允许各种软件模块拥有更多的通用依赖关系。因此,如果我们想选择更抽象、通用且可扩展的方法,我们会选择接口。 相反,如果我们希望对类层级有更强的约束(可能会失去抽象),我们会选择抽象类。
我们可以用一个通用的经验法则来决定使用抽象类还是接口类:如果“A 是 B ”成立,则使用抽象类和继承。如果需要“A 能够做到…”这样的陈述,就使用接口。例如,我们可以说一个矩形是一个多边形,但说一个矩形能够成为多边形是没有意义的。无论如何,我相信常识永远是战胜一切规则的原则。有时候界面更合适,即使上面的规则说了不一样。
接口命名的最佳实践
NI公司提供了一些接口命名指南:
- 使用描述物体能力的形容词或副词。例如,如果接口代表能够测量电压的硬件,那么将该接口命名为 CanMeasureVoltage.lvclass。任何继承该接口的类或接口都能测量电压。另一个例子是,如果一个接口代表了实现该接口能够序列化为字节序列(如二进制、XML 或 JSON 序列化)的能力,则称该接口为可序列化。
- 当你不能使用形容词或副词时,可以使用描述它们从接口继承的类别的名词。例如,如果接口描述了一类类,这些类负责将数据保存到不同数据库,则将该接口命名为Database.lvclass。
- 避免使用首字母“I”。虽然大多数基于文本的编程语言用大写字母“I”来区分接口和类,但LabVIEW通过字形区分接口和类。
使用接口的好处
接口的使用带来了多项好处:
- 接口允许你定义一种行为,由一组无关的类实现,而不必强迫它们共享共同的类层级结构。
- 通过实现接口,类可以扮演其类层级之外的角色。
- 由于一个类不受其实现接口数量的限制,它可以参与多种角色。
- 类可以通过其接口暴露,通过强制所有通信通过其公共接口方法来隐藏其实现细节。
- 你可以轻松修改(或者说扩展)现有系统,使其在当前类层级结构中提供新功能
