如何進行高質量的DDD領域建模?什麼是領域模型?如何捕捉?尺寸如何? - Manning

banq發表於2019-07-05

本文深入研究DDD和模型:它們是什麼,它們之間的關係以及模型在領域驅動設計中的工作方式。

模型作為深入洞察的工具

讓我們首先解釋DDD對模型的意義,因為它們位於DDD的中心。在系統開發中,“模型”一詞意味著許多事情 :流程上的UML圖表,資料如何在資料庫表格中佈局,以及許多其他內容。

在DDD中,我們使用“模型”一詞來解釋我們如何捕捉我們對手頭業務的基本理解,提煉成一組選定的概念。為什麼我們需要這樣的模型,它們應該是什麼樣的?

領域驅動設計不是靈丹妙藥。當您的系統處理一個不容易掌握的問題時,領域驅動設計是非常適合的。在這些情況下,最關鍵的問題是理解領域的複雜性。然後,理解和建模才應該是您的主要關注點

如果您無法掌握各種技術方面的複雜性,那麼您將獲得一個不太有用的系統。但是,如果你無法掌握領域的複雜性,你就會得到一個接近於毫無價值的系統。在這方面,領域是關鍵的複雜性。

想象一下,您有一個處理機場托執行李的系統,領域的複雜性可能是您的關鍵複雜性。如果您無法正確表達行李從登機櫃臺到飛機、通過傳送帶和裝載卡車的行程,那麼行李可能無法及時進行正確的託運,或最終導致錯誤託運的行李。乘客會生氣,相關單位公司將失去信任和金錢。

更糟糕的是,有重要的安全方面受到威脅。比如在檢查行李上機時,但發現乘客還沒有出現在登機口登記,那麼行李系統必須確保他的行李已下機。如果系統沒有正確設計,可能會欺騙它將行李裝入特定的航班,或者不解除安裝它 - 這可能會產生嚴重的安全後果。

如果您未能深入而準確地瞭解行李處理,那麼您不僅要構建一個有缺陷的系統; 你製造了一些對企業有害並且對客戶有潛在危險的東西。這比壞的系統更糟糕,它使系統變得毫無意義。相反關閉這個有缺陷的系統,機場甚至可能會更好。這不是一個假設的例子; 由於行李系統的不足,20世紀90年代丹佛機場的開通推遲了一年半。

在這種情況下,理解和建模行李處理領域應該是您工作的重點,但是這時花時間優化資料庫連線池將是一個糟糕的選擇。關鍵的複雜性是領域。未能解決關鍵複雜性使得解決方案毫無意義

現在談到安全性,很難掌握對領域足夠的理解來確保系統在所有可能的情況下都表現良好。對於仁慈的“正常”資料來說,這很難做到,可能會出現所有奇怪的情況。以一種抵抗惡意資料的方式來做這件事更難。有人可能會試圖通過向其傳送奇怪的資料來攻擊您的系統,將其操縱為做壞事。系統仍應以健全和安全的方式作出反應。

為了安全起見,必須專注於構建領域模型。避免許多安全問題作為副作用 - 尤其是業務完整性問題,但在某種程度上它還可以保護系統免受某些技術攻擊。

當某些情況並不是正確的選擇時,則會集中在對域進行建模。例如,如果您為網路路由器編寫軟體,那麼I / O吞吐量是最重要的。在這種情況下,您的關鍵複雜性是技術性 ,但即使在這裡,您也應該考慮一個草率的域模型是否可能有一個安全問題。關鍵複雜性始終是潛在的。請注意它是技術方面還是領域的。

領域建模的主要好處在於它可以作為深層學習的工具。在這個級別學習至關重要。“抓住商人的術語”並不難,我們可以使用相同的術語編寫一份看起來不錯的需求文件。但是,如果沒有深入的學習,這樣的檔案將包含微妙的誤解,不一致和邏輯漏洞。這些缺陷使得無法建立一個在棘手的情況下做正確事情的可靠系統 - 安全漏洞是最糟糕的後果。與領域專家合作建立一個領域模型,促進學習。

我們需要的是以穩定和安全的方式支援開發的領域模型。

要使域模型有效,它需要:

  • 簡單地將我們的注意力集中在要點上
  • 要足夠嚴格,它可以成為編寫程式碼的基礎
  • 深入理解,使系統真正有用和有用
  • 從務實的角度來看,是最好的選擇
  • 向我們提供我們在談論系統時可以使用的語言

