10個現代軟體開發過度設計上的錯誤

zcfy發表於2017-01-07

  有些事情總是會增加的:兩顆星星之間的距離,宇宙中的熵值,還有客戶對軟體需求!很多雜誌都說不要過度設計,但是並沒說為什麼,這裡列舉了十條清楚的觀點。

重要提醒: 以下的一些觀點像“不要濫用泛型”常常被誤解為“不要使用泛型”,“不要創造不必要的包裝類”被認為“不要用包裝類”等等。我僅僅是在討論過度設計而不是在討論要如何養成好的程式碼風格。

  1. 工程師比客戶更聰明

  工程師通常認為自己是最聰明的因為萬物都是他們創造的。這是使我們過度設計的第一個錯誤。如果我們計劃100件事情,商人通常會提出我們從來沒想過的第101事情。如果我們解決了1000個問題,他們將會反饋10,000個問題。我們以為所有都在我們的掌控之下,然而實際上我們連我們具體的方面都不知道。

  我們以為所有都在我們的掌控之下,然而實際上我們連我們具體的方面都不知道。

  在我15年的開發生涯中,我從未看到一個客戶減少軟體功能的需求,他們的想法通常會偏離原來的初衷,對於客戶這是可以理解的,這也不是客戶的錯。

TL;DR — 客戶總是對的。

  Tip: 如果你沒有時間讀本文的一下部分,這一點其實已經足夠了。


  2. 業務功能是可以重用的

  當客戶提出越來越多的功能需求(這是我們都知道將會發生的)我們通常做出這樣的反應:

  我們試著儘可能地分組和概括邏輯。這就是為什麼大多數MVC模型的系統到最後就剩下臃腫的模型或者臃腫的控制器。但是正如我們早已知道的,客戶的需求只會增加,不會減少。

  所以替代的,我們應該這樣反應:

  在系統中,共享功能的重用和抽象往往隨著時間的推移而趨於穩定。當功能增多的時候系統會保持穩定或者會相對的輕量。如果不這麼做的話,系統很可能由於太過臃腫而崩潰。(更可能會被重寫).

  舉例: 我們為以前的客戶建立了一個使用者配置檔案系統。起初我們認為功能需求與以前的很相似的,所以採用了重用的CRUD(增加(Create)、讀取查詢(Retrieve)、更新(Update)和刪除(Delete))控制器。但是到最後卻產生13種不同的開始流程——初始的連線,首次註冊的資訊填寫,編輯資訊等等,這使得重用的元件根本起不了作用。相似的,順序檢視和順序編輯流與實際的排序流本質上是不同的.。

  在水平劃分業務功能的時候先試著垂直的劃分。在各種情況下考慮——隔離服務,基於中繼的服務,特定於語言的模組等等。這也對從一個功能轉換到另一個功能十分方便。否則的話當系統的一部分發生改變的時候將會系統將會變得很複雜。

TL;DR — 功能的隔離好於功能間的組合

  Tip: 在你寫的程式碼中找一個面向外部的功能(Endpoint/Page/Job等)看看其他人要理解多少個選擇語句?


  3. 一切都是通用的

  (有時候這一點與前一點可以結合,但是它仍可以應用於獨立的專案)

  • 想要連線一個資料庫?寫一個通用的Adapter
  • 查詢資料庫? 通用的Query
  • 傳遞引數? 通用的Params
  • 建立引數? 通用的Builder
  • 對映返回值? 通用的Data Mapper
  • 處理使用者請求? 通用的 Request
  • 執行整個事件? 通用的 Executor
  • 等等

  有點工程師就是這樣,不解決業務上出現的問題而要浪費時間去尋找一個完美的抽象類,答案是非常簡單的。

  什麼是完美的抽象類?

  設計總是為了解決不斷變化的現實世界的需求。因此即使我們奇蹟般的找了一個完美的抽象類,它仍然有一個有效期的標籤 #1 — The House wins in the end. 今天最好的設計其實是不設計,這有一篇震驚的文章 寫容易被刪除的程式碼,而不是可擴充的.

TL;DR — 複製比錯誤的抽象更好

  相反的,複製有時是正確的抽象。當我們看到系統的某些部分具有相似的程式碼,一個好的抽象就出現了 複製暴露了許多的使用情況並使得界限更加清晰。

  Tip: 在服務間共用的抽象有時會導致 微型服務最終變成分散的大石柱.


  4. 表面的封裝

