構之有道,建之有法——軟工個人閱讀作業#2

Potassium發表於2021-03-13
專案 內容
這個作業屬於哪個課程 2021春季計算機學院軟體工程(羅傑 任健)
這個作業的要求在哪裡 個人閱讀作業#2要求
我在這個課程的目標是 提升工程能力和團隊意識,熟悉軟體開發的流程
這個作業在哪個具體方面幫助我實現目標 閱讀《構建之法》,初窺專案流程;瞭解基礎的程式碼版控軟體和持續整合方法

閱讀提問

剛發部落格作業的時候,被“快速看完整部教材”嚇了一跳。開啟書籍,\(600\) 多頁,頓感任務重大。

沒有讀書習慣,但有作業逼迫,迫不得已翻開了此書。沒想到書中居然沒有各種晦澀難懂的概念詞彙,取而代之的是以各種真實案例、以及幽默詼諧的“移山公司”事例,去解釋敏捷軟工的各個實踐步驟。知識解釋地非常鮮活易懂,常常在前面提出的問題,再讀一讀書、查一查資料後面都解決了,基本提不出什麼問題來。厚厚一本書,不到兩週很快就通讀了一遍。仍然有一些問題,聊做記錄。

問題一:單元測試一定要作者寫嗎

單元測試必須由最熟悉程式碼的人(程式的作者)來寫。程式碼的作者最瞭解程式碼的目的、特點和實現的侷限性。所以,寫單元測試沒有比作者更適合的人選了。

——《構建之法》第二章 2.1.2 好的單元測試的標準

書中,作者認為,程式碼的作者最瞭解程式碼,因此單元測試最適合的人選便是作者本人。

單元測試最常見的目標便是驗證一段程式碼是否符合預期設計邏輯。其實這種測試是簡單的,待測部分往往不是一個函式就是一塊程式碼,對於與外界的互動較少,沒有複雜的依賴。對於這樣的測試,程式碼編寫者很容易構造出其上下文進行測試。較小的、耦合度低的的程式碼塊,出現 bug 的機率也相對較小,這一部分的單元測試其實並非單元測試的側重點。

而重點的測試部分都是偏向整體的、業務的邏輯,經常在一個函式中就會有意無意的牽連到其他部分,包括類之間的相互作用等,如何處理這種依賴關係是一個非常大的問題。比如說,驗證一個需要資料庫依賴的業務一是否符合設計邏輯,那麼如果使用嵌入式資料庫進行本地測試,此時就是預設了一種“這段程式碼的 SQL 中,使用嵌入式資料庫和生產環境資料庫是等價的”的假設,要驗證這一個假設是否成立,還需要更進一步的、更全面的“整合測試”,比如真實的使用生產資料庫進行測試。

單元測試的流程主要是評估待測試程式碼範圍、拿到開發程式碼進行調整,之後進行全面仔細的、邊界情況和分支全覆蓋的測試,以最小化測試漏洞。也就是說,單元測試的一大重點便是對於邊界情況進行全覆蓋,覆蓋的越全面則測試的效果越好。然而寫程式碼本身的人在編碼時就在用其本身的邏輯進行開發,這種本身的邏輯很容易將自身陷入一種思維定式,對於某特定分支、邊界情況未必能夠考慮到,作者本人寫的單元測試也往往覆蓋不到這種邊界的情況。

這是一種設計上的欠缺,是一種思維上的漏洞,而這種漏洞單靠單元測試是無法發現的。筆者之前在 Ruby on Rails 敏捷開發時編寫單元測試總有一種感覺,寫一堆自己明顯感覺不會出錯的測試,像是在“湊字數”、“浪費時間”。寫完單元測試總是變綠的,但論及許多複雜的分支邊界情況,往往在後續的開發中,或者同學之間的交流才能夠發現,甚至再也沒能發現

當局者迷,旁觀者清。如果讓一個並不熟悉這一部分程式碼的人來編寫單元測試,他就有更大的概率突破作者本身的思維定式,而對於一些刁鑽的邊界情況進行處理,這種邊界情況不僅僅包含輸入輸出內容,還包括呼叫位置、呼叫時機等等多個方面。

