preface

First of all, this is not an AD or clickbait. Instead, I open source a tool that gracefully generates Class Diagrams for Java or Kotlin projects.

I suspect readers will come in for two reasons:

  • Get a tool to generate class diagrams and read the article quickly to see ifconvenientBe useful
  • See how I did it

If you only care about how to use it, you can use the example step by step

We will unfold the process of this tool clockwise, following the brain diagram below

Major issues and programs

background

Background: I changed jobs this year, under the company belongs to the medical apparatus and instruments of niche, but compared to the pure Internet industry field, belongs in the field of medical devices supporting software, all have clear documentation requirements, is not a dispensable, and company management is pay attention to details (core products for intracranial, medical devices implanted in the body, Very careful indeed).

Undoubtedly, accurate and key algorithm flow chart, sequence diagram, component diagram, state diagram, class diagram, etc. are of great help to the maintenance and development of the product itself!

For r & D workers, professional tool diagrams that highly outline processes, designs, algorithms, etc., are of great help. Since the documents that need to be reviewed also need this content, and it will help the work, why not do better.

Among the UML diagrams mentioned above, the class-diagram is a special one. It describes the relationships between classes, which can be accurately determined based on source code analysis. Flowcharts, sequence diagrams, state diagrams, component diagrams, and so on do not.

The problem

As the industry has evolved, software development has evolved to implement the most important functions in sequence in an iterative fashion, with continuous delivery and, naturally, we are no longer like our predecessors of decades ago: code remains untouched, documentation and UML diagrams come first. After the general outline design, the scheme is coded as feasible.

According to my actual situation, complex functions are generally sketched on draft paper, while simple ones are thought about in my mind, which is difficult to keep for archiving

Under this working mode, the author also encountered some problems:

  • Documentation (UML diagrams) is not updated in time after business iterations or code improvements
  • Manual maintenance takes time and effort

If the task could be left to a machine, it would obviously be great! Having the machine maintain the class diagram is the easiest thing to do!

To sum up: we need a tool or plug-in that can generate class diagrams (or intermediates, such as plant-UML files) directly from source code that can be archived with other toolchains.

Of course, the most important thing is to be free, which saves persuading companies to buy

Benefits of class diagram:

  • Easy to introduce business and code to others
  • When the project is large or complex, it is easier to focus on the requirements, and the state comes faster when the old business is re-maintained
  • Tubby code is friendly and private 🤣

The solution

As we all know, the official plug-in of Intellij-IDEA can analyze the class diagram. However, Idea is a paid software, and paying to support the official plug-in is a time-saving and labor-saving solution, which is a bottom-of-the-line solution. We’ll think about it when we finally run out of ideas.

Coding-time analysis

Following the thinking of official plug-ins, the analysis is based on the source document tree. With the support of Intellij, the relationship between classes can be analyzed based on PSI and UAST. This requires some knowledge of PSI and UAST.

Compile time analysis

Throughout the compilation process, there are sections that address specific problems, such as “annotation processing” and “Gradle Transformer”, where we can indirectly analyze the relationships between classes based on the compilation intermediates.

The simplest is annotation processing phase intervention, which requires only some basic knowledge of Element and TypeMirror.

Run-time reflection analysis

Obviously this is not a very good entry point, just pass.


Considering that the knowledge system of PSI is not very perfect, Intellij will have a big change when it goes over the big version, and the knowledge of annotation processing is still passed, so it is not a problem to generate a class diagram.

PS: AndroidStudio is based on the secondary development of Intellij core, and the PSI plug-in is adapted with the big version of Idea;

Therefore, the final solution is: start from the annotation processing stage, analyze and compile the intermediate products, and finally generate the class diagram


Divide and conquer and solve problems

Divide and conquer 1 — Simplify the output

With the big picture set, we need to think about the whole issue again. Generating class diagrams has two major problems to solve:

  • fromThe source codeOr analyze class relationships in compiled intermediates;Ps: We’ve decided to start by compiling the intermediates
  • Turn class relationships into diagrams