這是在整篇文章中最困難的一個觀點。提醒一下我們正在討論過度設計。

  在使用額外的庫之前希先封裝它,不幸的是大多數我們寫的封裝類都是非常表面的,我們在提供功能性和建立一個好的封裝類之間徘徊,所以大多數我們寫的封裝類都與底層庫十分相似(有些情況下是1:1的映象關係,而有時候以10倍的功耗來完成原始庫中1/10的功能)如果我們在不久對底層庫進行改動,這個庫的封裝類到後來也不得不更改。有時我們也將業務邏輯融入到封裝類中,使它既不是一個好的封裝類也不是一個好的業務程式碼,而是處於在兩者之間的層次。

  這是2016. 外部庫和我們的客戶都有很大的提升。開源庫十分流行的,它們被十分厲害的人專門所編寫,有著高質量,可測試的程式碼。有更加清晰,可測試,更高效的API,允許我們去按照初始化——組裝(Instrument)——實現的步驟去構建。

TL;DR — 封裝是一個異常,而不一個規範,不要給一個好的庫進行封裝。

  Tip: 建立一個模糊的封裝可不是開玩笑的。封裝一個庫的想法來源於可配置這一想法,並將配置過後的具體實現隱藏起來。


  5. 使用質量檢測工具

  盲目使用高質量程式碼的規範(像把所有的變數改為 “private final”,為所有的類寫一個藉口, 等等) 並沒有把程式碼變得更好

  檢視 企業級程式設計 (or Hello World). 它有大量的程式碼。在微觀層次中每一個類都遵循SOLID原則,使用各種設計模式(工廠模式,建造者模式,策略模式等)以及程式設計技巧(泛型,列舉等),從CQM工具中它們都獲得了很高的程式碼質量評分。

TL;DR — 總是後退一步來巨集觀的看待問題

  相反的,自動化CQM工具擅長於跟蹤測試覆蓋率,而並不能告訴我們是否我們正在測試一個正確的東西。一個基準工具能夠跟蹤效能表現,但是它不能告訴我們程式是否是並行執行的還是順序執行的。只有人才能解決這些問題。

  5.1. 劃分層次

  我們採取簡潔,緊密的動作來把程式劃分為10或20個層次,它們之中的任何一個層都離不開整體而獨立存在。這是由於我們想要遵循“測試程式碼”或“單一責任原則”的概念。

  在過去 — 這由繼承來實現 A extends B extends C extends D

  總共有200層

  現在 — 人們做著相似的事情,讓每一個類都有一個介面來繼承同時把它注入到下一層中,就是為了SOLID原則。

  2016年採用SOLID原則的層次 (沒有寫出處的必要, 圖片由Keynote製作)

  SOLID原則的提出是為了防止繼承和其他OOP概念被濫用,而大多數開發人員都不知道這些概念是哪或為什麼來的,僅僅是錯誤的遵照。

TL;DR — 原則是為了轉變觀念,不是要像工具一樣盲目的使用。

  學習一種不同的語言然後嘗試以一種不同的思維去做事。這一才會成為一個更好的開發者。這些概念在新的語言當中可能根本不起作用 我們不應該以遵循某種原則的名義來分割某個清晰的設計


  6. 過度使用

過度使用泛型. 現在一個簡單的 “HelloWorldPrinter” 都會變成 “HelloWorldPrinter<>”.

  當已經是很明顯的只是為了處理一種資料型別或者通用的型別已經足夠的情況下不要使用泛型。

多度使用策略模式. 每一個IF語句現如今都成了一個策略.

  為什麼?

過度使用DSL. 在哪都使用DSL(領域特定語言)

  我不知道…

使用虛擬物件測試. 測試的時候每一個物件都是虛擬的.

  如何去改變…

超程式設計是很酷的,在哪都是用超程式設計

  誰知道為什麼…

列舉/可覆寫的方法/Traits這些都很酷,在哪都用

  這是錯的.

