軟體系統的設計和實現

2016-04-29    分類:程式設計開發、設計模式、首頁精華0人評論發表於2016-04-29

本文由碼農網 – civic5216原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

1.引言

“Hello,wolrd!(世界,你好!)”,我寫一些技術性的文章已經有一段時間了,最近一段閒暇時間我沒有休息,而是思考我可以寫一些東西,可以對朋友們提升開發技能提供一些可能的幫助。自從來到 Stratum Security 公司工作後,我已經從無到有建立了幾個新系統並做了好些設計、文件、重構工作。我認識到將這些事情記錄到一處可能有些用處,能夠幫助減少為了獲取這些概念去閱讀不同文章的壓力。

2.目標人群

我一直想要分享一些經驗,分享這些年從程式設計和最近工作中學到的經驗,但是在部落格中我不想去深入到程式設計的細節。我寧願寫寫短小,精煉的文章,能夠給讀者灌輸一些領悟,這些領悟來自於我構建系統的經驗。因此,這篇文章針對於我所謂的“中級”開發人員,你可能會感悟最深。中級開發人員是這樣一種人,他已經對編碼過程相當熟悉了,開始思考一些抽象東西像是如何編寫軟體使得它在生產上是可靠的,既能夠靈活變通,也能讓同事理解的程式碼。

你將要讀到的東西是有些偏向的,因為它們是根據我個人喜好而未考慮類似程式設計正規化的東西,我拒絕盲目接受教條的流程和方法。我這裡訴說的東西受到了工廠最佳實踐,包括那些工作中有名的優選的方法、技術,更加受我自身經驗的影響。當然了,我覺得對我的文章你要秉持懷疑態度。如果你是物件導向的狂熱分子,你最好還是現在就離開此網頁。

3.解決問題和設計

假定你已經程式設計有一段時間了,你可能內心已經清楚意識到從有一個關於寫東西的點子直接跳到開始寫編碼是一個非常不好的做事方式。

上面的流程在實際生活中不可能生效。至少在你寫一些程式碼給同事看的時候不會發生。因而,我們要去探尋的是怎樣去獲取“一些神奇事件”的部分,將它拆解—彩虹,獨角獸,等等—直到我們擁有了專業的方法去確定要構建什麼以及怎麼去構建它,這樣我們就能和團隊成員溝通,也能減少未來讓我們犯頭痛的次數。我保證最後那些東西都是有趣和有意思的。我們的目標並不是將軟體開發歸結為一個完美的科學(我確信如果你這樣做一定會失敗的),而是辨析一些能指引我們通過混亂迷霧的策略。

3.1.自頂向下的方法

在涉及到解決問題類似的事情時,這句話你可能已經聽到過多次,那就是“自頂向下”或者可能是“分而治之”。後一個名稱更加有價值,正如它所建議的,這是一個相當簡單的解決問題的策略,它包含以下幾步:

  1. 辨識你的核心問題(例如:我想構建一個能做X的API)。
  2. 將已辨識的問題分解為邏輯部分(例如:我需要一個網路伺服器,一個資料庫,以及一些端點)。
  3. 將以上問題更深入的分解為細緻的邏輯單元,提些問題類似“這個單元包括什麼東西?”、“這個單元怎麼工作”,“為什麼我需要它”,等等。
  4. 反覆重複第三步,直到留下一些小問題,而對怎麼去處理它們你有非常清楚的概念。

上面的例子顯示了我們該如何開始分解想法去構建一個應用,這個應用能幫助潛在的動物領養人在我們的動物收養中心去找到他們心儀的寵物。第一件事情是,辨識涉及到使用者(寵物將來的主人)和管理者(那些運營收養中心的好心人)的動作行為。從這裡開始,我們分解每個步驟成為一些技術性的要求。我們一直進行這樣的分解動作直至最終留下了一些問題,這些問題我們可能能夠很快開始編碼去解決。

