解析IntelliJ IDEA内部设计

d433 10年前

IntelliJ IDEA第一版发布于2001年1月,这是第一款集成了高级代码导航和代码重构功能的Java IDE。

2009年,JetBrains开源了其社区版。从那时开始,就新出现了许多基于其社区版的IDE,如Google的Android Studio。

本文使用JArchitect作为工具,深入了解Intellij IDEA社区版,探索其中使用的一些内部设计决策。

1、模块化

Intellij IDEA以模块化的方式组织,使用了多个项目。最主要的项目是“idea”,“util”项目中实现了工具类,而“openapid”这个项目的jar含有开发Intellij IDEA插件所需的类型。

下面列出了Intellij IDEA的项目,以及相关类型的统计信息。

 解析IntelliJ IDEA内部设计

每个项目都以模块化的方式组织代码,使用多个包并将相同特性的包组织在一起。

基于特性划分包(package),将代码根据其特性放在不同的包里。即将所有与某个单一特性相关的内容都放在一个单个目录或包中。这样就使得所有的包都具有高聚合性和高模块化,把包与包之间的耦合性降到最低,互相依赖非常紧密的内容都放置在一起。

下面是idea项目中一些包的例子,可以看到其中的类型是根据特性分类的。

 解析IntelliJ IDEA内部设计

2、Intellij IDEA开发者广泛使用了GoF设计模式

设计模式是一个软件工程概念,描述了在软件设计中许多常见问题的解决方案。GoF模式是其中最著名的一个。

Intellij IDEA开发者使用了扩展的GOF模式,下面是一些在代码中使用的设计模式。

2.1、工厂模式

使用工厂模式可以分离逻辑并加强聚合性,下面是在代码中定义的一些工厂类。

 解析IntelliJ IDEA内部设计

在Intellij中实现了许多工厂类,下面就是一些继承自TextEditorHighlihtingPassFactory的工厂类。

图:http://www.codergears.com/Blog/wp-content/uploads/intellij2.png

2.2、适配器模式

适配器模式是两个不兼容接口之间的桥梁。这种类型的设计模式来自于结构模式,结构模式将两个互相独立的接口组合起来。

在Intellij IDEA的代码中实现了许多适配器:

 解析IntelliJ IDEA内部设计

2.3、装饰器模式

装饰器模式可以用来扩展(装饰)某个对象的功能,而且无需改变其结构。在Intellij IDEA中实现了许多装饰器。

 解析IntelliJ IDEA内部设计

2.4 代理模式

代理模式最普通的形式就是一个类,该类作为其他类型的接口。

下面是一个例子,FieldBreakpoint和FrameVariablesTree这两个类使用了两个代 理:VirtualMachineProxy和StackFrameProxy。VirtualMachineProxy接口用来替代 VirtualMachineProxyImpl这个实现。然而,与FrameVariablesTree相关的StackFrameProxyImpl 却不是这样。也许这里需要重构一下,移除互相的依赖比较好。

 解析IntelliJ IDEA内部设计

2.5、Facade(外观)模式

Facade模式隐藏了系统的复杂性,向客户端提供了一套用于访问系统的接口。下面是Intellij IDEA中实现的CodeStyle Facade。

 解析IntelliJ IDEA内部设计

2.6 访问者模式

访问者模式是用来将对象的结构本身和算法分离。

下面的代码高亮特性就是用访问者模式实现的。

 解析IntelliJ IDEA内部设计

2.7、策略模式

在许多情况下,某些类之间区别只在于他们的行为不同。这种情况下,最好的办法是将不同类中的算法与类本身分离出来,在运行时让类选择不同的算法。

在Intellij IDEA的源码中,许多类都实现了策略设计模式:

 解析IntelliJ IDEA内部设计

2.8、Builder模式

这种设计模式允许客户对象构建一个复杂的对象。Intellij IDEA的源码中,实现的ConrtolFlowBuilder就是这样一个builder。

下面列出了一组会被ControlFlowBuilder.build调用的方法:

 解析IntelliJ IDEA内部设计

3、耦合

应用程序的耦合度越低越好,这样当应用的一部分发生改变时,另一部分受到的影响就不大。从长远来看,在更改应用的需求时,低耦合可以节省大量的时间、精力和资金。