模型是簡化的

模型是一種更簡單的現實形式。這是我們刪除不相關部分的簡化。例如,當您在機場辦理行李託運時,系統無需顯示您的鞋碼。另一方面,它可能與表示袋子的重量有關。為了便於理解和編碼系統,我們建立了一個包含行李重量的模型,但不包括乘客的鞋碼。我們保留我們認為有關係的細節。

需要明確的是,模型不是圖表。在許多其他上下文中,“模型”表示特定的圖表型別,例如實體關係模型,這通常用於資料庫設計,或者來自UML的類圖。這些圖表是模型的表示,但模型是對我們簡化的現實如何工作的概念性理解。

領域驅動設計中“模型”的使用更接近於這個單詞的另一種用法,即時短語“模型訓練”意思。當建立模型訓練時,建模者付出了很多努力來保持現實的某些方面,而完全忽略了其他方面。要保留哪些細節以及要扭曲的細節是構建訓練模型以及領域模型的關鍵。

如何進行高質量的DDD領域建模?什麼是領域模型?如何捕捉?尺寸如何? -  Manning

毫無疑問,我們在圖1中看到的是模型火車。它看起來像火車,在鐵軌上移動,但它不是真正的火車。我們認為它是一種火車模型,因為它保留了一些重要的屬性,而我們允許它忽略其他一些屬性。

讓我們列出模型與現實共有的一些屬性:

  • 顏色 - 我們認為特定列車的模型應該與原始列車具有相同的顏色。
  • 相對大小 - 我們希望保持比例。如果門的寬度是實際寬度的兩倍,我們預計模型列車的比例相同。
  • 形狀 - 我們希望模型火車及其細節具有相同的形狀,例如前窗的曲率。
  • 運動 - 我們希望模型火車沿著軌道移動,就像真正的火車一樣。

讓我們列出一些模型與現實不同的屬性,以及我們認為差異很好的地方:

  • 材料 - 當原件由其他材料製成時,模型火車由塑料或錫製成是可以的。
  • 絕對尺寸 - 如果真正的貨車長30米,我們很好,它們在模型中要小得多。
  • 重量 - 模型更輕,這是可以的。
  • 推進方法 - 該型號沒有蒸汽機; 它依靠電力執行。
  • 軌道曲率 - 模型中的曲線比現實中的曲線要緊,我們接受。

奇怪的是,找到模型火車和真實火車之間的差異比找到它們共有的東西更容易。儘管如此,我們仍然堅定地認為這是一個合適的火車模型。很明顯,這個特定的模型已經設法捕捉到我們對火車的理解的基本要素。

似乎“顏色,相對大小和運動”足以讓我們理解模型是火車。這三個屬性是必要的 - 如果模型不能滿足它們,我們就不會玩並假裝它是火車。而這三個就足夠了 - 如果模型不能滿足其他一些期望,比如材料,我們仍會繼續玩並假裝它是一列火車。在一天結束時,模型是對現實的簡化,我們仍然接受簡化作為真實事物的有效表示。

我們現在離開了玩具領域,並帶著我們的想法,即模型是對真實事物的簡化理解。這適用於我們在系統開發中使用的模型。如果我們為一個人建模,我們可能會選擇抓住一些屬性:一個人有一個名字,一個年齡,一個特定的鞋子大小,並且可選擇有一個寵物。同意,這是一個粗略的模型,但仍然是一個模型。

如何進行高質量的DDD領域建模?什麼是領域模型?如何捕捉?尺寸如何? -  Manning

模型可能是一種簡化,但它必須足夠通用,以便我們可以捕獲一些我們認為有趣的變體。在我們的例子中,我們想要允許不同的名字,不同的年齡和不同的鞋碼,我們允許人們有或沒有寵物。我們允許在模型中顯示所有這些差異。我們不對不同身高的人做出任何區分,也不注意他們的髮型。

我們可以用許多不同的方式來表示這個模型。我們可以使用純文字來解釋我們的意思。我們可以使用不同型別的圖表來說明它。我們可以使用程式碼:虛擬碼或來自程式語言的實際程式碼。這裡重要的一點是,這些表示形式都不是模型。特別是類圖通常與作為模型混淆,但模型無關任何表示形式。模型是我們認為在我們的建模中必不可少的概念性理解 : 比如在這種案例情況下是名稱,年齡,鞋子尺寸和寵物。