在做 ROR 的時候,也曾使用過測試驅動開發(TDD)的開發方式,寫測試“變紅”再寫程式碼“變綠”,先考慮邊界情況、提出測試,再進行編寫,似乎也是一種不錯的解決思維定式的方案。是否僅有這種方式,單元測試自己編寫才更站得住腳?

問題二:結對程式設計的應用性如何

駕駛員和領航員不斷輪換角色,不要連續工作超過一小時,每工作一小時休息15分鐘。

只有水平上的差距,沒有級別上的差異。兩人結對,儘管可能大家的級別資歷不同,但不管在分析、設計或編碼上,雙方都擁有平等的決策權利。

——《構建之法》第四章 4.5.4 如何結對程式設計

領航員和駕駛員互相輪換角色、擁有平等的決策權利,是結對程式設計的重點所在。領航員審閱文件、思考架構,駕駛員進行編碼和單元測試,這樣的分工固然能夠節省整個專案的開發時間,但程式的整體編寫時間增加了。公司是否會拿兩份工資去鼓勵結對程式設計呢?

要做到高效率、高質量的結對程式設計,首先兩名程式設計師的水平不能差異太大,否則角色的交換可能會讓水平較高的一方難以接受。譬如有一個自己十分鐘就能寫出的版塊,隊友卻要用半個小時一點一點堆出來,堆出來的程式碼思路不清、碼風混亂、bug 頻出,把一架飛機駕駛成拖拉機;當領航員時,駕駛員更像是一種程式碼的表演,領航員理解不了程式碼的時候?這樣的隊怕是很難結起來。

其次兩名程式設計師的性格要互補,思路要一致。無論兩人開始的萌芽階段有多麼彬彬有禮、避免衝突,在接觸瞭解後難免會產生大量的矛盾衝突,這種矛盾衝突往往來自不同的教育體系、相異的思維模式,因此也難以站在對方的角度思考問題。

所以固然結對可以互相督促、互相學習、理清思路,但也需要兩人的配合與默契,找到這樣兩個人的難度是否和找物件的難度比較接近呢?而且,管理者也往往想用一份工資去換一份工資的效益。

Pair Programming(give it a rest)》一文中,作者表示:“不要認為這(結對程式設計)有多麼具有革命性,你只要集中注意力,努力思考,深入問題,設計解決方案並進行正確的實現和系統的測試”。

結對的一個主要目的便是讓程式碼出錯的概率降低、每一行程式碼都有其存在的意義,不留渾水摸魚的機會,但書中提到過的程式碼複審也能夠達到類似的目的,而且能夠多人蔘與,聲音來源更廣,在當下 github gitlab 等程式碼交流社群的 pull request 也十分便捷。事實上,瞭解過的部分周圍公司也都因為種種原因沒能開展結對程式設計。結對程式設計最早也是源自兩個程式設計師趕 DDL 不得不做的一個“創舉”,僅僅是契合當時的事件背景的一種解決方案。結對程式設計在當下的應用性究竟如何?

問題三:課程中例會是否仍要“每日”

衝刺期間,每天要開一個每日例會(Scrum Meeting),團隊成員大多站著開會,所以又稱每日立會。大家依次報告: 我昨天做了啥,我今天要做啥,我碰到了哪些問題。

每日立會強迫每個人向同伴報告進度,迫使大家把 問題擺在明面上。同時啟動每日構建,讓大家每天都能看到一個逐漸 完善的版本。

——《構建之法》第六章 6.2 敏捷流程的問題和解法

“例會”是為了報告在一段時間內團隊工作進度和任務完成程度,從而進一步安排下一步發展目標的會議。但在當下,同學們不僅要完成計網、計網實驗的作業內容,還要完成大家自己選各種各樣的一般專業課、通識課的相關部分。在這種無法做到每天時間充足的情況下,我們在例會中難免也會出現報告“我昨天沒寫程式碼,我今天仍然沒寫,我沒碰到困難”等“狗熊級程式設計師”的回答。

當場景、功能都計劃好的時候,要給員工足夠多的時間,讓他們投入到工作中去,而不要經常打斷他們。要儘量減少非開發時間,不要動不動就開“全體會議”。

——《構建之法》第十一章 11.5.1 閉門造車

