想把一個東西寫好很難。為什麼呢?因為只要寫好了,才會有很好的閱讀體驗。我們往往關注了前者而忽略了後者。我們忘記了程式碼只寫一次,但要讀很多次。
寫得好是指寫出來的東西讀起來容易,而不是指寫作本身,這一過程會產生大量的共鳴。它是指,退後一步,從讀者的角度來理解所寫的東西。人們必須以人的思維來理解問題,然後用其它人能夠理解的方式表達出來。在我看來,軟體屬於社會科學的一部分。我們要搞清楚程式碼寫出來是給誰看的,不是給人看的嗎?
因此,理解如何將思想和過程傳達給我們的同行甚至我們自己,這就是程式設計的核心。
為元件命名
為了說清楚第一個概念,我們來玩一個叫“我們在哪個房間?”的遊戲。我會給出一張圖,然後你告訴我這是哪個房間。
3個問題中的第1個
從這個圖很容易判斷出來是在客廳。我們從一個元件就能知道所處的房間。這非常容易,我們繼續。
3個問題中的第2個
從這個物體很清楚的知道這是在衛生間。
發現什麼規律了嗎?房間的名稱是一個標籤,它定義了這個房間裡有什麼。有了這個標籤,我們不知道進去看也知道里面有些什麼東西。這足以建立我們的第一個推論:
推論 1: 容器的名稱包含了其功能元素
注意這是最基本的“鴨子型別”[譯者注:如果它的動作像一隻鴨子,那它就是鴨子]。如果有一張床,那這裡就是臥室。
反過來也是如此:基於容器的名稱,我們可以推斷出它的組成部分。如果我們談論一個臥室,很可能它有一張床。這樣產生了我們的第二個推論:
推論 2: 可以根據容器的名稱推斷其中的元件
顯然我們已經有了一些規則,讓我們把這些規則應用到下一個房間。
3個問題中的最後一個
哇,床和馬桶怎麼會在同一個房間?這個房間的定義很模糊,朦朦朧朧,如果一定要用前面的兩個推論來為這個房間命名,它只能稱為怪物房間。
這裡的問題不在於房間裡物體的數量,而在於完全無關的事物被看作有同樣的功能。在家裡,我們會把相關的有類似作用或意圖的物品放在一起。如果把作用不同的東西胡亂放在一起,就讓人搞不明白架構師到底想怎麼來使用這些東西。由於混亂,我們在這裡不知所措。
推論 3: 容器定義的明確程度與其內部元件的緊密程度成正比。
這似乎不容易理解,那來看看圖示:
如果元件相關,就很容易找到一個好名字[譯者注:指容器的名字]。如果事務各不相干,找個合適的名字就會變得困難。這裡提到的關係,可能是指它們的功能、目的、策略、型別等。在我們談到標準之前,關係本身並不包含太多意思。現在先不要急,我們很快就會講到。
這對於軟體同樣適用。我們有元件、類、函式、服務、應用程式和其它一些東西。Robert Delaunay 曾經說過“我們的理解與我們的感知相關。”在當前的技術背景下,我們的程式碼是否能讓讀者以最簡單的方式感知到業務需求呢?
示例 1: HTTP 領域和汽車 domain and a car
HTTP 是一個領域,它有請求和響應。如果我們我們在其中放入一個汽車元件,那就不能再稱這為 HTTP。這種情況下它就已經變得混亂了。
1 2 3 4 5 |
public interface WhatIsAGoodNameForThis { /* methods for a car */ public void gas(); public void brake(); /* methods for an HTTP client */ public Response makeGetRequest(String param); } |
示例 2: 通過詞語來耦合
在類名中新增 Builder 或者其它以 er 結尾的單詞是種常見的模式。SomethingBuilder、UserBuilder、AccountBuilder、AccountCreator、UserHelper、JobPerformer。
通過名稱,我們可以瞭解三件事情。首先,在類名中使用 Build 這個動詞意味著它是穿著類這件外衣的程式。第二,它有兩個隱藏在內部的元素,User 和 Builder,這意味著可能違反了封閉性原則。第三,這意味著 Builder 可以訪問到 User 的內部工作,畢竟它們彼此糾纏。
這類似於工廠模組。我們的示例程式碼在整個程式碼庫中濫用時,它就會成為一個問題。此外,我得提醒你,在工廠模式中不需要什麼類。應用程式的 createUser() 就能完成工廠的工作。
[譯者注:Builder 也是一種模式,所以關於作者的這個觀點,請慎思]
示例 3: Base
來看一點實際專案中的例子。第一個例子是 I18n(國際化)的 Ruby Gem (為了簡便起見,只列出了類和方法的名稱):
1 2 3 4 5 6 |
class Base def config def translate def locale_available?(locale) def transliterate end |
這裡的 Base 並不能表達什麼意思。它可以進行配置和翻譯,也可以描述一個位置是否可用。它做了一些各不相同,毫不相干的事情。
示例 4: 名稱引導設計
我們在談論名稱如何引導我們的設計時,提到了好幾個例子,讓我們感興趣的例子中,有一個如下:
1 2 3 4 5 6 7 8 9 |
class PostAlerter def notify_post_users def notify_group_summary def notify_non_pm_users def create_notification def unread_posts def unread_count def group_stats end |
PostAlerter 這個名稱暗示我們它的功能是在提交的時候提醒某人。然而,unread_posts、unread_count 和 group_stats 卻很明顯在幹別的事情,這就使得類名稱不太理想。如果把這三個方法改到名為 PostsStatistics 的類中,表達出來就更清晰,讓新接觸的人一看就能明白。
1 2 3 4 5 6 7 8 9 10 11 |
class PostAlerter def notify_post_users def notify_group_summary def notify_non_pm_users def create_notification end class PostsStatistics def unread_posts def unread_count def group_stats end |
示例 5: 奇怪的名稱
Spring 框架中有一些例子說明元件做的事情太多,其名稱類似於我們的怪物房間。這裡就有一個 (因為這個就太多了點):
1 2 3 4 |
class SimpleBeanFactoryAwareAspectInstanceFactory { public ClassLoader getAspectClassLoader() public Object getAspectInstance() public int getOrder() public void setAspectBeanName(String aspectBeanName) public void setBeanFactory(BeanFactory beanFactory) } |
示例 6: 改變一下,說說好名稱
我們講了太多不好的名稱。D3 的 arc 中就定義了不錯的名稱,比如:
1 2 3 4 5 6 7 8 9 10 |
export default function() { /* ... */ arc.centroid = function() { /* ... */ } arc.innerRadius = function() { /* ... */ } arc.outerRadius = function() { /* ... */ } arc.cornerRadius = function() { /* ... */ } arc.padRadius = function() { /* ... */ } arc.startAngle = function() { /* ... */ } arc.endAngle = function() { /* ... */ } arc.padAngle = function() { /* ... */ } return arc; } |
這些方法每一個意義都很有完整的意義:它們都是基於弧所擁有的屬性來命名的。下圖中我非常喜歡的一點在於它真的很簡單。
方法 1: 拆解
應用場景:你不能為類或元件找到好名稱,但你知道如何拆解它們,而且期望給它們的組合找到一個好名稱。
這包含兩個步驟:
1. 確認我們擁有的概念
2. 拆解它們
在馬桶 + 床的情況下,我們把不同的事物拉開,床在左,馬桶在中。好了,我們終於把事物拆分成兩個部分,使它們不再那麼彆扭了。
如果你不能為某個事物找到好名稱的時候,也許是因為你面前不只一件事物。不過你現在已經知道對多個事物命名是件困難的事情。當遇到麻煩的時候,不妨確認一下構成這個事物的部分和動作。
示例
我們有一個尚未命名的類,包含 request、response、headers、URLs、body、caching 和 timeout。把所有這些從主類中拉出來,我們剩下了這樣一些元件:Request、Response、Headers、URLs、ResponseBody、Cache、Timeout 等。如果我們已知這些類的名稱,就會相當確定我們正在處理一個 Web 請求。HTTPClient 會是個不錯的 Web 請求元件名稱。
遇到困難的程式碼時,不要一開始就想著整體。考慮一下部分。
方法 2: 發現新概念
應用場景:某個類並不簡單或者不清晰時
發現新的概念需要業務領域的知識。如果在軟體中使用業務術語,因為專業語言已經建立了而且到處都在使用(Evans, 2003),不同專業領域的專業人士就會使用同樣的習語。
示例 1: 將元件封裝到新的概念中
幾年前,一家公司即將失去一份大合同。為什麼呢?因為該團隊釋出新功能和修復錯誤的速度太慢。
這個市場電子商務為不同國家的學生提供不同規則的多個支付閘道器,要求相當複雜。當我看到付款程式碼– PaymentGateway 時,我對其複雜性感到震驚,其中包括:User、UserAddress、CreditCard、BillingAddress、SellerAddress、LineItems、Discounts 等等。它的建構函式是巨大的,這種複雜性使得很難新增新的規則,因為改動一處會破壞其他規則,同時要求我們改動所有閘道器介面卡。
問題延伸到付款之外的事情。電子郵件會傳送給學生,通過 messaging 類再次聚合所有這些資料。技術支援也有自己的顯示端,這個資料第三次發生聚合,除了在這個特定的地方使用一個叫做 Aggregator(沒有上下文的單詞)的類。我們不得不做一些更改以修復這個架構阻礙。
為了解決這個問題,我開始了一個頭腦練習。這是一個如何去做的想法:
在這裡,我用於關於此事的這些細節,我需要你(PaymentGateway)對我負責。如果這是一張桌子,我會組織這些文案,我可能會把它們稱為 Invoice(發票)。那麼如果我建立了一個名為“Invoice”的類,這只不過是所有這些其他細節的彙總,這樣閘道器就不需要知道這些規則是如何完成的,因為 Invoice 是知道的?不是插入一百萬件物品,我只是把它交給你?
術語 Invoice 未在任何其他地方使用過。我們花了一個月時間在此重構,一旦完成,我們就能夠更快更新軟體。
Invoice 是概念的一個很好的示例,它是來自許多源的資料彙總,並且大多數人都知道它是什麼。 最終的解決方案增加了 Invoice 類,把它單獨注入到閘道器中,使用門面模式並隱藏其他類。
良好的命名不僅僅是寫出美麗的語句,還要更準確地描述以前無法表達的內容。
示例 2: 基於業務域的名稱變動
在一個綠地拼車專案中,我們從新設計了該系統,在研究其他交通運輸解決方案時發現,在某一天從起點到目的地描述某人旅程的最適當的詞是旅行(trip),而這群人被稱為乘客(ride)。 我們釋出了一個詞彙表,所以公司的其餘人員可以討論和分享相同的通用語言。
推出之後,我們的客戶總是把 trips 稱為 rides 。 不久之後,我們在將客戶要求轉換為必須完成的任務遇到麻煩,痛定思痛之後,我們決定是時間將 trips 重構為 rides ,將 rides 重構為 carpools 了。這就解決了在一家公司講兩種不同語言的問題。
示例 3: 抽象的等級
一個人說,移動右腿然後左腿然後右腿,另一個說走路。兩者都是一樣的,但後者據說更抽象。
理想情況下,隨著程式碼越來越接近其公開 API ,它越接近於企業術語。隨著它越接近資料庫和底層,它使用與其上下文相關的機器術語。在這之間,存在一個從多到少抽象漸變過程。
在一家公司,一個商人會說 post Tweet,所以一個如 postTweet() 的名字將會比一個公開的 API(比如makeHttpRequest())更有意義。在一家擁有更多技術服務的公司中,後者將更為充實。
第二,考慮特異性。postTweet() 非常具體,而 makeHttpRequest() 是如此通用,它可以用於 Facebook 或基本上涉及 HTTP 的任何內容。一個通用名稱可以輕易地重用,代價是不確定性。 這就解釋了為什麼框架程式碼與商業軟體程式碼的有如此大的區別。
示例 4: generalization 泛化
很久以前,CMS 中有資料庫表 news、history、videos、articles、pages、other。他們中大多數具有相同的列:title、summary、text。videos 表具有額外的屬性,例如 url(嵌入YouTube); history 表具有日期屬性,以便頁面可以按年份顯示歷史事件列表。所有這些表格看起來像是副本,在這裡和那裡僅有一些差異,而且新增新的功能需要重寫大量的樣板。
我將所有這些表摺疊成一個稱為 contents 的外來鍵,指向一個名為 sections 的表,其中包含 news、history、videos、others 的列表。現在,contents 的一個編碼就足夠了。多年以後,一個朋友不得不寫一個小的 CMS,我推薦使用同樣的做法。一旦管理內容的表單完成,它一般花費了1/N時間來實現任何功能,因為對於同一型別的每個新的部分,它都已經完成了。
通過賦予它另一個名字極大得提高生產力。news 是一類內容,Article 也是一類內容,history 又是一類內容。所有這些都可以共享相同的屬性嗎?是的。
方法 3: 分組的標準
什麼時候使用:當名字很好但是他們不能很好地配合時。
元件可以按照各種標準進行分組,包括物理性質、經濟性、感情色彩、社會性和軟體中最常用的功能。Photo 框架根據感情色彩方面分組,而產品則根據經濟動機分組。 沙發和電視留在同一個房間,根據功能標準分組在一起,因為它們具有相同的功能或目的,均用於休閒。
在軟體中,我們傾向於按功能對元件進行分組。列出你的專案檔案,你可能會看到像 controller/、models/、adapters/、templates/ 等等。 然而,有些時候,這些分組可能不太合理,這將是重新評估模組結構的最佳時間。
示例: 使用策略進行分組
用於自動化文件操作的庫根據程式碼、lints 描述檔案(保證格式正確)生成規範文件(比如 API 藍圖)並上傳到雲中(比如 S3 )。
根據檔案格式,將自動進行各種後續決定。選擇 API 藍圖將會選擇不同的 linter,不同的測試器和不同的 API 元素轉換器。 這裡基於一個輸入來組合所有這些不同的功能的關鍵詞是策略(strategy)。此後,該庫中包含一個將檔案格式、linter、文件測試器和儲存提供程式組合在一起的模組或者名稱空間,被稱為Strategy(策略)。這使得庫可以將業務核心策略中的普通檔案操作(如上傳者、解析器和命令列等)分開。
利用上下文
每個應用程式都有不同的上下文,包括其中的每個模組,它們中的每個類,每個函式都有。單獨的 User 一詞可以表示系統的使用者,但也可能是資料庫表或第三方服務憑證。 lib/billing/user 與 lib/booking/user 是不同的,但仍然屬於使用者範疇。
設想一下,每個諸如模組的容器,都是一個桶。 在其中,元件與外界絕緣。 你可以自由地命名這些類,無論你想要什麼樣的名字。這使得不必為常見事物尋找深奧的名字而絞盡腦汁。
針對微服務(許多獨立的桶)勝過整體式架構(一個內嵌小桶的大桶)的一個強有力的論據是它加強了對每個服務中的責任的限制,因為現在你不能輕易地將完全不相關的事情互相整合在一起。 Billing 位於 BillingApp 內部,booking 位於 BookingApp 內等等。但在整體架構中,這些相應的服務名稱可以是簡單的模組名稱,但並不是每個人都有責任來保持整潔。
示例: Namespaces (名稱空間)
馬克正在建立一個需要生成數十萬個廣告的 ads 平臺,然後將其傳送到 AdWords、Facebook 和 Bing ,所有這些都通過圖形使用者介面(GUI)進行管理。
馬克從一個稱為 Ad 的實體開始,很快就變得膨脹。AdWords 的廣告有 headline_part1 和 headline_part2 ,但 Facebook 裡面不是這樣,而 Bing 只有一個 headline(標題)。 他需要想辦法分開他的實體。他考慮到不同的語境,以及他如何利用語言的名稱空間來表達這一點。 他想出了以下結構:
- Adwords::Ad:這表示 Adwords 中一個 ad 物件。它用於專屬於 Adwords 的屬性以及可包含在該類的邏輯處理。
- Facebook::Ad:和上一個類似,但是它擁有 Facebook 專有的要求和邏輯處理。
- Bing::Ad:和上面的類似。
- RemoteAdService::Ad:這個作為 Adwords::Ad、Facebook::Ad、Bing::Ad 與系統的其他部分互動的介面。這意味著這三個類將會擁有同樣的公共 API,允許系統使用多型。
- Database::Ad:這是 ads 表的 ORM。它使用 ActiveRecord、DataMapper 或者其他自定義方案。
- GUI::Ad:這表示在 UI 上用於顯示廣告的屬性。它可能包含展示和國際化的功能。
- API::Ad:針對那些用於自定義屬性的廣告的 HTTP 終端,因此序列化的邏輯儲存在這裡是有道理的。
單詞在不同的上下文中可以表示不同的東西,當我們考慮上下文時,我們可以為元件選擇更簡單的單詞。 在這個示例中,我們沒有必要做任何複雜動作來找到這些元件名稱,因為它們是一回事,ad(廣告)。
無意義和新詞
多年來,名字變革並獲得新的含義。其他人來補充新的意義。
小助手 Helper:Helper 是那些支援應用程式的主要目標的函式。但是,那麼用來定義應用程式的主要目標的標準是什麼呢?應用程式中的所有東西都是用於支援應用程式主要目標的。
在實踐中,它們被集中在一個非自然的分組中,為一些其他常用的操作提供可重用模組。他們傾向於遇到依賴情結Feature Envy,他們需要訪問另一個元件的內部資料來工作。他們也是那些找不到合適名字的東西的緣由。
Base:名為 Base 的類是很久以前在 C# 中在缺少一個更好的名字時用於指定繼承的慣例。例如,汽車和自行車的父類將是 Base 而不是 Vehicle。儘管微軟的編碼規範中建議避免了這個名字(Cwalina,2009),但它影響了 Ruby 世界,其中最著名的是 ActiveRecord。到目前為止,我們依然可以看到在開發者找不到合適名字時使用 Base 作為類名的情況。
Base 的變體包括 Common 和 Utils 。例如,JSON Ruby gem Common 類具有解析、生成、載入和 jj 的方法,但是這裡的 Common 究竟是什麼意思呢?
Tasks:在 Javascript 社群有一個指示來呼叫非同步函式,tasks。 它起源於 task.js ,即使原始庫不存在,該術語也依然被使用。
團隊中的每個人都能理解這個嗎? 那就好了。但是,當一個新人加入該團隊,遇到被拋棄在垃圾堆中的 60年代以來就存在的命名法,又會是怎樣呢?
我在一個專案中工作,其中一個類的名字,猜猜看,Atlanta。 是的,亞特蘭大, 操蛋的亞特蘭大。沒有人知道或可以告訴我為什麼使用這種叫法。
溝通
“Reality exists in the human mind and nowhere else.(事實存在於人的思想中,而不是其他任何地方)” George Orwell
我認為溝通交流的做法是一個利他主義的行為,我們提高技能的努力與我們對他人的關心程度有關。我們希望人們更容易理解,我們想要消除衝突和障礙。
其次,我們希望別人能理解我們。通過承認投遞訊息給接收者是發件人的責任,我們建立一個移情的環境。 這是一個雙贏的局面。 沒有任何藉口不去練習我們的溝通技巧 – 除非你住在叢林中。
隨著寫作,我們優化閱讀,移情的練習可能是枯燥的。但是,正如生活中的一切,熟練度只會出現在那些常練習的人身上。
參考書目
Cwalina,Krzysztof.2009,框架設計指南:可重用 .NET 庫的約定、慣用語和模式,第二版。 Boston: Pearson Education, Inc. 206。
Evans, Eric. 2003。域驅動設計:解決軟體核心複雜性。Boston: Addison-Wesley Professional。