執行那個我們剛剛做的自頂向下的分析動作可能也是你的構建系統中面對主要“利益相關者(客戶)”時處理的事情。這個詞是設想的敏捷詞語,用來描述這一類人,要你把給他們構建的東西放在第一位,或者是一些代表你未來使用者的人。然而,這可能不是你想要給開發者團隊帶去的東西,這些開發者要你去分配任務。實際上,你的問題沒有一個能夠分解成如此清晰的層次結構,接下來我們會看到這是件好事情!現在你需要做的是掃描你已經辨識到的所有需求,問題,以及潛在的解決方案,努力提出你需要去構建的一系列事情,這裡使用了一些相當技術性的行話。

3.2.交流你的設計

在那個寵物收養的例子中,可以想象到我們會有一個類似下面步驟的列表:

  1. 使用者和管理者註冊和會話處理;
  2. 動物資訊管理;
  3. 使用者喜好管理;
  4. 動物查詢和動物-主人匹配;
  5. 預約排程。

這些個是有些厚重的,相對獨特的高階別技術問題,這些問題我們的應用一定會遇到。我們可以想像著將這個列表分離開,分配一個或多個子專案到小的開發團隊。現在我們已經大體知道我們需要構建什麼內容,是時候去確定我們怎樣去構建它,或者,更精確地說,最終產品應該怎麼執行。

方盒箭頭表示的流程圖

這是另一個很技術性的術語,描述了上面我們建立的那種圖表。不是去努力分解問題成子問題,這裡的思路是,我們努力繪製系統元件之間的資料流。這正是我喜歡的方式,在一個高的層次上描繪系統,因為它可以讓我視覺化那些元件,能夠立即開始規劃如何去敘述和構建每個元件。

上圖是我們過去設計的一個應用的架構概覽。你能看到那些需要我們去構建的主要的子系統,以及它們之間和使用者之間是如何互動的。每個灰色方塊是能夠獨立實現的子系統,當構建的時候可以連線到其他子系統。稍後將詳細介紹。

這種圖表不好的地方在於,他們太抽象了,有時候努力分解系統成為類似這樣的元件會弊大於利。例如,如果你在構建一個REST API,用這種方法你可能不會收穫很多。

使用者故事

另一個我要描述的抽象交流方法就是在軟體工程課程中經常被教授的方法。但是它可能並未在程式設計(自學)教育指定的書籍中出現。使用者故事正如字面上所顯示的含義。它們是些簡短的,一段一段的故事,關於使用者與我們的系統之間互動來達到某個目標時採取的步驟。通常情況下,你會對前置條件做些假設,例如,假定使用者已經登入了。

這裡是一個使用者故事的例子,用於描述上面那個動物收養例子中預約排程的行為。

  1. 登入後,使用者點選“上一次檢視的動物”;
  2. 使用者從他們最近檢視的動物檔案列表中選擇一中動物;
  3. 使用者點選“安排一個預約”;
  4. 使用者從日曆中選擇一個時間點,點選“提交”。

就是那麼簡單!當然了,如果你需要處理一些有條件的例項,你可以在步驟下建立一個子列表。使用者故事具有如下優勢,它能全面描述一個完整特性是如何工作或者一個要求是怎樣被滿足的。它提供了一些你能顯式測試的例子,可以幫你去驗證一個特性是實實在在存在的。不好的地方在於,對每個功能的關鍵點寫使用者故事有很多的工作量,依賴於你想多細緻。很難證明通過這樣一種方式你已經覆蓋了所有東西,然後你可以去有意圖地開始分配工作。

4.實現

現在你已經獲取了系統的所有需求,充滿希望地將每個你需要解決的問題分解成了小塊工作,你準備開始構建軟體。但是,打住!現實世界中構建軟體不僅僅是蹲下來寫程式碼,就如同建房子不僅僅是將一塊一塊磚疊放到另一塊上面!

4.1.文件

