对于TypeScript的接口和类编码指南感到困惑

149

我阅读了TypeScript编码准则

但我发现这个声明有些令人困惑:

  

不要在接口名称前使用“I”作为前缀

我的意思是,没有“I”前缀,类似这样的东西就没有太多意义。

class Engine implements IEngine

我是否错过了什么显而易见的东西?

还有一件事情我不是很明白,就是这个:

为了保持一致性,在核心编译器管道中不要使用类,而应该使用函数闭包。

这是说我根本不应该使用类吗?

希望有人能为我澄清一下 :)


22
澄清一下,这是有关TypeScript代码风格的文档,而不是有关如何实现项目的风格指南。如果使用“I”前缀对您和团队有意义,请使用它。如果不是,也许像Java风格中的“SomeThing”(接口)与“SomeThingImpl”(实现)那样使用。 - Brocco
4
编码规范之美在于它们只是“指南”,你可以选择遵循或不遵循。个人而言,我使用"I"表示接口,因为我习惯这么做,并且对我的团队成员来说也很合理。 - Jon Preece
2
是的,它们只是一些指导方针。但大多数情况下,这些指导方针背后都有充分的理由。所以仅仅坚持自己习惯的做法并不能让你作为一个程序员得到进步 :) - Snæbjørn
6
当我的团队开始使用TS时,我们也为接口使用I前缀。我猜这是受C#的影响。然后我阅读了TypeScript团队关于不使用I前缀的指导方针,花了我几周时间才意识到这一点。我急切地寻找信息,想知道他们为什么有这样的规定。后来我意识到:在TypeScript中,为接口使用I前缀是一个缺陷,它带来的问题比提供的好处更多。 - Stanislav Berkov
1
在VSCode中,如果你输入 let engine = new E...,只会自动建议Classes而不是Interfaces。我之前没有意识到这一点 - 这就是我以'I'为前缀的原因之一。 - Drenai
2
@StanislavBerkov 抱歉,我知道这已经很老了,但能否详细说明一下?特别是在这个方面:过了一段时间后,我意识到:接口的I前缀是一个缺陷(至少在TypeScript中),它会带来更多问题而不是好处。 - matronator
9个回答

301
当一个团队/公司发布框架/编译器/工具集时,他们已经有了一些经验和最佳实践。他们将其作为指南分享。指南是建议。如果您不喜欢任何建议,可以忽略它们。编译器仍然会编译您的代码。尽管如此,入乡随俗
这就是我认为TypeScript团队为什么推荐不使用I前缀的接口的原因之一。

原因 #1:匈牙利命名法的时代已经过去了

主张使用以 I 作为接口前缀的支持者认为,这种前缀有助于立即理解类型是否为接口。声称前缀有助于立即理解类型的说法是对 匈牙利命名法 的呼应。用 I 作为接口名称前缀,C 作为类前缀,A 作为抽象类前缀,s 作为字符串变量前缀,c 作为常量变量前缀,i 作为整数变量前缀。我认为这种命名修饰可以在不用将鼠标悬停在标识符上或通过热键导航到类型定义的情况下提供给您类型信息。然而,这种微小的好处被 匈牙利命名法 的缺点和下面提到的其他原因所抵消。匈牙利命名法在当代框架中不再使用。C# 由于历史原因(COM)使用 I 前缀(也是 C# 中唯一的前缀)表示接口。回想起来,.NET 架构师之一(Brad Abrams)认为 不使用 I 前缀 更好。TypeScript 不受 COM 遗留问题的影响,因此没有 I 前缀作为接口规则。

原因 #2 I 前缀违反了封装原则

假设你得到了一个黑盒子。你获得了一些类型引用,允许你与该盒子进行交互。你不应该关心它是接口还是类。你只使用其接口部分。要求知道它是什么(接口、特定实现或抽象类)是违反封装的。

例如:假设你需要在代码中修复 API Design Myth: Interface as Contract ,例如删除 ICar 接口并改用 Car 基类。然后你需要在所有消费者中执行这样的替换。 I 前缀导致消费者对黑盒实现细节的隐式依赖。

原因 #3 防止错误命名

开发人员往往懒得认真考虑名称。命名是计算机科学中的两件难事之一。当开发人员需要提取接口时,只需在类名前添加字母I即可获得接口名称。禁止在接口上使用I前缀,迫使开发人员费尽心思选择适当的接口名称。所选名称不仅应在前缀上不同,而且还应强调意图差异。
抽象情况:您不应定义ICar接口和相关的Car类。 Car是一个抽象概念,应该用于契约。实现应具有描述性、独特的名称,例如SportsCar、SuvCar、HollowCar
好的例子:WpfeServerAutosuggestManager实现AutosuggestManagerFileBasedAutosuggestManager实现AutosuggestManager
坏的例子:AutosuggestManager实现IAutosuggestManager