Obviously, “developing an engine for generating graphs” is too expensive and unnecessary. Fortunately, UML is not a new creature, and there is a well-known PlantUml in the industry.

PlantUml is based on Graphviz. Graphviz itself uses Dot grammar to describe the relationship between elements and elements. It is simpler to use Graphviz directly

Thus, we can turn the problem into: analyze the class relations from the compiled intermediates, and generate puml files with plain text content from the relations according to PlantUml syntax.

Divide and conquer 2 — Identify the starting point for analysis

If the end result is a directed graph, it is customary to start with the starting point of the graph itself.

That is, we will add annotations to the class corresponding to the start point as the target start point for annotation processing

Such as:

Cat and Dog will be the starting points.

Since only the tag class is needed, we agree on annotations:

@Target(AnnotationTarget.CLASS)
annotation class GenerateClassDiagram {}
Copy the code

On the code, this will appear as:

class Animal

@GenerateClassDiagram
class Dog : Animal(a)@GenerateClassDiagram
class Cat : Animal(a)Copy the code

In the example, when we deal with GenerateClassDiagram, can obtain the Cat and Dog class corresponding javax.mail. Lang. Model. The element. The element example, hereinafter referred to as “element

A few possible questions:

  • Why not “two-way” analysis:Bidirectional analysis of inheritance and implementation relationships will bring additional complexity, and the rules in use are not clear, and it is difficult to bidirectional analysis of dependency relationships.However, if there is clarity in the use of rules, it is worth achieving
  • Why not tag it on Animal and do a reverse analysis:If the higher-level classes are in the library package, you need to modify the library package, which is not conducive to daily management and maintenance
  • If YOU label Cat and you don’t label Dog, then Dog won’t be in the picture, right? :Yes.
  • If all are marked, will there be any adverse effects?No, but there’s no need

Divide-and-conquer 3 — Analytical method for determining relationships

Inheritance & Implementation

Since the markup object of the annotation is a class or interface, we should expect TypeElement, an Element-based visitor pattern implementation, which is not difficult.

public interface TypeElement extends Element.Parameterizable.QualifiedNameable {

    TypeMirror getSuperclass(a);

    List<? extends TypeMirror> getInterfaces();

    // Omit other irrelevant code
}
Copy the code

Self-explanatory, we can use TypeMirror getSuperclass(); To get the inheritance relationship, use List
getInterfaces(); Get the realization relation

Note that this can be subdivided; interfaces and enumerations only need to analyze the implementation relationship; Element#getKind():ElementKind determines the type

Dependency & association & Aggregation & composition

These four relationships are very similar but different. Firstly, reduce the complexity and consider them as dependencies. In subsequent iterations, functions can be further added to refine the relationship

To further reduce complexity, we analyze the dependencies only from the attributes of the class, ignoring the relationships contained in the method declaration (which can be analyzed), method body (which cannot be analyzed), and static block (which cannot be analyzed).

public interface TypeElement extends Element.Parameterizable.QualifiedNameable {

    List<? extends Element> getEnclosedElements();
    // omit irrelevant code
}
Copy the code

It goes without saying that through this API, elementFile #fieldsIn(Iterable
):List

can get declared fields;

Element’s API makes naming and decorating easy;

After converting it to TypeMirror via Element#asType():TypeMirror API, We can get the field type, DeclaredType, based on its Visitor pattern design and retrieve the Element via the DeclaredType#asElement():Element API

Divide and conquer 4 — Determine the end point of the analysis

In Divide and Conquer 2, we have identified the starting point for analysis (there may be more than one), and in Divide and Conquer 3, we have identified how relationships are analyzed. For easy expression, we use:

Relation(From,End) describes the Relation From From to EndCopy the code

Perform a divide-and-conquer 2& divide-and-conquer 3, and we will get a series of Relation(From,End). At this time, we will take all End as the new From, and iterate this process, and then we can complete the graph traversal!