將模型保持為現實的簡化版本的主要好處是簡單模型更容易嚴格,這在我們以後從中構建軟體時是必不可少的。

模型很嚴格

領域模型不是現實的淡化版本; 它在豐富中失去了它的嚴格性。人是複雜的生命,有很多屬性和很多關係。當我們決定專注於名字,年齡,鞋碼和寵物時,我們會失去很多豐富感。但是我們在“人”的意思中獲得了精確度 - 這種精確度使得用軟體表示這個實體成為可能。瞭解該域的人稱為領域專家

編寫軟體是兩種來自不同方向並且需要以富有成效的方式進行會面的專業人員之間的協作:業務人員和開發人員。每個人都有不同的需求,必須滿足這些需求來建立出色的軟體。業務人員需要看到他們熟悉的術語,而不是準技術的mumbo-jumbo。如果他們無法表達他們的領域,我們就失敗了。但是,在使用者介面或列印報告的標題中使用熟悉的單詞作為標籤是不夠的。系統還必須以業務人員認為合理的、一致和易懂的方式行事。

為此,領域模型必須嚴格。如果模型不嚴格並且包含模糊性,那麼系統的一部分可能以一種方式表現而另一部分表現為另一種方式。例如,值機櫃臺的螢幕可能會顯示“行李數量”,門口的另一個螢幕可能會顯示“行李數”,裝載人員使用的平板電腦可能會顯示“行李箱”。

更糟糕的是,一些這些術語可能會將隨身攜帶數量作為數字的一部分,而其他術語則不然。每當工作人員互相交談時,他們每個人都必須記住對方正在看的螢幕,並記住從他們看到的號碼中新增或減去隨身攜帶物。有時會產生誤解,丟失行李。該系統使業務失敗,甚至領域專家都認為它沒有意義。

另一個可恥的變體是模型在術語中是一致的,但在約束和關係上過於寬鬆。這通常是使用“標準系統”和“配置”到域的結果,這是使用企業資源規劃(ERP)產品的常用方法。

最初為製造業建立的ERP,用於規劃機器和原材料的使用。由於工廠不同,ERP系統可以高度配置,以滿足每個工廠的需求。如今,它們經常被描述和銷售為“標準系統”,可以配置為處理任何領域,而在引擎蓋下它們仍然是相同的材料流系統。但是,這一業務已成功銷售此類系統到其他領域,以處理各種客戶投訴,比如警方調查或其他完全不同的領域。

如果你想配置物流系統來處理警察調查,你需要做一些非直觀的抽象:“警察可以看作是一臺機器,關於入室盜竊的報導可以看作是一堆原始的材料,在調查過程中由警察機器完善。“為了將一個領域變成另一個領域,你需要越來越少的具體,越來越不精確。

比如有一個“物件管理系統”,其中一切都是“物件”。通過使用者介面,您可以更新物件的屬性,但它幾乎不瞭解這些物件代表什麼。您通常可以填寫屬性和關係的任意組合。

過於寬泛的系統容易出錯,這種寬容甚至可能導致安全漏洞。這需要快樂的業務人員和快樂的開發人員來建立一個好的系統。兩個群體都需要滿足他們的基本需求。

關注業務人員是很重要的。他們需要認識到他們習慣使用的領域,我們應該選擇他們熟悉的術語。無法滿足領域專業人士的需求是一個很大的錯誤。不能滿足其他專業人士如開發人員的需求也是一個同樣大的錯誤。作為開發人員,我們需要嚴格。說“大多數人有一隻寵物”是不夠的。我們需要知道“有寵物”是否僅限於只有一隻寵物。

這是成為開發人員需要一些勇氣的地方。我們需要問問題,使模型嚴格,沒有含糊之處。

如果我們問“可以有多隻寵物嗎?”我們可能會得到答案,“哦,這真的很不尋常。”這給我們留下了兩個選擇。要麼我們想要,“那麼我需要允許一個寵物清單,”或者我們認為,“只允許一隻寵物。”