第四个原因:恰当选择命名可以让你免受API设计神话:接口即契约的影响。

在我的实践中,我遇到了很多人,他们无思考地复制了类的接口部分到一个独立的接口中,采用 Car implements ICar 命名方案。将类的接口部分复制到独立的接口类型中,并不会自动将其转换为抽象。您仍会得到具体的实现,但也会带来重复的接口部分。如果您的抽象不够好,重复的接口部分不会任何改进。提取抽象是艰苦的工作。

注:在TS中,您不需要单独的接口来模拟类或重载功能。您可以使用TypeScript utility types而不是创建描述类的公共成员的单独接口。例如,Required<T>构造一个由类型T的所有公共成员组成的类型。

export class SecurityPrincipalStub implements Required<SecurityPrincipal> {
  public isFeatureEnabled(entitlement: Entitlement): boolean {
      return true;
  }
  public isWidgetEnabled(kind: string): boolean {
      return true;
  }

  public areAdminToolsEnabled(): boolean {
      return true;
  }
}

如果您想构建一个类型,排除某些公共成员,则可以使用省略和排除的组合

7
我喜欢你提到的为具体类命名比接口更加具体化的观点,但我认为在类型名称前加上“ I” 前缀有助于立即了解这个类型是否有实现。使用接口的主要优势是将外部代码的实现细节与特定实现解耦。 - cchamberlain
3
虽然你的观点在理论上听起来不错,但在足够大的团队中,“I”前缀通常会被替换为“Impl”后缀。 - jameslk
6
我知道在计算机科学中,不使用"I"和"Impl"来命名东西很难(“在计算机科学中,有两件难事:缓存失效和命名事物”——Phil Karlton),但这是可以做到的。 - Stanislav Berkov
6
我同意移除"I"通常会被替换成在实现上添加前缀或后缀,比如DefaultFooService或FoodServiceImpl。通过前缀,你可以避免在合同和实现/抽象之间出现重叠的命名问题。重复的观点也是正确的,但接口的优点在于有合同,能够用其他东西替代实现。通常这在单元测试中非常关键...更不用提抽象和不可变性等好处了...我完全支持使用I前缀的接口。 - karczilla
3
命名很难,而困难的正是不应该受到任意限制和禁令的恶意增加,这些限制和禁令是由于墨守成规的哲学所形成的。提出匈牙利命名法的幽灵是一种谬论:没有人会开始写“lpcstr”。拥有一个界面和一个参考实现,在大多数情况下都会被使用,这是一种常见的模式,不应该被去优化。 - Szczepan Hołyszewski
显示剩余13条评论

59

关于您引用的链接的澄清:

这是有关TypeScript代码样式的文档,而不是实现项目的样式指南。

如果使用前缀对您和您的团队有意义,请使用它(我也是这么做的)。

如果不行,也可以使用Java风格的(接口),然后使用(实现),这样做也完全可以。


31
仅作澄清。Typescript手册仍然表示:“通常不要在接口名称前加上I(例如IColor)。因为在TypeScript中,接口的概念比在C#或Java中更广泛,IFoo命名约定并不十分有用。”虽然这不是强制规定,但它看起来像是一条强烈推荐的建议。 - Guria
4
同意。这似乎是来自微软针对所有基于 TypeScript 的项目的代码指南,而不仅仅是 TypeScript 本身。我们发现这个指南非常令人困惑,仍然在所有接口名称前加上"I"。 - demisx
4
有一个场景,我认为保留 I 是有意义的。在一个 React 项目中,有一个名为 Card 的接口和一个名为 Card 的组件。在 props 中,我需要一个 Card 数组,但类型不是组件。我必须选择命名为 ICard 还是 CardComponent... - BrunoLM
5
SomeThingImpl 实现 SomeThingSomeThing 实现 ISomeThing 一样好。如果 SomeThing 是抽象的话,命名应该是 Concrete[Some]Thing 实现 SomeThing - Stanislav Berkov
正如 @Guria 所说,我认为 TypeScript 手册已经涵盖了一切,我们只需要注意示例,就能很好地使用它。 - giovannipds
即使懒惰召唤,TImpl implements T 仍然比 X implements IX 更好,因为当我们懒惰时,我们谈论抽象而不知道所有实现细节及其后果,尽管我们认为我们正在谈论具体实现。ConcreteT implements T 稍微差一点,因为 ConcreteTTImpl 更长。为了最大限度地简洁,只需使用 TI implements T,因为实现也以字母 I 开头。 - Yufan Lou

6