當我們回看方盒箭頭(或架構)圖表時,我們經常關注裡面的規模大的模型。我們這麼做是因為它們代表了這些東西:元件、我們的資料庫、使用者,等等。還有這些模型之間的箭頭,如果圖中沒有更重要東西!箭頭出現在那裡不是用來顯示的,它們告訴我們什麼元件需要與其它元件進行互動,更會包括兩個元件之間是如何互動的資訊,不管是顯式還是隱式的提示出來。這就是文件的來源。

文件型別

  1. 關係圖 用來視覺化資料庫中資料是如何建模的;
  2. 架構圖 顯示一部分軟體的子系統,以及它們之間是如何作用聯絡的;
  3. 使用者故事 解釋了使用者如何完成一個任務;
  4. API規格說明  描述了服務/模型/物件/…暴露給使用者的方法,它們期待什麼輸入,產生什麼輸出。
  5. 程式碼註釋 當考慮功能中某部分的實現時給出的註釋,主要是方便別的開發者和你自己將來定位程式碼。

當你編碼時,這裡僅僅是需要編寫的文件中的一少部分。這裡的中心主題是,文件用來與其他開發者和將來的你交流編碼意圖。換言之,文件用於解釋為何一些東西使用一種特別的方式執行,以及它是怎麼執行的。給定的文件應該敘述多少東西嚴格取決於內容。可能你不需要在程式碼註釋中重寫你的使用者故事,在使用者故事中也不需要拷貝函式符號。然而,往API規格說明中拷貝函式符號是比較合適的。

最理想的是,你應該在構建東西或寫測試程式碼之前輸出文件。這並不是說你要提前寫完所有文件,但是你應該把那些你實際構建之前需要準備的文件寫完。

  1. 你能夠提交文件給你的同事或領導以便評審從而能夠獲取有價值的反饋意見;
  2. 當同事有可用文件時,可以使他們方便開始自己負責的編寫相關文件和編譯子系統的工作;
  3. 文件代表了測試和實現之間的一份合約,這點待會兒還要細說。

4.2 測試

雖然對測試驅動開發以及相關細節持懷疑態度,開發者社群對另一個觀點看上去已經取得了一致,即程式碼需要有測試伴隨。我個人也贊同測試先行的開發策略。

文件驅動的測試先行開發

我上面說的話:“文件代表了測試和實現之間的一份合約”,是因為文件充當了一個非常合適的頂樑柱,每件事情都能依賴它。一個關於TDD非常普遍的抱怨(這點我也有深有同感)是,在實現任何東西之前,很難知道要測試什麼,而實現卻又趨向於驅動程式碼一級的大部分設計和決策。你的文件需要足夠完善,使得你能夠簡單地編寫測試程式碼去驗證實現是否滿足了文件中描述的那些條件和介面。建立這個之後,你應該會感到自信滿滿,因為測試能夠告訴程式碼是否執行成功,是否它執行了你文件中敘述的情況。

測試也提供了另外一個對你的程式碼庫有價值的特性,一個支點。當你需要修改實現的時候,不管你是新增一個新特性還是修改某事物的工作模式,你應該再一次開始更新文件,保證它是最新狀態。做完之後,你能夠去重構或新增新測試程式碼去檢驗你的程式碼符合新的規範。即使你的實現未能通過測試,失敗的案例也能夠作為修改實現的一個指導。你也能修改實現而不至引起混亂。

4.3自底向上的開發

採用自頂向下方式去解決問題是一個非常有效的法子。因為我們最開始就清楚我們的工作要在哪裡結束,所以我們能將最終目標分解成一塊一塊直到獲取到今日我們能解決的問題。許多人在開發中努力應用自頂向下的方法,特別是那些很喜歡OPP,或物件導向程式設計的傢伙。正如我在部落格開頭所說的,我對OOP是有嚴重偏見的,而且我認為自頂向下開發不如自底向上開發有效。自頂向下開發方法會失敗,因為它假設你清楚最終構建的東西是什麼樣子的,在你還未開始構建之前就清楚。從我和許多其他人的經驗看來,即使有個正式的規格說明書,這也是非常罕見的。當出現改變或過去問題有了新的解決方案時,很難轉換你的物件層級也難以往中間層插入物件。而且,以一種“首先抽象,最後授權細節”的方式開發的軟體經常是很複雜的(是ObjectFactoryFactories模式吧,有誰清楚?)