What about the appropriate end to the process?

We only need to maintain a set of Sfrom to store the From in the iteration process. Each End we get is a new From only if it meets the condition “does not exist in Sfrom”. When we cannot get a new From, the iteration ends

Divide and conquer 5 – a complement to divide and conquer 3, dealing with collections, arrays, generics

Following the convention in Divide-and-conquer 3, we consider the types involved in collections, arrays, and generics to be dependent on the current type. Although this is not rigorous

Thanks to TypeMirror’s Visitor pattern implementation, it’s easy to write the following code to get what we care about!

private abstract class CastingTypeVisitor<T> constructor(private val label: String) :
    SimpleTypeVisitor6<T, Void? > () {override fun defaultAction(e: TypeMirror, v: Void?).: T {
        throw IllegalArgumentException("$e does not represent a $label")}}private class FetchClassTypeVisitor : CastingTypeVisitor<List<DeclaredType>>(label = "") {
    override fun defaultAction(e: TypeMirror, v: Void?).: List<DeclaredType> {
        //ignore it
        return emptyList()
    }

    override fun visitArray(t: ArrayType, p: Void?).: List<DeclaredType> {
        return t.componentType.accept(this, p)
    }

    override fun visitWildcard(t: WildcardType, p: Void?).: List<DeclaredType> {
        valret = arrayListOf<DeclaredType>() t.superBound? .let { ret.addAll(it.accept(this, p)) } t.extendsBound? .let { ret.addAll(it.accept(this, p))
        }
        return ret
    }

    override fun visitDeclared(t: DeclaredType, p: Void?).: List<DeclaredType> {
        valret = arrayListOf(t) t.typeArguments? .forEach { ret.addAll(it.accept(this, p))
        }
        return ret.toSet().toList()
    }

    override fun visitError(t: ErrorType, p: Void?).: List<DeclaredType> {
        return visitDeclared(t, p)
    }

    override fun visitTypeVariable(t: TypeVariable, p: Void?).: List<DeclaredType> {
        valret = arrayListOf<DeclaredType>() t.lowerBound? .let { ret.addAll(it.accept(this, p)) } t.upperBound? .let { ret.addAll(it.accept(this, p))
        }
        return ret
    }
}

fun TypeMirror.fetchDeclaredType(a): List<DeclaredType> {
    return this.accept(FetchClassTypeVisitor(), null)}Copy the code

Divide and conquer 6 – Storage of relationships

Obviously, we need a proper data structure to store graphs, thanks to some of my exploration in componentization: Sequential initialization of components last year, when I developed Maat which contains a directed acyclic graph analysis of component dependencies, including an implementation of DAG.

Obviously, by disabling “acyclic detection”, we can use the data structure directly, without making the wheel!

Obviously, various situations of Relation can establish a mapping relationship with degree, and artificially maintaining a virtual vertex as the starting point of traversal can reduce a lot of trouble.

Divide and conquer 7 — Type details

In Divide and Conquer 3, we did a good job of analyzing types (enum, class, interface), but we left out some details, such as methods, modifiers, and so on.

In Divide and Conquer 6, we identified the relational storage scheme, and we also needed to describe the vertices of the graph.

We define the UmlElement class to describe it

