<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          白嫖一個Android項目的類圖生成工具!(建議收藏)

          共 10128字,需瀏覽 21分鐘

           ·

          2022-03-18 20:16

          ?BATcoder技術(shù)群,讓一部分人先進(jìn)大廠

          大家好,我是劉望舒,騰訊最具價值專家,著有三本業(yè)內(nèi)知名暢銷書,連續(xù)五年蟬聯(lián)電子工業(yè)出版社年度優(yōu)秀作者,百度百科收錄的資深技術(shù)專家。

          前華為面試官、獨角獸公司技術(shù)總監(jiān)。


          想要加入?BATcoder技術(shù)群,公號回復(fù)BAT?即可。

          作者:leobert-lan?

          https://juejin.cn/user/2066737589654327/posts

          前言

          首先必須聲明,這不是一篇廣告或者標(biāo)題黨。而是我開源了一個工具,可以優(yōu)雅的為Java or Kotlin項目生成Class Diagram。

          我推測讀者會進(jìn)來閱讀,原因無非以下兩點:

          • 獲得一個生成類圖的工具,并通過文章快速了解是否方便且好用
          • 了解一下我是如何折騰的

          僅關(guān)心如何使用的,可以移步使用示例章節(jié)

          我們將按照下面的腦圖順時針展開,揭開這一工具的誕生過程。

          主要問題與方案

          背景

          背景:筆者今年換了份工作,所在的公司屬于醫(yī)療器械下的細(xì)分領(lǐng)域,而相比于純互聯(lián)網(wǎng)行業(yè)領(lǐng)域,醫(yī)療器械領(lǐng)域所屬的配套軟件, 都有明確的文檔要求,并非可有可無?,而且公司管理層比較重視細(xì)節(jié)(核心產(chǎn)品為顱內(nèi)、體內(nèi)植入的醫(yī)療器械,確實需要非常認(rèn)真仔細(xì))。

          毋庸置疑,準(zhǔn)確的 、關(guān)鍵的算法流程圖,時序圖,組件圖,狀態(tài)圖,類圖等, 對于產(chǎn)品本身的維護(hù)及發(fā)展具有很大幫助!

          對于研發(fā)工作者而言,高度概括流程、設(shè)計、算法等的專業(yè)工具圖對工作有極大幫助。既然需要審核的文檔中也需要這些內(nèi)容,又對工作有幫助,何不做的好一些呢。

          上文提到的各類UML圖中, 類圖 Class-Diagram 是非常特殊的, 它表述的是 類之間的關(guān)系 ,基于源碼文件分析可以得出準(zhǔn)確的結(jié)果。而流程圖 、時序圖 、狀態(tài)圖 、 組件圖等則不行。

          問題

          隨著行業(yè)的發(fā)展,軟件開發(fā)也演變?yōu)橐缘姆绞?,依次實現(xiàn)最重要的功能,持續(xù)性交付,順其自然的,我們已經(jīng)不再像幾十年前的前輩們那樣:代碼未動,文檔與UML圖先行。一般概要設(shè)計后,方案可行便進(jìn)行編碼了。

          根據(jù)我的實際情況,復(fù)雜的功能一般在草稿紙上畫畫草圖,簡單的就腦子里想想,難以留下存檔

          在這一工作模式下,筆者也遇到了一些問題:

          • 業(yè)務(wù)迭代后或者代碼改進(jìn)后,文檔(uml圖)未及時更新
          • 手動維護(hù)耗時耗力

          如果這件事情可以交給機器來做,那顯然是極好的!而讓機器維護(hù)類圖是最容易實現(xiàn)的!

          綜上所述:我們需要一款工具或者插件,可以直接基于源碼生成類圖 (或者中間產(chǎn)物,例如:plant-uml文件) ,能夠配合其他工具鏈,直接進(jìn)行歸檔。

          當(dāng)然,最重要是免費, 這省去了說服公司進(jìn)行購買。

          留有類圖的好處:

          • 方便向他人介紹業(yè)務(wù)和代碼
          • 項目龐大或者復(fù)雜時,更容易找到需求對應(yīng)的關(guān)注點,重新維護(hù)時日久遠(yuǎn)的業(yè)務(wù)時,狀態(tài)來的快
          • 圖比代碼親切而且保護(hù)隱私??

          解決方案

          眾所周知,Intellij-Idea的官方插件可以分析出類圖,但是Idea是收費軟件,付費支持官方插件是一個省時省力的方案,這是個兜底方案。最終沒轍時,我們再考慮它。

          • 編碼時分析


          仿照官方插件的思路,基于源碼文檔樹進(jìn)行分析,在Intellij的支持下,基于 PSI和uast即可分析出類之間的關(guān)系。這需要一定的PSI、uast知識基礎(chǔ)。

          • 編譯時分析


          在整個編譯環(huán)節(jié)中,有一些切面應(yīng)對特定問題,例如:"注解處理" 、 "Gradle Transformer" ,在此切面處,我們可以基于編譯中間產(chǎn)物,間接分析出類之間的關(guān)系。

          最簡單的是注解處理階段介入,這只需要對Element和TypeMirror有一定的知識基礎(chǔ)即可。

          • 運行時反射分析


          顯然這不是一個太好的切入點,直接pass。

          考慮到PSI方面的知識體系掌握地不太完善,Intellij跨越大版本時,會有較大變更, 而注解處理方面的知識還過的了關(guān),搞個類圖生成問題不大。

          PS: AndroidStudio 基于Intellij核心二次開發(fā),PSI插件跟隨Idea大版本進(jìn)行適配;

          所以最終方案為:從注解處理階段入手,分析編譯中間產(chǎn)物,最終生成類圖

          問題分治與解決

          分治1-簡化輸出產(chǎn)物


          確定了大方向之后,我們需要再思考下整個問題的方方面面。生成類圖有兩大問題需要解決:

          • 從源碼、或者編譯的中間產(chǎn)物中分析出類關(guān)系;ps:我們已經(jīng)確定了要從編譯中間產(chǎn)物出發(fā)
          • 將類關(guān)系轉(zhuǎn)變?yōu)閳D

          顯然,"開發(fā)一個用來生成圖的引擎",這件事情成本過大且沒有必要。所幸的是,UML不是一個新生物,業(yè)內(nèi)也有大名鼎鼎的PlantUml。

          PlantUml?(https://plantuml.com/zh/)基于?Graphviz(http://www.graphviz.org/)?, Graphviz 本身使用Dot語法描述元素與元素關(guān)系, 直接使用 Graphviz 比較樸素,PlantUml通過自定義語法,使得內(nèi)容可閱讀性提升,且無須關(guān)注轉(zhuǎn)換圖片時進(jìn)行各類裝飾問題

          于是,我們可以將問題轉(zhuǎn)化為:從編譯的中間產(chǎn)物中分析出類關(guān)系,將關(guān)系按照PlantUml語法生成puml文件,它的內(nèi)容是純文本。

          分治2-確定分析的起始點


          如果從最終結(jié)果看,我們得到的是一個有方向的圖,那么按照圖本身的起始點出發(fā)比較符合習(xí)慣。

          也就是說,我們將在起始點所對應(yīng)的類上添加注解,作為注解處理的目標(biāo)起始點。例如:


          Cat 和 Dog 將作為起始點。

          因為只需要標(biāo)記類,我們約定注解:

          @Target(AnnotationTarget.CLASS)
          annotation?class?GenerateClassDiagram?{}

          在代碼上,將表現(xiàn)為:

          class?Animal

          @GenerateClassDiagram
          class?Dog?:?Animal()

          @GenerateClassDiagram
          class?Cat?:?Animal()

          在示例中,當(dāng)我們處理GenerateClassDiagram時,可以掃描獲得Cat以及Dog類對應(yīng)的javax.lang.model.element.Element示例,下文簡稱?Element。

          幾點可能存在的疑惑:

          • 為何不 "雙向" 分析:繼承和實現(xiàn)關(guān)系,雙向分析會帶來額外的復(fù)雜度,且在使用上規(guī)則不清晰,依賴關(guān)系難以雙向分析。但是,如果使用規(guī)則上可以做到清晰明了,這一點值得實現(xiàn)
          • 為何不標(biāo)注在Animal上,進(jìn)行反向分析:如果高層級的類在庫包中,則需要修改庫包,這不利于日常管理與維護(hù)
          • 如果只標(biāo)注了Cat而沒有標(biāo)注Dog,Dog將不會體現(xiàn)在圖中?:是的
          • 如果全部標(biāo)注了,是否產(chǎn)生不良影響 :不會,但是沒有必要

          分治3-確定關(guān)系的分析方法


          • 繼承&實現(xiàn)


          因為注解的標(biāo)記對象是類?或者接口,我們理應(yīng)得到TypeElement,基于Element的訪問者模式實現(xiàn),這一點并不難。

          public?interface?TypeElement?extends?Element,?Parameterizable,?QualifiedNameable?{

          ????TypeMirror?getSuperclass();

          ????List?getInterfaces();

          ????//其他無關(guān)代碼略去
          }

          不言自明,我們可以通過?TypeMirror getSuperclass();?得到繼承關(guān)系,通過?List getInterfaces();?得到實現(xiàn)關(guān)系

          注意,此處可以細(xì)分,接口和枚舉僅需要分析實現(xiàn)關(guān)系即可,通過?Element#getKind():ElementKind?可以判斷類型

          • 依賴&關(guān)聯(lián)&聚合&組合


          這四個關(guān)系非常的類似但又不同,先降低復(fù)雜度,均認(rèn)為是依賴關(guān)系,在后續(xù)迭代中,可以進(jìn)一步增加功能,將關(guān)系細(xì)化

          進(jìn)一步降低復(fù)雜度,我們僅從類的屬性出發(fā),分析依賴關(guān)系,忽略掉方法聲明?(可分析)、方法體?(無法分析)?、靜態(tài)塊?(無法分析)中所包含的關(guān)系。

          public?interface?TypeElement?extends?Element,?Parameterizable,?QualifiedNameable?{

          ????List?getEnclosedElements();
          ????//無關(guān)代碼略去
          }

          不言自明,通過這一API,配合 ElementFilter#fieldsIn(Iterable):List 可以得到聲明的 fields;

          通過Element的API可以很輕易的得到命名以及修飾;

          通過 Element#asType():TypeMirror API 將其轉(zhuǎn)換為TypeMirror后, 基于其Visitor模式設(shè)計可以得到field的類型 DeclaredType 并通過DeclaredType#asElement():Element API重新得到Element

          分治4-確定分析的終點


          在分治2中,我們已經(jīng)確定了分析的起點?(可能有多個)?, 在分治3中,我們已經(jīng)確定了關(guān)系的分析方式。為了方便表述,我們以:

          Relation(From,End)?表述?從From?到?End?的關(guān)系

          執(zhí)行一輪 分治2&分治3,我們將得到一系列的Relation(From,End),此時我們將所有的End作為新的From,不斷迭代這一過程,即可完成圖的遍歷!

          那么何時結(jié)束這一過程呢?

          我們只需要維護(hù)一個集合Sfrom,存儲迭代過程中的From ,每次得到的End 只有滿足 "不存在于Sfrom中" 這一條件時,才是新的From,當(dāng)無法獲得新的From時,迭代結(jié)束。

          分治5-分治3的補充,處理集合、數(shù)組、泛型


          按照分治3中的約定,我們將集合、數(shù)組涉及的類型,以及泛型去泛化時的類型,都認(rèn)為和當(dāng)前類型是?依賴關(guān)系;雖然這并不嚴(yán)謹(jǐn)

          得益于TypeMirror的Visitor模式實現(xiàn),我們很容易寫出以下代碼,獲取我們關(guān)心的內(nèi)容!

          private?abstract?class?CastingTypeVisitor<T>?constructor(private?val?label:?String)?:
          ????SimpleTypeVisitor6Void
          ?>()?{
          ????override?fun?defaultAction(e:?TypeMirror,?v:?Void?):?T?{
          ????????throw?IllegalArgumentException("$e?does?not?represent?a?$label")
          ????}
          }

          private?class?FetchClassTypeVisitor?:?CastingTypeVisitor<List>>(label?=?"")?{
          ????override?fun?defaultAction(e:?TypeMirror,?v:?Void?):?List?{
          ????????//ignore?it
          ????????return?emptyList()
          ????}

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

          ????override?fun?visitWildcard(t:?WildcardType,?p:?Void?):?List?{
          ????????val?ret?=?arrayListOf()

          ????????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?{
          ????????val?ret?=?arrayListOf(t)
          ????????t.typeArguments?.forEach?{
          ????????????ret.addAll(it.accept(this,?p))
          ????????}
          ????????return?ret.toSet().toList()
          ????}

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

          ????override?fun?visitTypeVariable(t:?TypeVariable,?p:?Void?):?List?{
          ????????val?ret?=?arrayListOf()

          ????????t.lowerBound?.let?{
          ????????????ret.addAll(it.accept(this,?p))
          ????????}
          ????????t.upperBound?.let?{
          ????????????ret.addAll(it.accept(this,?p))
          ????????}
          ????????return?ret
          ????}
          }

          fun?TypeMirror.fetchDeclaredType():?List?{
          ????return?this.accept(FetchClassTypeVisitor(),?null)
          }

          分治6-關(guān)系的存儲


          顯然,我們需要一個合適的數(shù)據(jù)結(jié)構(gòu)用以存儲圖,得益于我去年在組件化:組件的按序初始化(https://juejin.cn/post/6884492604370026503/)方面的一些探索,當(dāng)時我開發(fā)了Maat(https://github.com/leobert-lan/Maat) 其中包含組件依賴關(guān)系的有向無環(huán)圖分析,其中包含DAG的實現(xiàn)。

          很顯然,我們將“無環(huán)檢測”禁用,就可以直接將數(shù)據(jù)結(jié)構(gòu)拿來使用了,不需要再制造輪子!

          顯而易見,Relation的各種情況可以和度建立映射關(guān)系,人為維護(hù)一個虛擬頂點作為遍歷的起始點可以減少很多麻煩。

          分治7-類型的細(xì)節(jié)處理


          在分治3中,我們已經(jīng)已經(jīng)對類型?(enum、class、interface)?進(jìn)行了很充分的分析,但還遺漏了一些細(xì)節(jié),例如方法、修飾符等;

          在分治6中,我們確定了關(guān)系存儲方案,我們還需要描述圖的頂點。

          我們定義UmlElement類來進(jìn)行描述。

          abstract?class?UmlElement(val?diagram:?ClassDiagram?,?val?element:?Element?)?{
          ????/**
          ?????*?return:?plant-uml?中相應(yīng)的文本
          ?????*?*/

          ????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>)

          }

          并實現(xiàn):

          • UmlInterface:接口
          • UmlEnum:枚舉
          • UmlClass:類
          • UmlStub:分治6中提到的虛擬頂點

          定義:IElementDrawer接口與IJavaxElementDrawer接口。

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

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

          并參考Plant-Uml的語法規(guī)則,實現(xiàn)了一系列的切面處理,例如:修飾符解析與輸出,類型解析與輸出,名稱解析與輸出,方法解析與輸出等

          此處使用了責(zé)任鏈,將Element轉(zhuǎn)化plantUml語法進(jìn)行了鏈?zhǔn)角蟹?,并定義了一系列的切面處理。限于篇幅不做展開,有興趣的讀者可以在文末獲取源碼了解更多。

          例如:

          abstract?class?SuperClz?:?SealedI?{
          ????var?superI:?Int?=?0
          }

          其Element被處理后,將轉(zhuǎn)變?yōu)槿缦碌奈谋緝?nèi)容:

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

          由PlantUml處理后形如:


          分治8-輸出為PlantUml文件


          得益于我先前的一些探索,我曾開發(fā)過一款用于生成文檔的注解處理器,通過(
          https://blog.csdn.net/a774057695/article/details/106603455)簡單了解 。

          設(shè)計中采用了SPI機制,我們可以很輕松地實現(xiàn)一個擴(kuò)展,實現(xiàn)上面提到的所有內(nèi)容,并且輕松地輸出文本文檔。

          雖然在APT中輸出一些文本文檔是一件很簡單的事情,但我決定使用以前造的輪子,畢竟它本身就是為了生成文檔而開發(fā)的

          至此,我們已經(jīng)對整個問題的主要流程進(jìn)行了推演,可以得出結(jié)論:這件事情可以成!

          感謝我老婆早早就買到了中秋節(jié)回家的高鐵票,在?回家的途中?便完成了方案的推演,并進(jìn)行了框架編寫與冒煙。 而現(xiàn)在整理博客的時間遠(yuǎn)遠(yuǎn)超過了編碼??

          保持優(yōu)雅

          顯而易見,上述的內(nèi)容僅止于“解決問題”, 還不能“出色地解決問題”。例如:

          • 繪制多張ClassDiagram
          • 增加配置,屏蔽一些輸出,例如:不想看見private修飾的fields
          • 包名是在是太長了,存在閱讀干擾
          • 等等

          下面繼續(xù)雕琢

          維持簡單


          在繼續(xù)雕琢功能的同時,我們必須兼顧簡單性,這一點非常重要!

          一方面,我們不要過早的考慮用不著的功能,維持功能體系的簡單。另一方面,功能使用要簡單,方法或規(guī)則要明確。

          例如在實現(xiàn)“繪制多張ClassDiagram”功能時:

          我最先想到的是在GenerateClassDiagram中添加qualifier:List, 可以將被標(biāo)識的類分配到不同的組別。但是它看起來并不太友好。

          于是我產(chǎn)生了:將配置與標(biāo)識分離的想法。定義一個注解,可以進(jìn)行配置,它僅可被標(biāo)識于注解。

          @Target(AnnotationTarget.ANNOTATION_CLASS)
          annotation?class?ClassDiagram(
          ????val?qualifier:?String?=?"",
          ????val?fieldVisible:?Array?=?[Visible.Private,?Visible.Protected,?Visible.Package,?Visible.Public],
          ????val?methodVisible:?Array?=?[Visible.Private,?Visible.Protected,?Visible.Package,?Visible.Public],
          )

          這樣使用者可以自由的定義注解,例如:

          @ClassDiagram("Demo")
          annotation?class?DemoDiagram

          這樣,注解處理器需要關(guān)心的注解將變?yōu)閮蓚€:

          • ClassDiagram : 標(biāo)識注解表達(dá)分組,并且包含配置
          • GenerateClassDiagram :標(biāo)識類圖中的分析起始點

          如此,我們使用時的規(guī)則更加清晰!注意,被GenerateClassDiagram 注解的類,必須添加分組注解,即被DemoDiagram注解的注解,否則將被忽略。例如:

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

          目前增加的功能,僅僅是在前文大流程上的細(xì)節(jié)優(yōu)化,實現(xiàn)不再展開。

          結(jié)合我目前的工作使用了一段時候后,插件的功能還夠用,就先不做超前的功能實現(xiàn)了。

          減少侵入


          我們從上文獲知,使用這一插件,需要在代碼中進(jìn)行侵入修改,增加注解標(biāo)注。理論上而言,侵入應(yīng)該最少越好!在后續(xù)功能迭代設(shè)計中,同樣需要考慮這一點。

          在分治3中,我們將組合、聚合等關(guān)系暫時全部認(rèn)為是依賴關(guān)系。

          在最初的設(shè)計中,需要使用者通過注解標(biāo)識其關(guān)系,但侵入性大大提升。

          過多的注解會影響源碼可讀性以及增大侵入

          在我的構(gòu)想中,將通過ClassDiagram對依賴?、關(guān)聯(lián)?、組合等進(jìn)行一些約定,以解決這一問題并盡可能地減少侵入。

          擴(kuò)展能力


          目前還有一些功能尚未有優(yōu)雅的解決方案,在前文已經(jīng)提及。 我預(yù)留了足夠多的擴(kuò)展性用于裝飾Plant-Uml語法文檔,如果讀者有處理方案,可以直接擴(kuò)展后提PR。

          因本文并非為介紹編程技巧或者剖析如何提升項目的擴(kuò)展能力?,故不再展開。

          使用示例

          添加依賴


          implementation?"io.github.leobert-lan:class-diagram-reporter:1.0.0"
          annotationProcessor?"io.github.leobert-lan:report-anno-compiler:1.1.4"
          annotationProcessor?"io.github.leobert-lan:class-diagram-reporter:1.0.0"

          均已發(fā)布到MavenCentral,最新版本號可參考以下:


          配置信息:

          kapt?{
          ????arguments?{
          ????????arg("module",?"ktsample")?//模塊名稱
          ????????arg("mode",?"mode_file")?
          ????????arg("active_reporter",?"on")
          ????}
          }

          自行定義注解


          該注解需被ClassDiagram注解,其他自行配置,例如:

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

          //or

          @ClassDiagram(
          ????qualifier?=?"AAAB",
          ????fieldVisible?=?{Visible.Package,?Visible.Public}
          )

          public?@interface?AAAB?{
          }

          配合GenerateClassDiagram注解使用。

          此處以橋接模式示例,一個滿足橋接模式的代碼實現(xiàn)如下:

          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("加急:$message",?toUser)
          ????????}
          ????}

          ????@BridgePatternDiagram
          ????@GenerateClassDiagram
          ????class?MessageSMS?:?MessageImplementor?{
          ????????override?fun?send(message:?String,?toUser:?String)?{
          ????????????println("使用系統(tǒng)內(nèi)短消息的方法,發(fā)送消息'$message'給$toUser")
          ????????}
          ????}

          ????@BridgePatternDiagram
          ????@GenerateClassDiagram
          ????class?MessageEmail?:?MessageImplementor?{
          ????????override?fun?send(message:?String,?toUser:?String)?{
          ????????????println("使用郵件短消息的方法,發(fā)送消息'$message'給$toUser")
          ????????}
          ????}
          }

          編譯后,我們得到的puml文件,渲染后得到:


          ??恰好滿足了plant-uml中package的語法,正常情況不會有package。

          我們可以注意到,關(guān)系上沒有太大問題,AbstractMessage與MessageImplementor之間表現(xiàn)為關(guān)聯(lián)更加恰當(dāng)。

          除此之外,從閱讀習(xí)慣上而言,圖中的一些位置關(guān)系,還需要再調(diào)整,我們可以在后續(xù)的版本加,添加相應(yīng)的配置方式。

          項目地址:
          https://github.com/leobert-lan/ReportPrinter/



          ? 耗時2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!

          ? 『BATcoder』做了多年安卓還沒編譯過源碼?一個視頻帶你玩轉(zhuǎn)!

          ? 『BATcoder』我去!安裝Ubuntu還有坑?

          ? 重生!進(jìn)階三部曲第一部《Android進(jìn)階之光》第2版 出版!

          為了防止失聯(lián),歡迎關(guān)注我的小號

          ??微信改了推送機制,真愛請星標(biāo)本公號??
          瀏覽 66
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人男人的天堂视频在线观看 | 中文无码在线观看中文字幕av中文 | 欧美aaa在线 | 五月天成人免费视频 | 亚洲无吗在线视频 |