敏捷開發中,頻繁的小會是為了及時同步全域性資訊,讓團隊清楚個人的進度情況、困難之處,同步資訊的主要目的在於增加整個團隊的效率,即通過隊友的進度、壓力、困難來 push 每一個人。我相信我們的組員都不會習慣於渾水摸魚,但人的精力是有限的。在這種課程壓力下,在例會會佔掉課餘時間一大塊蛋糕時,同步資訊的例會是否還有必要保持“每日”?如果適當地增大例會之間的間隙,並適當規定每個人的貢獻與任務,是否也能夠在給組員足夠壓力的同時允許我們自主安排其他課程與軟工專案的時間?

問題四:NABCD 模型是否變為 BD 模型

那我們怎麼才能按部就班地分析需求,然後有條理地說服別人?大家可以參考NABCD模型。

  1. N(Need,需求)
  2. A(Approach,做法)
  3. B(Benefit,好處)
  4. C(Competitors,競爭)
  5. D(Delivery,推廣):在實際專案中經歷多次的NABC之後,許多人意識到這個框架還應該加一個元素D:Delivery。

——《構建之法》第八章 8.4 競爭性需求分析的框架

先舉幾個例子,看一下這幾個當下成功的軟體是如何做的:

QQ:抓住了手機剛興起的使用者社交需求,在 ICQ 的基礎上敏銳地嗅出使用者的滿足點,開創了離線傳送和隱身登入等功能,是其主要的成功原因。

微信:微信的發展是逐步抓住市場契機。先是抓住手機發展的契機,不斷更新滿足使用者需求的功能,比如“附近的人”,漂流瓶、多種語言、手機號的支援,以及便捷的檔案傳輸,甚至一些細節如翻頁不卡的處理;再是抓住二維碼的契機,率先新增“掃一掃”功能並且與支付功能掛鉤;再是對於叫車、外賣、買電影票等的支援(所謂“全家桶”繫結),逐漸發展到數億使用者。

網易雲:首先抓住了各種能夠滿足使用者的細節,比如與大量音樂人的合作、乾淨利索的排版、無關評論的刪除、智慧推薦,甚至是播放歌曲時旋轉的膠碟,都能夠給使用者一種 Benefit。但是作為一款音樂軟體,其遷移成本固沒有社交軟體那麼龐大,但重新進行歌曲的收藏仍是與 QQ 音樂等拉開差距的重大阻礙。網易雲通過完美的市場推廣營銷手段,從 Uber 叫車券、音樂明信片,到一些滿足使用者虛榮需求的“聽歌測試”、引發廣大輿論的地鐵 Touch 行動,以及有格調的公關,成功地打下了音樂的半壁江山。

bilibili:最初的競爭力在於高質量視訊內容和彈幕,以及大量因缺乏監管而存在的內容,以及極其重要的一點,無廣告。這一個競爭優勢貫穿 b 站始終,是其核心競爭力。近年來 b 站通過與央視、共青團等多方的合作,發展成了一個受眾範圍較大的規範視訊軟體。

觀察這幾個軟體的做法,擴大使用者量的主要因素就在一內因(B)一外因(D)。滿足使用者基本需求的 N,在 QQ 那時或許是一種開創性的做法(其實當時也不算基本需求了),在現下已經是一種企業生存的必需要求了,單單滿足這一需求是無法談論競爭力的。而這些軟體之所以能夠取得如此規模使用者量,更多的其實是滿足使用者“興奮需求”的 B,以及市場推廣或者捆綁的手段 D。同樣地,許多被騰訊推廣的遊戲,其設計、內容等都遠遠落後於同型別其他遊戲,但無論是使用者量還是知名度往往都能夠吊打其他遊戲。所以其實 NABCD 模型在當下是否轉化成了 BD 模型?軟體的成功到底是軟體工程還是市場營銷?

問題五:設計文件如何平衡嚴謹性與複雜性

規格說明書(Specification)簡稱Spec,分為以下兩種:

  1. 軟體功能說明書(Functional Spec),主要用來說明軟體的外部功能和使用者的互動情況(把軟體當作一個黑盒子)。
  2. 軟體技術說明書(Technical Spec),又叫設計文件(Design Doc),主要用來說明軟體內部的設計規範(把軟體當作一個透明的箱子)。

——《構建之法》第十章 10.3 規格說明書

書中寫到,軟體技術說明書主要用來說明軟體內部的設計規範。一個參考資料中也說到,需要包含每個模組的詳細設計(輸入,處理,演算法,輸出)的軟體詳細說明書。