下面列出了使用接口带来的3个主要优点:

  • 接口就是定义了一套契约,用来方便重用。如果某个对象实现了一个接口,就表明这个对象遵循这个接口的标准。使用这个对象的对象成为消费者,接口就是这个对象和消费者之间的契约。
  • 接口还提供了某种层次的抽象,让程序更易理解。接口允许开发者以更一般的方式讨论代码的行为,而不是一头扎进代码的细节中。
  • 接口让组件之间的耦合更低,使得接口的消费者不会接触实现这些接口的类中的内部改动。

Intellij IDEA中定义的许多接口和抽象类都是为了降低耦合。

 解析IntelliJ IDEA内部设计

下面的Metric View中,蓝色区域就是使用了接口的代码。

 解析IntelliJ IDEA内部设计

在Metric View中,代码是使用Treemap表示的。Treemap是一种表示以嵌套矩形,按树的方式组织的数据。树结构一般用来表示代码层次。

  • 项目中含有多个包。
  • 项目中含有多个类型。
  • 类型中含有多个方法和字段。

Treemap视图可以很好的表示CQLinq查询的结果:蓝色矩形表示的查询结果,所以能以可视的方式了解查询中关注的类型。

我们可以查看所有包中定义的接口和抽象类,方便地表示包中提供的特性。

4、聚合

单一功能原则(Single responsibility principle)规定每个类都应该只有一个单一的功能,即类应该是聚合的。一般LCOM值越高,表示类聚合越低。LCOM有好几种表示方式。LCOM 表示的是0-1之间的值,而LCOM HS(HS表示的是Henderson-Sellers)表示的值范围是0-2。LCOM HS值大于1是很危险的事。下面是LCOM的计算方式:

LCOM = 1 – (sum(MF)/M*F)  LCOM HS = (M – sum(MF)/F)(M-1)

其中:

  • M是类中方法的数量(包括静态方法和实例方法,其中还包括构造器、属性设置和获取方法、添加和移除事件的方法)。
  • F是类中实例字段的数量。
  • MF是类中访问特定实例字段的方法的数量。
  • Sum(MF)是类中所有实例字段的MF的总和。

这些公式中的基本思想为:完全耦合的类中,所有方法会使用其所有的示例字段,这意味着sum(MF)=M*F,即LCOM = 0且LCOMHS = 0。

如果LCOMHS的值大于1,就要小心了。

 解析IntelliJ IDEA内部设计

只有少数类型不是聚合的。

5、多线程和并发

为了让Intellij IDEA具有更好的响应能力,其中创建了许多线程来提高用户体验。

首先,来搜索所有以直接和非直接线程开始的方法:

 解析IntelliJ IDEA内部设计

其中并发逻辑分离出来,位于下列的包中:

 解析IntelliJ IDEA内部设计

为了使用并发开发,这里使用了JSR166

下面列出了所有jsr166 jar使用的所有类型:

 解析IntelliJ IDEA内部设计

6、抽象度与不稳定性图

这幅图的思想是,程序中使用的代码元素越多,其抽象度就应该越高。换句话说,不应该过多依赖实现,而应该依赖抽象。这里的代码元素是指一个项目(也可以是包或者类型),该项目被程序中被其他项目大量使用。

在代码中,不应该大量使用具体类型。这会在程序中带来“痛苦区(Zones of Pains)”,当改变某个具体实现时,会潜在的影响程序的其他部分。而使用实现比抽象影响的范围更大。

下图中的主线(虚线)表示的是应该如何维持抽象和稳定性的平衡。稳定的组件应该位于左边。如果检查主线,可以看出,该线附近的组件抽象度都很高。如果抽象度非常低,则会位于“痛苦区”。

 解析IntelliJ IDEA内部设计

只有工具项目位于“痛苦区”,但这并不是大问题。实际上,工具库提供的是一些工具类,而不像接口提供功能。

7、Open API和插件系统

可以用插件扩展Intellij IDEA。“openapi”就是用来完成这个目标的jar。

openapi这个jar提供了许多接口,这些接口表示可以通过插件扩展的特性。

 解析IntelliJ IDEA内部设计

