前言
首先必须声明,这不是一篇广告或者标题党。 而是我开源了一个工具,可以优雅的为Java or Kotlin 项目生成 Class Diagram
。
我推测列为读者会进来阅读,原因无非以下两点:
- 获得一个生成类图的工具,并通过文章快速了解是否
方便
且好用
- 了解一下我是如何折腾的
仅关心如何使用的,可以移步 使用示例
我们将按照下面的脑图顺时针展开,揭开这一工具的诞生过程
主要问题与方案
背景
背景:笔者今年换了份工作,所在的公司属于
医疗器械
下的细分领域,而相比于纯互联网行业
领域,医疗器械
领域所属的配套软件,
都有明确的文档要求,并非可有可无
,而且公司管理层比较重视细节(核心产品为颅内、体内植入的医疗器械,确实需要非常认真仔细)。
毋庸置疑,准确的
、关键的
算法流程图,时序图,组件图,状态图,类图等, 对于产品本身的维护及发展具有很大帮助!
对于研发工作者而言,高度概括流程、设计、算法等的专业工具图对工作有极大帮助。既然需要审核的文档中也需要这些内容,又对工作有帮助,何不做的好一些呢。
上文提到的各类UML图中, 类图 Class-Diagram
是非常特殊的, 它表述的是 类之间的关系
,基于源码文件分析可以得出准确的结果。
而 流程图
、时序图
、状态图
、 组件图
等则不行。
问题
随着行业的发展,软件开发也演变为 以迭代的方式,依次实现最重要的功能,持续性交付
,顺其自然的,我们已经不再像几十年前的前辈们那样:代码未动,文档与UML图先行。
一般概要设计后,方案可行便进行编码了。
根据我的实际情况,复杂的功能一般在草稿纸上画画草图,简单的就脑子里想想,难以留下存档
在这一工作模式下,笔者也遇到了一些问题:
- 业务迭代后或者代码改进后,文档(uml图)未及时更新
- 手动维护耗时耗力
如果这件事情可以交给机器来做,那显然是极好的!而让机器维护类图是最容易实现的!
综上所述:我们需要一款工具或者插件,可以直接基于源码生成类图 (或者中间产物,例如:plant-uml文件) ,能够配合其他工具链,直接进行归档。
当然,最重要是免费, 这省去了说服公司进行购买
留有类图的好处:
- 方便向他人介绍业务和代码
- 项目庞大或者复杂时,更容易找到需求对应的关注点,重新维护时日久远的业务时,状态来的快
- 图比代码亲切而且保护隐私🤣
解决方案
众所周知,Intellij-Idea的官方插件可以分析出类图,但是Idea是收费软件,付费支持官方插件是一个省时省力的方案,这是个兜底方案。最终没辙时,我们再考虑它。
编码时分析
仿照官方插件的思路,基于源码文档树进行分析,在Intellij的支持下,基于 PSI和uast 即可分析出类之间的关系。这需要一定的PSI、uast知识基础。
编译时分析
在整个编译环节中,有一些切面应对特定问题,例如:“注解处理” 、 “Gradle Transformer” ,在此切面处,我们可以基于编译中间产物,间接分析出类之间的关系。
最简单的是 注解处理阶段 介入,这只需要对 Element 和 TypeMirror 有一定的知识基础即可。
运行时反射分析
显然这不是一个太好的切入点,直接pass。
考虑到PSI方面的知识体系掌握地不太完善,Intellij跨越大版本时,会有较大变更,
而注解处理方面的知识还过的了关,搞个类图生成问题不大。PS: AndroidStudio 基于Intellij核心二次开发,PSI插件跟随Idea大版本进行适配;
所以最终方案为:从注解处理阶段入手,分析编译中间产物,最终生成类图
问题分治与解决
分治1 – 简化输出产物
确定了大方向之后,我们需要再思考下整个问题的方方面面。生成类图有两大问题需要解决:
- 从
源码、或者编译的中间产物中分析出类关系; ps:我们已经确定了要从编译中间产物出发 - 将类关系转变为图
显然,“开发一个用来生成图的引擎”,这件事情成本过大且没有必要。所幸的是,UML不是一个新生物,业内也有大名鼎鼎的PlantUml。
PlantUml 基于 Graphviz , Graphviz 本身使用Dot语法描述元素与元素关系,
直接使用 Graphviz 比较朴素,PlantUml通过自定义语法,使得内容可阅读性提升,且无须关注转换图片时进行各类装饰问题
于是,我们可以将问题转化为:从编译的中间产物中分析出类关系,将关系按照PlantUml语法生成puml文件,它的内容是纯文本。
分治2 – 确定分析的起始点
如果从最终结果看,我们得到的是一个 有方向的图
,那么 按照图本身的起始点出发 比较符合习惯。
也就是说,我们将在起始点所对应的类上添加注解,作为注解处理的目标起始点
例如:
Cat 和 Dog 将作为起始点。
因为只需要标记类,我们约定注解:
@Target(AnnotationTarget.CLASS)
annotation class GenerateClassDiagram {
}
在代码上,将表现为:
class Animal
@GenerateClassDiagram
class Dog : Animal()
@GenerateClassDiagram
class Cat : Animal()
在示例中,当我们处理 GenerateClassDiagram
时,可以扫描获得 Cat
以及 Dog
类对应的
javax.lang.model.element.Element
示例,下文简称 Element
几点可能存在的疑惑:
- 为何不 “双向” 分析:继承和实现关系,双向分析会带来额外的复杂度,且在使用上规则不清晰,依赖关系难以双向分析。 但是,如果使用规则上可以做到清晰明了,这一点值得实现
- 为何不标注在Animal上,进行反向分析: 如果高层级的类在库包中,则需要修改库包,这不利于日常管理与维护
- 如果只标注了Cat而没有标注Dog,Dog将不会体现在图中?:是的
- 如果全部标注了,是否产生不良影响 :不会,但是没有必要
分治3 – 确定关系的分析方法
继承&实现
因为注解的标记对象是类 或者接口,我们理应得到 TypeElement
,基于 Element
的访问者模式实现,这一点并不难。
public interface TypeElement extends Element, Parameterizable, QualifiedNameable {
TypeMirror getSuperclass();
List<? extends TypeMirror> getInterfaces();
//其他无关代码略去
}
不言自明,我们可以通过 TypeMirror getSuperclass();
得到继承关系,通过 List<? extends TypeMirror> getInterfaces();
得到实现关系
注意,此处可以细分,接口和枚举仅需要分析实现关系即可,通过
Element#getKind():ElementKind
可以判断类型
依赖&关联&聚合&组合
这四个关系非常的类似但又不同,先降低复杂度,均认为是依赖关系,在后续迭代中,可以进一步增加功能,将关系细化
进一步降低复杂度,我们仅从类的属性出发,分析依赖关系,忽略掉 方法声明 (可分析)、方法体 (无法分析) 、静态块 (无法分析) 中 所包含的关系。
public interface TypeElement extends Element, Parameterizable, QualifiedNameable {
List<? extends Element> getEnclosedElements();