abstract class UmlElement(valdiagram: ClassDiagram? .val element: Element?) {
    /** * return: the corresponding text in plant-UML ** /
    abstract fun umlElement(context: MutableSet<UmlElement>): String
    
    abstract fun parseFieldAndMethod(diagram: ClassDiagram,
                                     graph: DAG<UmlElement>,
                                     cache: MutableSet<UmlElement>)
    
    abstract fun drawField(fieldDrawer: FieldDrawer, 
                           builder: StringBuilder,
                           context: MutableSet<UmlElement>)
    
    abstract fun drawMethod(methodDrawer: MethodDrawer,
                            builder: StringBuilder,
                            context: MutableSet<UmlElement>)
}
Copy the code

And implementation:

  • UmlInterface: interface
  • UmlEnum: enumeration
  • UmlClass: class
  • UmlStub: virtual vertex mentioned in Divide-and-Conquer 6

Definition: IElementDrawer interface and IJavaxElementDrawer interface

interface IElementDrawer {
    fun drawAspect(builder: StringBuilder, element: UmlElement, context: MutableSet<UmlElement>)
}

interface IJavaxElementDrawer {
    fun drawAspect(builder: StringBuilder, element: Element, context: MutableSet<UmlElement>)
}
Copy the code

With reference to the grammar rules of plant-UML, a series of section processing is implemented, such as modifier parsing and output, type parsing and output, name parsing and output, method parsing and output, etc

Here the responsibility chain is used, the Element is converted to plantUml syntax for chain segmentation, and a series of aspects are defined.Limited to space does not expand, interested readers can obtain the source code at the end of the article to learn more

Such as:

abstract class SuperClz : SealedI {
    var superI: Int = 0
}
Copy the code

Its Element, when processed, is converted to the following text:

abstract class "SuperClz"{
  .. fields ..
  {field}-superI : int
  .. methods ..
  {method}+ getSuperI(): int
  {method}+ setSuperI(int): void
}
Copy the code

Treated by PlantUml, it looks like:

Divide and conquer 8 — Output as PlantUml file

Thanks to some of my previous explorations, I’ve developed an annotation processor for generating documents, which is a simple blog link.

With the SPI mechanism in the design, we can easily implement an extension that implements all of the above and easily outputs text documents.

Although it would have been easy to output some text documents in APT, I decided to use a previously built wheel, which was developed for generating documents itself


So far, we have deduced the main process of the whole problem, and we can draw a conclusion: this thing can be done!

Thanks to my wife, she bought a ticket for the high-speed train to go home early in the Mid-Autumn Festival. On the way home, she completed the elaboration of the scheme, and wrote the framework and smoke. Now it takes more time to organize your blog than to code 🤣


grace

Obviously, the above content only “solve the problem”, not “solve the problem brilliantly”.

Such as:

  • Draw multiple class diagrams
  • Added configuration to mask some output, such as: do not want to seeprivateModify the fields of
  • The package name is too long, there is reading interference

And so on.

So let’s keep working on that

To maintain a simple

It’s important that we keep it simple while continuing to polish features!

On the one hand, we should not prematurely consider unnecessary features and keep the functional system simple. On the other hand, functionality should be simple to use and methods or rules should be clear.

For example, when implementing the “ClassDiagram” function:

  • The first thing I thought of wasGenerateClassDiagramaddqualifier:List<String>, you can assign the identified classes to different groups. But it doesn’t look very friendly
  • This is where I came up with the idea of separating configuration from identity. Defines an annotation that can be configured to be identified only by the annotation.
@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class ClassDiagram(
    val qualifier: String = "".val fieldVisible: Array<Visible> = [Visible.Private, Visible.Protected, Visible.Package, Visible.Public],
    val methodVisible: Array<Visible> = [Visible.Private, Visible.Protected, Visible.Package, Visible.Public],
)
Copy the code

This allows users to define annotations freely, such as:

@ClassDiagram("Demo")
annotation class DemoDiagram
Copy the code

Thus, the annotations processor needs to be concerned with two annotations:

  • ClassDiagram: Identifies annotation expression groups and contains configurations
  • GenerateClassDiagram: Identifies the starting point of analysis in the class diagram

In this way, we use the rules more clear! Note that classes annotated by GenerateClassDiagram must add group annotations, i.e. annotations annotated by DemoDiagram, otherwise they will be ignored

Such as:

@GenerateClassDiagram
@DemoDiagram
class Clz : SuperClz(), SealedI {
    val int: Int? = null
}
Copy the code

At present, the added functions are just the details of the above process optimization, implementation is no longer expanded

Combined with my current work after using for a period of time, the function of the plug-in is still enough, I will not do advanced function implementation

Reduce the invasion

As we learned above, using this plug-in requires intrusive changes in the code to add annotations. In theory, intrusion should be as minimal as possible! This also needs to be considered in the design of subsequent functional iterations.

In Divide-and-conquer 3, we temporarily consider all relationships, such as composition and aggregation, to be dependencies.

In the original design, users were required to identify their relationships with annotations, but this was much more intrusive.

Too many comments will affect the readability of the source code and increase intrusion!

In my vision, there will be conventions for dependencies, associations, compositions, and so on through the ClassDiagram to solve this problem and minimize intrusion.

Ability to scale

There are still some features that don’t yet have an elegant solution, as mentioned earlier. I have reserved enough extensibility to decorate the plant-UML syntax document so that readers can extend the PR directly if they have a solution.

For this articleIs notIntroduction to programming skillsorAnalyze how to improve the scalability of the project, so it is no longer expanded

The project address

Use the sample

Add the dependent

Implementation "IO. Making. Leobert - LAN: class diagram - reporter: 1.0.0" annotationProcessor "IO. Making. Leobert - LAN: report - anno - compiler: 1.1.4" annotationProcessor "IO. Making. Leobert - LAN: class diagram - reporter: 1.0.0"Copy the code

All have been released to Mavan Central. The latest version numbers are as follows:

* *

* *

Configuration information:

kapt {
    arguments {
        arg("module"."ktsample") // Module name
        arg("mode"."mode_file") 
        arg("active_reporter"."on")}}Copy the code

Custom annotations

This annotation should be annotated by ClassDiagram, otherwise you can configure it yourself

Such as:

@ClassDiagram(qualifier = "BridgePattern")
annotation class BridgePatternDiagram

//or

@ClassDiagram(
    qualifier = "AAAB",
    fieldVisible = {Visible.Package, Visible.Public}
)
public @interface AAAB {
}
Copy the code

Use with the GenerateClassDiagram annotation

Here is an example of bridge mode, a code that meets the bridge mode is implemented as follows:

class BridgePattern {

    @ClassDiagram(qualifier = "BridgePattern")
    annotation class BridgePatternDiagram

    interface MessageImplementor {
        fun send(message: String, toUser: String)
    }

    abstract class AbstractMessage(private val impl: MessageImplementor) {
        open fun sendMessage(message: String, toUser: String) {
            impl.send(message, toUser)
        }
    }

    @BridgePatternDiagram
    @GenerateClassDiagram
    class CommonMessage(impl: MessageImplementor) : AbstractMessage(impl)

    @BridgePatternDiagram
    @GenerateClassDiagram
    class UrgencyMessage(impl: MessageImplementor) : AbstractMessage(impl) {
        override fun sendMessage(message: String, toUser: String) {
            super.sendMessage("The urgent:$message", toUser)
        }
    }

    @BridgePatternDiagram
    @GenerateClassDiagram
    class MessageSMS : MessageImplementor {
        override fun send(message: String, toUser: String) {
            println("Send a message using a system short message."$message'to$toUser")}}@BridgePatternDiagram
    @GenerateClassDiagram
    class MessageEmail : MessageImplementor {
        override fun send(message: String, toUser: String) {
            println("Send a message using short message mail."$message'to$toUser")}}}Copy the code

After compiling, we get the puml file, and after rendering, we get:

😂 exactly satisfies the syntax of package in plant-UML, normally there is no package

Note that the relationship is fine, AbstractMessage and MessageImplementor behave more appropriately as an association.

In addition, from the perspective of reading habits, some position relations in the figure need to be adjusted, we can add the corresponding configuration mode in the subsequent version.

omg

From the Mid-Autumn Festival design, finished the development piecemically, and then to the National Day to write blog, and made several modifications. During this period, there was an accident at home, and I sincerely hope that you pay attention to your health and spend more time on your family. Code is not finished, knowledge is not finished, but health and life are at an end.

This article belongs to fun series, understand the original intention of fun series.