我认为@stanislav-berkov对于提问者的问题给出了相当不错的答案。我只想分享我的两分钱,最终达成共识并设立自己的规则/指南是由你的团队/部门/公司/等等来决定的。

尽可能和希望一致地坚持标准和约定是一个好习惯,也使事情更易于理解。另一方面,我认为我们仍然可以自由选择编写代码的方式。

在情感方面思考一下,我们编写代码的方式或我们的编码风格反映了我们的个性,在某些情况下甚至反映了我们的情绪。这就是让我们成为人类而不仅仅是遵循规则的编码机器的原因。我认为编码可以是一种工艺而不仅仅是工业化过程。


3

Typescript文档中建议的指南并不是给使用Typescript的人,而是给那些为Typescript项目做出贡献的人。如果您在页面开头阅读详细信息,它清楚地定义了谁应该使用该指南。这里是指南的链接。 Typescript guidelines

总之,作为开发人员,您可以根据自己的喜好命名接口。


2
我喜欢添加一个“Props”后缀。
interface FormProps {
 some: string;
}

const Form:VFC<FormProps> = (props) => {
  ...
}


2

我个人非常喜欢通过添加-able后缀将名词变成形容词的想法。听起来很不正式,但我很喜欢!

interface Walletable {
  inPocket:boolean
  cash:number
}


export class Wallet implements Walletable {
  //...
}

}


19
但是,一个钱包本身并不能被钱包化——你放入钱包中的物品才能被钱包化。 - jKlaus
钱包类应该可以更好。 - Adam
如何将接口称为“钱包(Wallet)”,其实现分别称为“皮夹(LeatherWallet)”,“RFID钱包(RFIDWallet)”和“支票簿(Checkbook)”? - undefined

1

我正在尝试使用类似于其他答案的模式,但是将实例化具体类作为接口类型导出函数,如下所示:

export interface Engine {
  rpm: number;
}

class EngineImpl implements Engine {
  constructor() {
    this.rpm = 0;
  }
}

export const createEngine = (): Engine => new EngineImpl();

在这种情况下,具体实现永远不会被导出。

0

类型作为接口是实现细节。实现细节应该在API中隐藏。这就是为什么你应该避免使用 I

你应该避免使用前缀后缀。以下两种都是错误的:

  • ICar
  • CarInterface

你应该做的是在API中显示一个漂亮的名称,并将实现细节隐藏在实现中。这就是为什么我建议:

  • Car - 在API中公开的接口。
  • CarImpl - 该API的实现,对消费者隐藏。

目前我也使用了这个结构,这是从我的SpringBoot背景中建立的。那么在TS中这被认为是正确的吗?在TS社区中似乎有很多关于什么是正确的意见,但没有明确的答案。 - Impurity
2
使用 Impl 后缀会产生副作用,即用 Uncle Bob 替换那些因为 I 前缀而责骂你的自以为是的人。 - Szczepan Hołyszewski
3
避免在接口中使用后缀,然后在实现中到处添加它,这样做有意义吗?如果您决定为该类删除接口,那么会保留“Impl”吗?还是也会将其删除? - Konrad Gałęzowski
1
如果我决定删除接口,我将使用Car名称作为类名。这是一个很好的例子,说明当您不公开ICar和CarInterface等实现细节时,可以做些什么。 - Tomas Bjerre
好的,所以你有一个类型为 CartItem 的东西和一个 CartItem 组件,它需要一个 CartItem 作为 props。你在这里建议的不可能,你需要一些后缀或前缀。 - Ini

0
如果您想要实现一个干净的架构,在某些情况下,当然是在我看来,使用I前缀确实是有意义的...
例如,假设您希望所有业务对象都可以进行审计(即具有createdBy、createdAt等字段)。
您可能最终会在您的域包中得到以下内容:
export interface MyInterface {
     createdDate : Date;
     ....
}

export class AuditableModel implements MyInterface{
     createdDate : Date;
     ....
}

在你的基础设施包中,你可能会有:

export class AuditableDbEntity implements MyInterface {
    @Property()
    createdDate: Date = new Date();
    ...
}

那么,MyInterface 应该命名为什么呢? 在我看来,IAuditable 是一个不错的名称。我很难找到一个更好理解的名称。


这与在您的域中定义服务的情况不同:

export interface Logger {
    log(msg:string)
}

在您的基础设施中,需要一个具体的实现,该实现将被注入到域中:

export class ConsoleLogger implements Logger {
      log(msg:string){
           console.log(msg)
      }
}

在这里,接口被直接命名为“Logger”迫使实现者为实现找到一个更好的名称(例如ConsoleLogger而不仅仅是Logger)。
最后,我在各处都使用了I前缀以保持一致性。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接