技術說明書包含各個模組的先驗條件(假設)、後驗條件(功能)、副作用和異常,容易聯想到我們在物件導向設計與構造中,學習過的 JML。JML 是用於對 Java 程式進行規格化設計的一種表示語言,提供了一個比較嚴謹的描述規格的方法,為程式自動化測試提供了可能。但其邏輯的嚴謹性是由描述的複雜性保證的,一個簡單的方法的規格可能比實現還要長几倍;這不僅增大了閱讀量,增加了閱讀的複雜度,同時還增加了編寫的難度

因此,設計文件應該如何平衡嚴謹性與複雜性?真實軟體工程中,是否用到了類似 JML 的規格語言進行功能描述,如果是,如何保證編寫的正確性?如果不是,如何保證規格的嚴謹性?

問題六:整合測試應不應該在模組構建時進行

問:應該什麼時候做整合測試?是不是越早越好?

答:原則上是當一個模組穩定的時候,就可以把它整合到系統中,和整個系統一起進行測試。在模組本身穩定之前就提早做整合測試,可能會報告出很多Bug,但是這些由於提早測試而發現的Bug,有點像汽車司機在等待綠燈時不耐煩而拼命地按喇叭——也就是說,有點像噪音。我們還是要等到適當的時機再開始進行整合測試。

——《構建之法》第十三章 13.2.7 場景/整合/系統測試

書中提到,儘量在一個模組穩定後,才與系統進行整合測試。

但實際應用中,比如開發人員在做使用者模組,在一天的衝刺開發中只開發出了使用者登入登出、修改密碼功能。那麼類似“使用者登入-修改密碼”這樣的整合測試放在這裡未必沒有必要,因為它可能涉及到整體模型的構建問題,如果在整個使用者模組都做完之後再進行整合測試、發現模型需要重構時,這或許已經浪費了大量時間,這便是一個潛在的“風險”。

根據自己之前 Ruby on Rails 開發的經驗(雖然只是個人開發),在實現一個功能後立即對其進行測試是最保險的辦法;推廣到專案,我認為整合測試可以在與開發進度溝通的基礎上適當進行,而沒必要在一個模組徹底穩定後再進行。老師和助教們如何理解這裡的差異?

問題七:事後諸葛亮會議為何不能融入每日例會

一個里程碑結束了,接下來怎麼辦?團隊有什麼經驗教訓?產品怎麼才能做得更好?我們常說“軟體的生命週期”——這個軟體開發的週期結束了,生命也結束了。我們能不能像醫學的屍體解剖一樣,把這個軟體開發的流程解剖一下?解剖的過程可以叫:Postmortem,Retrospective,Review,事後諸葛亮會議,等等……

——《構建之法》第十五章 15.2 釋出之後——事後諸葛亮會議

觀察到其中許多影響進展的問題都是源於資訊不同步、不準確,進行交流時不夠全面公開造成的。而這樣的事後諸葛亮會議的目的無非在於:1. 暢所欲言積極參與,2. 尋找短板並分析原因,3. 根據各種聲音分析合適的決策。那麼這樣的覆盤與平常的會議有何不同?為什麼不在平時的會議就把這些問題解決掉,做到盡全盡美呢?

問題八:70%的創新者都是跨領域創新嗎

這個想法看起來沒什麼錯,我們不就是為了成為某個領域的專家,才來上學,拿學位,希望拿到學位之後成為專家,然後再開始這個領域的創新?但是統計資料表明,70%的創新者說,他們最成功的創新,是在他們的拿手領域之外發現的。

——《構建之法》第十六章 16.1.5 迷思之五:要成為領域的專家,才能創新

統計資料表明?70% 的創新者?有具體的資料來源嗎,創新者的來源又有哪一些?

某重點高中 A 百年建校史裡突然有個人沒考上大學,試問該高中是否就不算重點高中了?我可不可以這麼說:統計資料表明,在 A 高中,30% 的學生無法考上大學。哪裡來的 30%?統計了一下,原來他們班最後一排一共三個人。

不是說個例不能做規律層面的評論,有些情況是可以的。但是把個例往規律上引導,這是上綱上線的手法,某時期常用,文化人不恥。感情牌再怎麼打,也不能借題發揮;個例再誇張,也不能擅自誇大。