在第一種情況下,我們最終編寫的系統可能比必要的複雜性更多,遲早會有一些奇怪的組合將會發生。在第二種情況下,我們不允許多隻寵物,只是在幾個月後發現有一些顧客 - 也許是我們在收購另一家公司時獲得的顧客 - 有兩隻或更多寵物時才會被打擊。業務人員甚至會將責備轉向我們。

擺脫這種困境的方法是積極詢問模型中應該包含的內容:“我們應該允許多隻寵物,還是我們只限制一隻寵物?”決定是否應該覆蓋不尋常的多寵物人群技術決策 - 這是一個業務決策。如果我們沒有系統支援,那麼必須通過單獨的手動例程來處理它們。

另一方面,為許多多樣性提供可能性也不是免費的。允許越來越多的通用模型很誘人; 遲早,一切都與其他一切都處於多對多的關係中,但從長遠來看,這並沒有帶來任何好處。很難預見並概述一般模型的後果。

假設有一個功能允許一個人與另一個人“交換寵物”。如果我們還允許每人擁有多個寵物,那麼我們需要弄清楚交換寵物意味著什麼。這是否意味著A獲得B人的所有寵物,反之亦然?或者我們只交換一隻寵物?

如果我們不讓模型反映業務領域,我們就會讓商人失望。如果我們不讓模型嚴格,我們就會讓開發人員失望。一個好的模型必須反映業務領域並且要嚴格

模型嚴格意味著我們能夠使用模型作為基礎來構建程式碼。一個好的模型必須反映業務領域並且要嚴格。

當我們設計軟體時,我們做出類似的選擇 我們對複雜現象進行簡單表示。讓我們看一下教科書的物件導向示例,其中忽略了許多屬性和關係,只剩下一個人的狹窄檢視:

class Person { 
    private String name;
    private int age;
    private int shoeSize;
    private Animal pet;
    void growOlder() {
       this.age++;
    }
    void swapPetWith(Person other) {      ...
    }
}

在這個設計中,我們刪除了一個人可能擁有的大量屬性和行為,將其減少為四個基本屬性。省略細節似乎會使系統變得更窮,但它給我們帶來了很大的好處。

我們通過省略細節獲得的是準確的可能性。在人的領域中,“人”是一個複雜的互動,但在我們的域模型中,一個Person是具有名稱,年齡,鞋子大小和變老能力的東西。當我們使用“人”這個詞時,這正是我們的意思。我們在豐富性方面失去了我們在精確度上獲得的東西

模型深刻理解

現實世界的問題更為複雜:比如 機場行李搬運,我們在領域模型中捕獲的嚴格理解比大多數人想象的要深刻。事實上,我們需要捕獲的知識甚至比大多數領域專家在日常工作中的理解更深刻,特別是當他們根據具體情況處理情況時。

這樣做的原因是我們不僅需要足夠的理解才能在域中工作,我們需要足夠深入的理解來構建機器。讓我們將其與騎自行車的挑戰進行比較。

我們大多數人都是騎自行車的專家。我們可以通過騎自行車並騎行來證明這一點,即使在極具挑戰性的條件下,例如在崎嶇不平的道路和大風天氣中,甚至在一隻手臂下攜帶大包裝時。這需要專業知識 - 將其與在陽光明媚的夏日學習騎平地的孩子所面臨的困難相比較。

這種專業知識可與領域專家的專業知識相媲美。他們知道領域是如何運作的。例如,航運專家知道如果在條件變得艱難時如何佈置貨物集裝箱,例如當集裝箱錯誤地從船上卸下,並且在相當長的時間內沒有其他船舶離開同一目的地時,也就無法再找到船舶裝運這一個錯誤卸下的集裝箱。領域專家可以處理甚至棘手的每個案例。

不幸的是,我們編寫軟體系統所需要的理解更深入。我們沒有奢侈的“在現場”來處理任何出現的情況,能夠評估和即興解決問題。我們正在編寫一個應該這樣做的程式,沒有我們(我們所有的專業知識)以人的形式存在。我們面臨的挑戰不是騎自行車,而更像是騎自行車的機器人。

如果我們要建造一個騎自行車的機器人,我們對自行車騎行的理解需要比大多數專家更深刻,甚至專業的自行車信使或BMX專業人士。例如:騎自行車時如何右轉?想想它幾秒鐘 - 你可能已經完成了一千次。大多數人自發地回答:“我拉上右側車把。”不幸的是,由於離心力的原因,這樣做會導致你跌落到瀝青路面上。你下意識地做的是把把手轉向左邊,導致你在短時間內向右傾斜。幾毫秒後,您向右傾斜到合適的角度,然後將車把向右轉 - 讓您右轉。您向右傾斜的角度正是補償離心力所需的角度,您向右轉,安全穩定。