每个Intellij IDEA插件含有一个或多个行为(Action)。下面这个CQLinq查询结果中,显示了代码中实现的数千个行为。

 解析IntelliJ IDEA内部设计

研究已有的行为可以帮助开发者更容易开发自定义插件。

8、使用缓存提高性能

优化应用最常用的方式就是使用缓存。Intellij IDEA使用两个缓存管理器:

 解析IntelliJ IDEA内部设计

FindInProjectTask使用CacheManager接口来搜索单词。

下面列出了FindInProjectTask.getFilesForFastWordSearch方法调用的其他方法:

 解析IntelliJ IDEA内部设计

9、使用的外部库

Intellij IDEA使用了许多外部jar,下面列出了所有用到的外部jar:

 解析IntelliJ IDEA内部设计

 解析IntelliJ IDEA内部设计

当使用了外部库,最好检查一下是否能方便的使用其他第三方库而不对整个应用造成影响。在许多情况下都需要使用其他第三方库。其他第三方库会有以下特性:

  • 特性更多
  • 更强大
  • 更安全

现在来看看这些外部库的耦合度是否很高。

Swing

Swing实现了许多GUI组件,为Java应用添加了许多GUI功能和交互功能。Swing组件完全用Java语言实现。其可替换的外观和体验可以在不同的平台上拥有相同的GUI外观,也能在当前的系统上拥有本地的外观(如Windows、Solaris或Linux)。

现在来看看所有直接使用Swing组件的类型:

 解析IntelliJ IDEA内部设计

从下图可以看出,蓝色的类型都直接使用Swing组件。

 解析IntelliJ IDEA内部设计

用其他GUI框架来替代Swing很困难。虽然Swing的争议很大,但Intellij IDEA这样优秀的GUI应用证明了Swing作为GUI库也是很好的选择。

Netty

Netty是同步的时间驱动网络应用框架,用于快速开发易维护的高性能服务端、客户端协议。

下面列出了所有实用该库的类型:

 解析IntelliJ IDEA内部设计

其中只有少数是直接使用了该库,这样如果想使用其他库替换也非常容易。

ASM

ASM是一个非常小的Java字节码操作框架。这个框架非常有名,许多工具都在使用。在JArchitect中也使用它来分析字节码。

下面列出了所有直接使用ASM的类型:

 解析IntelliJ IDEA内部设计

如Netty一样,对ASM的使用也仅限于几个包中,这样就可以方便的用其他库替换。

除了Swing,其他库都没有在Intellij IDEA中紧耦合。

10、统计

10.1、使用最多的类型

了解项目中那些类型使用的最多是一件非常有趣的事。这些类型必须在设计上、实现上都非常优秀,而且经过完善的测试。这些类型中的一丁点改动都会影响整个项目。

可以使用TypesUsingMe衡量标准来找到这些类型:

 解析IntelliJ IDEA内部设计

然而,还有另一种有趣的衡量标准类搜索常用类型:TypeRank。

TypeRank值是使用Google PageRank算法在图上计算类型的依赖关系。使用的位似中心(homothety of center)为0.15,所以平均TypeRank为1。

TypeRank值较高的类型应该仔细测试,因为这些类型中的bug会导致更大的问题。

下面是使用TypeRank衡量的所有常用类型:

 解析IntelliJ IDEA内部设计

在这种衡量方式中,PsiElement取代了Project接口,成为最常用的类型。

10.2、使用最多的方法:

 解析IntelliJ IDEA内部设计

10.3、过多调用其他方法的方法

了解某个方法调用了多少个其他方法也很有趣,如果一个方法过多调用其他方法可能会有设计问题。在某些情况下需要对这些方法进行重构,以提高可读性和维护性。

 解析IntelliJ IDEA内部设计

总结

Intellij IDEA的设计和实现都非常好,其中使用了许多设计模式,并提供了许多优秀的实践经验。探索其中的代码是一条实践之路,从中可以学习如何设计和实现你的应用。这比只读书和文档来提高设计技能要好得多。

原文链接: codergears 翻译: ImportNew.com - 孙 波翔
译文链接: http://www.importnew.com/14260.html