扯遠了,再回來說跨領域創新。所謂拿手領域之外,其實是對該行業沒有那麼擅長,但自己的經歷與該行業存在著一些關係;沒有專業的專家瞭解程度深,少了一層思維定式,可以從不同的角度看待問題、進行創新,但這並不表明跨領域創新更多更容易,這有點像程式碼開發者和測試者的感覺。書中提到的 Tim Berners-Lee,便是如此。

另外,書中描述的“隨身聽”成功案例,專家們不僅僅是認為隨身聽沒有市場,還

做了多次市場調查,來證明大眾不會喜歡“只能放音樂,不能錄音樂的小玩意”。

但在這樣的基礎上,盛田用直覺推動研發、辭職要挾,最終取得了成功,其中充滿了冒險主義和“賭怪”的氣息,書中想通過這樣的例子說明什麼呢?外行的直覺比內行的調研計算要準確?似乎不見得。

因此,與其做著青天白日夢想著自己有朝一日能隨便找個行業搞出個創意來,不如腳踏實地幹好分內的事、增加專業的知識儲備和市場需求的瞭解,這才是拿出創新產品的正道。

問題九:如何判定“價值觀”

如果業績很好,但價值觀不太對路(不太聽話?),則是一條野狗。要堅決清除,不然功高震主……

——《構建之法》第十七章 17.3 績效管理

價值觀固然很重要,關係到個人利益與集體利益的合理分配,但如此主觀的一種方面如何進行判定與評估呢?

以阿里為例,它這樣評估的:客戶第一、團隊合作、激情、誠信、敬業、擁抱變化”六脈神劍“,進行案例分析和績效談話。舉個極端點的例子,對於一些一心悶在程式碼上的、表達能力較弱但業績能夠非常突出的程式設計師,他能夠做到以上幾點,但由於表達能力弱、又想不出價值觀對應的案例,那這種人就應該當做“野狗”涮掉嗎?把一條潛在的藏獒當做野狗逐出公司,這無論對公司還是員工,都是一個極大的損失。

題外話:其實自己比較怕以後成為這樣的人,因為赧於表達,常常在需要表達的時候犯嘀咕、怕笑話,不敢去表達自己的想法。學長因此建議我去當 PM,但這涉及到團體利益,一個 PM 要是沒有表達和決斷能力,註定在開始的萌芽和磨合階段就當不好,這影響的是整個團隊的利益,而這不是我希望的。

調研原始碼版本管理軟體

相同之處

都是供用 Git 版控的專案使用的託管服務,有建立、上傳、修改、fork、PR、合併專案的功能,支援 markdown,能 follow 膜拜大佬,能發 Issue,而且都有組織 Group,可以以小組為單位進行程式碼管理、專案集中規劃、程式碼協作、測試和部署。

不同之處

Github 是最早供用 Git 版控的專案使用的託管服務,有一億多的倉庫數量,使用者基數大,目前全球最大的開源社交程式設計及程式碼託管網站。由於其不僅社群龐大,還“提供了訂閱、討論組、文字渲染、線上檔案編輯器、協作圖譜(報表)、程式碼片段分享(Gist)等功能”[1],對於協作專案開發、以及專案的儲存,Github 無疑是首選網站。免費區間,單個專案不能超過 1G,單個檔案不能超過 100M。就是 Github Desktop 做的有點屎,用起來不如直接 git 好用。

Gitlab 是用 ROR 做的,而且開源。“與輕量級目錄訪問協議整合,允許在Internet上定位和訪問各種資源”。缺點是速度較慢,而且許多 UI 比如 CI 都做的不如 Github 好看(個人吐槽)。私有庫免費,這是它的一個重要優勢。社群版免費區間有 10G 限制,非常適合自託管,這也許就是 OO 改革對準了 gitlab 的原因之一吧。

BitBucket 功能比較少,比較適合小型團隊,小型團隊提供無限免費私有倉庫;不僅僅支援 Git,還支援 hg 專案。被 Atlassian 收購,所以對於這家公司的服務(SourceTree,Cloud9 等)需求量較大的大型企業適合選擇 BitBucket。

調研持續整合/部署工具