TL;DR — TL;DR不應該到處都用


  7. –ity

  • Configurability(可配置性)
  • Security(安全性)
  • Scalability(可量測性)
  • Maintainability(可維護性)
  • Extensibility(可擴充套件性)

  很難反駁

  例1:讓我們為我們的公司構建一個CMS,它遵循可擴充性,這樣人們就可以很容易的增加一個新的欄位。

  結論: 客戶永遠不會使用它,因為它們必須有一個開發者坐在他旁邊然後幫他完成操作,或許我們需要點選的介面替換成花幾個小時來成為一個簡單開發者的指南?

  例 2: 讓我們設計一個大型資料庫並遵循可配置性,我們應該通過一個檔案對資料庫進行各種配置

  結論:十年間,我只見到過一個客戶去努力來完全配置一個資料庫。這發生之後,那個所謂的配置檔案就沒有用了。 有如此多需要操作的工作,功能是的相容問題,接著客戶讓我們把資料庫裡的一半模型都轉化為NOSQL資料庫,我們氣瘋了——配置檔案上只是一行的改變,我們卻需要把整個系統重寫。

  在今天這個世界,我們已經可以構建一個單一的配置層對於現代的檔案系統/KV儲存(如Redis/ CouchDB/ DynamoDB/ )甚至像 Postgres/ HSQLDB/ SQLite 這些資料庫都可以做這樣的事 或者你根本使用資料層(同時掙扎與怎樣實現功能上的傳遞)或者使用其他資料庫來作為你的一部分(如postgres geo/json 特色) 這樣就可以沒有配置上的羞愧,你寫的堆疊和你寫的程式碼一樣都是解決問題的一部分。當你遠離X-ity這些原則的時候,你就會發現新的解決方式的出現,如:你可以採用垂直的打破資料傳輸許可權(使用小型DAOs來實現每一項操作)來替代水平的(配置層),或者對於不同的功能採取不同的資料儲存方式。

  例3: 我們為客戶構建OAuth系統。為了安全對於內部的管理員——我們要求他在使用一次Google的OAuth。 如果有人攻擊我們的OAuth,客戶不希望攻擊者得到他們的管理員許可權。Google OAuth是更加安全的,而且誰能夠否認更加安全這個觀點呢?

  結論: 如果有人真想要攻擊你的系統,他們並非必須經過OAuth層,在周圍還存在許多脆弱的層次,例如他們可以提權。因此為了支援兩套OAuth的配置的努力在保護系統安全方面並沒有成效,還不如先把基礎搞好。

TL;DR — 不要一味的追求 -ities要弄清楚場景/事例/需求/適用範圍

  Tip:問一個簡單的問題- 舉一個事例/場景的例子?然後在其中深入發掘,這將會暴露-ities的缺陷.


  8. 開發內部程式碼

  一開始感覺很酷,這幾年有很多開源的珍寶:

  • 內部庫 (HTTP, mini ORM/ODM, Caching, Config, etc)
  • 內部框架 (CMS, Event Streaming, Concurrency, Background Jobs, etc)
  • 內部工具 (Buildchains, Deployment tools, etc)

  被忽略但是:

  • 這些開源的東西需要大量的技巧和深入的理解。一個“Service runner”庫需要精通守護程式是怎樣工作的,程式管理,I/O重定向,PID檔案等等,一個CMS不僅僅是關於把一個資料型別傳遞給一個欄位中——這之間還有依賴關係,驗證、嚮導、通用渲染器等。甚至是一個簡單的“retry”庫實際上都不是很簡單的
  • 需要持續的努力去保持其穩定,甚至是一個微小的開源庫都要花費大量時間去維護。
  • 沒有人會關心你的開原始碼,只有起初的建立者才會花時間去維護它。
  • 而最初的建立者則會最終以“x的建立者”的名號離開
  • 奉獻與一個現有框架如今會花很多時間,但是建立一個新的東西將會花費更多的時間。

TL;DR — 重用,Fork,貢獻,重審

  最後,如果真的想要一直做下去,抱有一個開源的心態去做,與相似的專案競爭,努力說服內部人員去使用它,不要因為這是理所當然的就因為你是一個內部人員。


  9. 在原有程式碼上構建

  一旦某段程式碼正確的實現之後,每一個人都開始在其基礎之上毫無保留的構建自己的程式。沒有人質疑現狀,工作中被引用的程式碼就被視為正確的方式,甚至是調整現有程式碼去適應原來的 。

  每一天中團隊在原有程式碼上構建與更改原有程式碼之間的對比:

TL;DR — 重構是程式設計的一部分,沒有任何程式碼是不能改變的


  10.錯誤的預估

  我們經常看到一個好的團隊或者是開發者在最後開發出來的東西很糟,我們檢視他們的程式碼然後就想說“WTF,這真是我所仰慕的那個團隊/開發者寫出來的嗎?

  高質量的程式碼不僅需要程式設計技巧也需要時間。一些聰明的開發者往往會高估自己的能力,到最後他們將發現自己做出程式的醜陋和時間的緊迫。

TL;DR — 預先錯誤的預估將會降低程式碼的質量

相關文章