你不假思索地做到這一點,並且在不瞭解微妙的動覺機制的情況下 - 赫伯特戈德斯坦的經典力學是一本關於這一主題的優秀書籍。但如果我們想要建造一個騎自行車的機器人,這就是理解的必要深度。

這個騎自行車的機器人故事給了我們一些壞訊息和一些好訊息。壞訊息是,如果我們檢視領域專家的負責人,我們發現沒有現成的模型。那裡沒有“真實”的模型。我們不能要求領域專家,並希望得到我們需要的所有答案。好訊息是,與領域專家合作製作模型是一項有趣而有益的工作。這樣做是一個迭代過程,探索許多可能的模型,並選擇一個適合解決我們手頭的問題的模型。

製作模型意味著選擇一個

建模的一個常見神話是,某處有一個“真實”模型,通常被認為是嵌入領域專家的頭腦中。事實並非如此。製作模型涉及許多可能模型之間的主動選擇,我們需要選擇最適合我們需求的模型。

在領域驅動設計中,我們有時會使用“蒸餾模型”這一短語。讓我們與威士忌蒸餾器進行一段時間的比較。威士忌酒從一大批發酵麥芽汁開始 - 基本上是不可飲用的 - 然後加入一些熱量並收集蒸汽。蒸餾器丟棄第一部分,其中含有丙酮。中間部分由大部分酒精,一些水和溶解在其中的天然香料組成。這被認為是好的部分並保留。最後一部分包括一些酒精,大量的水和一些不太吸引人的味道。這也被丟棄了。保留的是我們所說的威士忌。您對威士忌或您的口味的個人態度可能會有所不同,但您明白了。當我們提煉模型時,我們拋棄現實的某些部分並保留其他部分。

這裡重要的一點是蒸餾器有很多方法可以完成它們的工作。他們有一個選擇。保持中間部分是一種選擇,因為蒸餾器的目標是獲得具有某些特定風味的高酒精度結果。但蒸餾器本可以做出其他選擇。如果蒸餾器需要丙酮,那麼蒸餾看起來會有所不同。蒸餾器將保留第一部分並丟棄其餘部分。同樣,我們可以根據我們打算使用模型的方式,從同一個現實中提煉出不同的模型

我們描述了一個名字,年齡,鞋子大小和寵物的人是一個模型。另一種模型可以是按出生日期,出生地,母親姓名和父親姓名來描述一個人。兩種模型都沒有比另一種更正確。每個都是不同的,有利於其設計目的。如果我們為狗主人俱樂部保留一個登錄檔,第一個模型明顯優於第二個模型。如果我們正在研究一個家庭如何通過遷移傳播到世界各地,那麼第一個模型就毫無價值,第二個模型非常優秀。

在進行建模時,請主動嘗試查詢表達域的不同模型。嘗試找到三種不同的模型,並比較它們在表達域問題方面的優勢。找到一個好的模型很重要,因為它可以以有效和明確的方式討論域。一個好的模型形成一種語言。

模型形成了無處不在的語言

建模的一個有趣方面是模型建立了一種語言 - 我們談論系統的語言。

首先,我們必須意識到,當域專家互相交談時,他們會使用自己的語言。這是域語言。它可能聽起來像英語,但它在一個方面是英語的一個子集 - 有很多常見的英語單詞沒有在這個領域專家語言中使用。另一方面,它是英語的超集 - 有很多特定領域的術語和慣用語在普通英語中沒有使用。哪些領域專家互相交流是一種旨在實現有效溝通的語言。

花點時間考慮系統開發人員的領域專家語言。在我們自己中,我們很容易丟擲對我們來說非常有意義的術語,但對於非開發人員來說完全不可能理解; 我們可以“彙集關係”或“制定一項戰略。”金融,物流或醫療保健領域的專家也有自己的術語。

如果我們正在建立一個物流系統,從物流中獲取術語並將其編碼為軟體系統似乎是一種合乎邏輯的方法。這是個好主意,但不幸的是有缺陷。物流專家使用的語言在邏輯上並不一致。這並不是因為他們的術語特別草率。軟體開發人員的術語同樣草率。