Gitlab CI

  1. 使用 oo-2020 pre2task6 的內容,本地用 maven 建立 project,並編寫一個簡易的 JUnit4 測試:

  1. 配置 Gitlab CI,上傳到 Gitlab 觸發 CI:

  1. 設定 cobertura,並捕獲測試覆蓋率:

  1. 增加 readme,檢視覆蓋率:

Gitlab CI 是筆者之前用的比較多的一個 CI/CD 工具,在同一個映象上實現多個 stage,不同的 stage 之間可以通過 artifact 進行資料交流。

Github Action

  1. 將上述程式碼轉移至 Github,根據官方文件編寫 .github/workflows/maven.yml ,即可觸發啦!

  1. 配置 codecov,並相應更改 pom 中 cobertura 的格式,順便更改 README,就可以實時顯示覆蓋率啦!

Github Action 則是可以在多個平臺上、不同環境下執行的工具,每一個 job 都是一個不同的環境,這個環境需要用 runs-on 說明平臺型別,需要時還要使用 actions/setup-java 之類的 action 進行環境的配置。由於在不同虛擬環境上跑,因此需要特殊的 actions/upload-artifact 以及 actions/download-artifact 進行 job 之間的資料傳輸,與此同時,使用頻率較高且不常更改的檔案/資料夾還可以使用 actions/cache 進行快取。

題外話:之前由於思維慣性,認為 Github Action 不同 job 也是在同一映象上跑的,結果 build 裡配置了 java-version: 1.8 ,而 test 中沒配置,導致不存在 $JAVA_HOME/lib/tools.jar 而報錯。原因是虛擬環境不配置時預設為 jdk11,而從 jdk9 以上,tools.jar 就已經被刪掉了,費了好長時間。(╯‵□′)╯︵┻━┻

在應用的過程中,能看到在輸出內容較多的時候,Gitlab CI 不會卡頓,而 Github Action 上下拖動時會產生卡頓現象;但 Github Action 的速度較快。

至於技術、產品、需求方面的特性和優劣,筆者只是試用一兩天總結不太出來,從相關資料摘要分析瞭如下幾點:

Github Action Gitlab CI
獨特的功能 可能實現最佳GitHub整合 AutoDev Ops /允許將程式碼管理和CI放在同一位置
併發作業 允許併發作業,甚至是多平臺。 yml輕鬆配置要並行執行的作業
分散式構建 沒有具體提及,但是鑑於任務可以在多個平臺上執行,因此分散式構建很有可能也可用。 支援
容器支援 Linux,mac,Windows,或直接在VM中執行 預設Docker容器登錄檔已整合到GitLab中
管理支援/自託管/報表 即將推出,尚不可用。 支援
構建管道 工作流 通過YML配置檔案定義
生態系統 followers 多,已經擁有各種可用的預製工作流
整合 通過共享的第三方工作流程(AWS,Azure,Zeit,Kubernetes等)使整合成為可能 整個GitLab都有大量第三方整合
可通過API或其他方式使用API 目前尚不清楚,但假設GitHub Actions將與GitHub GraphQL API整合(可用的更成熟的GraphQL API實現之一) 提供REST API和(新的)GraphQL API,並計劃僅在以後維護GraphQL API。至少在CI需求方面,允許執行幾乎可以通過介面執行的所有操作。

可以看出,Github Action 作為一個新興的工具,有一定欠缺之處的同時,在生態系統上有著固有的領先優勢(Github 上進行 PR、Issues 等團隊協作方面更加便捷,因此 Github Action 的使用物件也比較廣),也在實現機制上與 Gitlab CI 有著很明顯的區別(上文所述),同時前端比 Gitlab 做的好看(符合筆者審美!)。當下 Gitlab CI 支援自託管、管理和報表,而 Github Action 無法支援報表等內容。

感覺 Github Action 最牛的地方在它支援直接使用其他倉庫的 Action,並且允許自定義引數。上面提到的使用 codecov 測覆蓋率就是用 codecov 這個專案的 Action 進行的,也就相當於把庫搬到了 Github 上。

因此筆者認為,Gitlab CI 會在較長的一段時間由於其功能上的方便保持使用量的優勢,而且比較適合小規模團隊開發;而 Github Action 自從建立到現在一直在高速發展,在使用者基數大、功能前景很強、(以及前端做的舒服)的條件下,有機會超越 Gitlab,而且許多大型的專案現在都在 Github 上開源,對於大型專案有著不小的優勢。

相關文章