針對自底向上開發的一個常見爭論是,它是一種無結構,無組織的,你會寫一些最終會扔掉的程式碼。事實上,自底向上開發正好是無結構無組織的反面,另外,刪掉程式碼也是好事。

在自底向上方法中,你開始編碼去解決系統中需要處理的最低階的操作。在那個動物收養例子中,這些操作可能是SQL查詢資料庫操作,資料庫用於管理動物資訊、使用者資訊、喜好資訊、預約資訊。接下來就是要構建一系列的抽象集合,像是你在程式語言中使用的函式,更低階操作的呼叫,清除操作,輸入輸出結構。從這裡開始構建更高階的抽象,例如模型類,然後呼叫你編寫的模型方法構建API端點。

將低階操作彙總成高階操作的行為通常被稱為構造(composition)。正如數學中你學到的如下兩個函式是如何構造的一樣,

(f.g)(x) = f(g(x))

軟體中構造也是一樣的原則執行。當然了,既然我們是討論程式碼,我們在其中會做很多額外的工作,但是核心思想是一樣的。自底向上方法中需要注意的一個關鍵點是,當你的目標只是建立儘可能多的抽象層,而每一層只會呼叫到它下面的低階層中定義的功能時,你有很多自由度去構造功能,只要你認為合適的方式都行。鑑於層之間的交叉衝突會導致混亂的程式碼,我認為,這個方法無論如何也會給你提供相當大的靈活性。當你要改變某些東西時,你可以很方便地新增有用的功能到你正在工作的層以下,以便構建一個適當抽象的解決方案。

測試時,自底向上方法也很容易去處理。自頂向下方法經常需要使用一些技巧像是在更高階抽象層用模擬來模擬低階層的實現,而自底向上方法允許你在進入抽象之前測試加固你的低階層程式碼。這就意味著你能有效地在每一抽象層進行單元測試,使用整合測試去檢驗你的建構函式是否如預期一般。如果你以一種測試優先的風格進行開發,或者至少全面測試你構建的每一層,你也會很有信心說,你構建的下一層就如你用來構造低層級功能的程式碼一樣有同樣少的錯誤。

5.結語

下面幾點將我之前敘述的內容總結成一個流程,在你的下一個軟體專案中你可以直接使用此流程。

  1. 與你的客戶(利益相關者)交談,盡力去獲取有關特性和需求的儘可能多的資訊。
  2. 將每個特性和需求分解成子任務,子子任務直到你有了可辨識、可執行的動作塊。
  3. 確定和文件化你要構建的系統中的元件來解決你之前概述的問題。
  4. 文件化你的設計,解釋每個需要實現的元件、介面。寫使用者故事幫助理解細節。
  5. 從其他開發者和客戶那裡獲取反饋,確保你已經掌握了每件需要做的事情。
  6. 在對上述文件做了任何必要改動之後,建立你的開發環境,開始建立低階功能的測試用例。
  7. 在實現了系統中最低階的工作單元之後,為抽象層編寫測試用例並實現它們。
  8. 重複5-7步驟直到最後完成。在其中你可以引入其他你們團隊覺得合適的開發方法。

希望我這篇文章能給你,可能是一箇中級開發者,提供一點關於現實世界中軟體工程是如何執行的概念。我們已經揭開了之前提到的“神奇事件”,但是我希望你接受這一事實,我們這裡討論的創造性的和社會化的流程一直會是有挑戰性的和令人滿意的。

最後,將高水平軟體工程師與“好的編碼者”區分開來的是與團隊溝通規劃的能力,理解需求並轉換成為技術模型的能力,以及嚴格遵循開發流程的能力。

譯文連結:http://www.codeceo.com/article/software-design-implementation.html
英文原文:The design and implementation of software systems
翻譯作者:碼農網 – civic5216
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章