聆聽任何兩位經驗豐富的開發人員的談話,你會發現他們可以互換地使用“物件”,“例項”和“類”這兩個詞,就好像它們是同義詞一樣。而且我們知道它們不是,因為當我們向初學者解釋物件導向時,我們會小心區分“類”和“物件”。但是當兩位專家討論時,他們可能會草率,因為他們無論如何都要相互理解和真正的討論在其他地方,更高層次。當他們互相交談時,不要變成語言警察,糾正領域專家。在和同齡人交談時,讓自己變得草率。

如果我們正在建立一個物流系統,如果我們能夠形成一種能夠以精確的方式談論系統而又沒有誤解風險的語言,那會不會很好?這正是模型的內容。如果我們在物流專家和開發商之間共同決定“支路”是指使用同一車輛從一個地方到另一個地方的運輸,我們已經決定“終止一條路線”意味著貨物在目的地卸下,那麼我們就可以使用這些術語並讓自己理解。如果我們說,“如果兩個運輸工具在同一個碼頭終止各自的路線,那麼它們可以在下一條路線上共同運輸”,那麼這個短語可以被明確地理解並且可以實現該功能。

在討論系統的功能時,請使用作為模型一部分的單詞和短語。通過這樣做,您將很快意識到功能是否可以實現。如果使用模型中的術語來表達功能很尷尬,這是一個肯定的跡象,它實現起來很尷尬。這可能表明模型需要擴充套件以包含新術語,並且系統重構以保持一致性。

使用領域驅動設計的術語,我們希望模型在談論系統時成為無處不在的語言。無處不在,在這種情況下,我們的意思是術語應該在我們談論系統的任何地方使用。應在使用者介面,手冊,需求或使用者故事,程式碼和資料庫表中使用相同的術語。為什麼在使用者介面中呼叫某個“數量”,在手冊中將其稱為“金額”,並將資料庫列命名為“卷”?堅持跨學科使用相同的語言有助於發現可能表現為錯誤或安全漏洞的歧義。

值得指出的是,永續性模型可能與概念模型略有不同。例如,我們可能必須將概念拆分為不同的表,並且我們可能需要連線不屬於概念模型的表或合成鍵。同樣,出於特定於實現的原因,程式碼中的類可能與概念模型中使用的術語略有不同。然而,我們捕獲的理解仍然是相同的,並且當我們命名我們的構造(類或資料庫表)時,我們嘗試儘可能多地使用無處不在的語言中的術語。

這並不意味著我們正在變成語言警察部隊。在談論系統時,模型或領域模型語言是無處不在的語言。領域專家仍被允許在他們自己之間使用他們模糊的領域語言,就像在與其他開發人員的討論中允許開發人員對“物件”與“類”的草率一樣。

關於在普遍存在的語言中保持精確的重要一點是,當我們談論系統時,我們需要精確。當業務專家和開發人員進行互動時,這一點尤其重要,並且誤解的風險最高。在這些情況下,我們應該堅持使用普遍存在的語言的術語。堅持在任何需求文件中使用域模型中的單詞。如果在域模型的術語中難以表達某些內容,則可能很難將其編寫為軟體。

值得指出的是,因為語言無處不在並不意味著它是普遍存在的。在談論這個特定的系統時,這是無處不在的語言,而不是談論其他系統(甚至是其他物流系統)。

不同的系統有不同的需求和不同的重點。他們有不同的模型和不同的語言。每種領域模型語言都是其領域內無處不在的語言,但不在外部。語言的上下文有一個外邊界。在域驅動設計中,我們將其稱為模型的有界上下文。在有界上下文中,模型中的每個單詞都有明確定義的含義,但在有界上下文單詞之外可能意味著完全不同的東西。

請在這裡檢視liveBook上的書並檢視這個幻燈片

作者簡介:Dan Bergh Johnsson,Daniel Deogun和Daniel Sawano幾十年來一直致力於安全與開發。他們是開發人員的核心,並且理解安全性通常是一個側面問題。他們還改進了工作習慣,使他們能夠以促進安全的方式開發系統,同時專注於高質量的設計習慣 - 開發人員在日常工作中更容易記住這些習慣。這三位都是國際發言人,經常出席有關高質量發展和安全的會議。

相關文章