閱讀521 返回首頁    go 阿裏雲 go 技術社區[雲棲]


程序員之痛點:取個好名字

0?wx_fmt=png

在計算機科學中隻有兩件事情最難:緩存失效和取名字。

——Phil Karlton

寫出好代碼很難,為什麼?因為好的代碼都是易讀的。我們總是關注於當下,卻不顧及以後;我們總是關注於代碼一次寫成,但忽視了在以後會被一次又一次地讀寫。

0?wx_fmt=png

好代碼總是為了方便他人易讀在不斷優化,這就需要我們將共情作為核心。好代碼能幫助別人從讀到的一段文字裏麵去回溯,從其他不同的視角去觀察這個問題,這甚至比寫代碼本身還難;程序員需要去理解這段代碼解決了一個人類問題,並且用一個其他人都能明白的方法表達出來。對於我來說,這就是將軟件問題放在人類社會的這個大環境中:我們是為哪位讀者編寫代碼?讀者能理解麼?

所以,如何加工處理、並傳遞我們的想法給我們的人類同胞們,這才是編程的核心。


根據成員命名

為了闡述我們的第一個概念,讓我們來做一個小遊戲:“我們在哪個房間?”。我會給你一些圖片,你來說出這是什麼房間。

問題1/3:有沙發的是什麼房間

0?wx_fmt=png

從家具判斷,這一定是客廳。根據一個成員(沙發),我們就可以知道我們在哪個房間。這很容易。

下一個問題。

問題2/3:有坐便器的是什麼房間

0?wx_fmt=png

從圖片物體判斷,我們非常肯定地說這是洗手間。

看到一個模式了麼?房間的名字就是一個標簽,定義了房間裏的東西。有了這個標簽,我們不需要查看容器,就可以知道裏麵有什麼元素。因此我們有了第一個推論:

推論1:容器的名稱指示其元素的功能

其實這基本就是鴨子類型(Duck Typing)。如果房間裏有床呢?那這就是臥室。

反之也成立:通過容器的名稱,我們可以推理其成員。如果我們說的是臥室,那就很可能有一張床。因此我們有了第二個推論:

推論2:通過容器名稱我們可以推理其成員

現在我們有了以上的兩個推論,讓我們試著把它們應用到下一個房間上。

問題3/3:有坐便器和床的是什麼房間

0?wx_fmt=png

坐便器和床在一個房間?這就讓這個房間的定義模煳化了。如果必須用推論1和2來對房間進行命名,恐怕得叫怪獸房間了吧。

問題並不在於房間內物件的數量,而在於完全不相關的事物被當做具有相同的功能來對待。在家裏,我們將具有相同目的的事物歸類,這樣會讓我們的組織更加簡單;如果各個物件功能被混淆、放置一起,我們就不確定設計者想要什麼,或者這些事物是被設計來怎麼用的。一旦混淆,工作進程就會受阻。

推論3:容器功能的清晰度是跟其成員之間相關性成正比的

以上推論比較難讀懂,所以用一張圖來闡述:

0?wx_fmt=png

當成員之間是相互關聯的,很容易找到一個好的名字;當不相關的時候,就變得非常困難。相關的方麵,可能是功能、目的、它們的策略、類型或者其它。也隻有討論到準則的時候,相關性才有意義。在後麵我會討論到這一點。

軟件中,同樣適用。我們有成員、類、功能、服務、應用、小怪物。Robert Delaunay說過:“我們的理解與我們的感知有關”。在這個技術背景下,我們的代碼可以用最簡單的方法,讓我們的讀者感知到商業的需求麼?

示例 1: HTTP 域和car(車)

HTTP是一個域,有請求和回複。但如果我們把一個叫做car的成員放進HTTP,那麼我們就不能再叫它HTTP了。在這個案例中,這個類變得很混亂。

public interface WhatIsAGoodNameForThis {
  /* methods for a car */
  public void gas();
  public void brake();

  /* methods for an HTTP client */
  public Response makeGetRequest(String param); 
}

0?wx_fmt=png

示例2: 詞語的連接

一個普遍的模式是在類名後麵加上Builder或者其它er結尾的詞。比如:SomethingBuilder. UserBuilder, AccountBuilder, AccountCreator, UserHelper, JobPerformer.

0?wx_fmt=png

從名字上解析,我們可得到三層意思。

  1. 類名裏的動詞Build,表示程序上這是一個類中包含有過程,具有複合功能;

  2. 它有兩個內在的、隱藏的實體,一個User一個Builder,所以可能會有違反封裝原則的危險;

  3. Builder可以訪問User的內部工作機製,因為Builder和User就是相互纏繞的啊。

這就像是一個工廠模式。當這個類在濫用代碼庫的時候,我們的示例就會帶來問題。需要提醒的是,在工廠模式裏,我們並不需要創建一個類。應用的createUser()就可以很好地實現工廠模式。

示例3: Base

讓我們來看一些真實的示例。第一個I18n Ruby gem(為了簡潔起見,隻提供類和方法的名字):

class Base
def config
def translate
deflocale_available?(locale)
def transliterate
end

在這裏,Base這個類沒有表達任何含義。它可以設置(configure)、翻譯(translate),以及判斷locale是否可得(available)。它做的是一些不同的、無關聯的事情。

示例4: 取名對設計的指導意義

當我們討論到這些類名是如何影響我們的設計時,Discourse有一些例子。以下的例子引起了我們的興趣:

classPostAlerter
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 的類裏,就會使類更加清晰明了,便於新人理解。

Class PostAlerter
def notify_post_users
def notify_group_summary
def notify_non_pm_users
def create_notification
end

classPostsStatistics
defunread_posts
defunread_count
defgroup_stats
end

示例5: 怪獸名字們

Spring框架裏有一些例子,因為成員太多,所以起了一些怪獸一樣的類名。以下是一個:

classSimpleBeanFactoryAwareAspectInstanceFactory {
 public ClassLoadergetAspectClassLoader()
 public Object getAspectInstance()
 public intgetOrder() 
 public void setAspectBeanName(String aspectBeanName) 
 public void setBeanFactory(BeanFactorybeanFactory)

示例6:一個好名字的例子

壞名字的例子已經夠多了,在D3’s arc(https://github.com/d3/d3-shape/blob/master/src/arc.js)可以找到好的取名例子,比如:

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;
}

這裏的所有的方法都表達了一個意思:它們都表示了圓弧的組成。我特別欣賞下麵這張圖,清晰明了。

0?wx_fmt=png

方法1:分解


0?wx_fmt=png

什麼時候使用: 這個類找不到好的名字,但是你對各成員已經有了獨立的概念,這時候你想為這些小組取個好名字。

分成兩步:

  1. 識別概念

  2. 對它們進行分解

在坐便器+床這個場景中,我們把床推到左邊、坐便器推到右邊。這樣,我們就有了不同的事物,我們可以自然而然地給出不同的命名。

當你不能為一些事物給出好的名字時,可能在你麵前的不止一件事物;目前你所知道的,給不同的事物命名很難。當你遇到麻煩的時候,試著分解你麵前的事物。

示例

我們有一個未命名的類,包含了request、 response、 essay-headers、URLs、body、 caching和timeout.把所有的成員從主類(main class)中拉出來,我們就有了Request、 Response、 Headers、URLs、ResponseBody、 Cache、Timeout等等。如果我們所擁有的隻是這些類的名字,我們可以非常肯定我們是在處理一個網頁請求。對於網頁請求一個很好的命名就是HTTPClient。

當這個代碼很難取名,不要首先想到整體!不要!想一想部分。


方法2:發現新概念


0?wx_fmt=png

什麼時候使用: 當類不簡單或者不連貫。

發現新概念需要商務領域的知識。當軟件與商務使用相同的術語時,一切就進行了統一,不同領域的專家用著同一種語法。

示例1:將多種元素封裝於一個新概念中

曾經,有個公司幾乎要丟失一單大合同,為什麼呢?因為那個團隊在開發新功能和問題處理方麵,速度特別慢。

這個市場電子商務為不同國家的學生提供不同規則的多個支付網關,需求確實比較複雜。當我讀到支付代碼PaymentGateway的時候震驚了,這是有多麼複雜啊。代碼裏麵還有相當一部分依賴條件,包括:User,UserAddress, CreditCard, BillingAddress, SellerAddress, LineItems,Discounts,等等。這個巨大的構造函數使得他們很難再添加新規則,因為添加任何一條規則都會打破其他規則,並需要去改動網管適配代碼。

這個問題甚至影響到了支付之外的服務。為了聚集消息類數據,他們給學生發送了郵件。技術支持有自己獨立的屏幕來看數據的第三次匯聚,除了這個特別的用到了名為Aggregator類的地方(類名也就是本義)。

於是,我們不得不做點什麼,來挽救這個架構缺陷。

處理這個問題,我想到了一些主意:我需要你( PaymentGateway)為我處理一些支付細節。如果我是一張課桌,或許我需要把這些發票幫我整理好。所以說如果我創建一個名為 invoice的類,僅用來處理這些細節信息的匯聚,那麼網關是不需要知道那些細節的,因為invoice會處理的,對嗎?與其注入無數個對象,我是否可以嚐試隻傳遞一個給你?

Invoice這個術語之前從來沒有應用過。我們花了一個月的時間進行重組,然後我們就能夠更為靈活地對代碼進行改動。Invoice是一個很好的實例,它描述了匯聚的概念而且大多數人都能看懂。最終解決方案是向Gateway單獨注入了Invoice類作為接口,用於屏蔽其他更多類。

好的命名不僅僅是優美的詞匯,而是要用精準的語言去表達代碼的內涵。

示例2: 根據業務領域的調整命名

在一個未開發的拚車項目中,我們從頭設計我們的係統。在對其它交通方案的研究中,對於某人在某天從某處出發到某目的地的這一旅行,最合適的詞是trip;而這一些旅行的人,就被稱為ride。我們打印了一個詞匯表,這樣公司的人就可以討論並且分享使用這些共同的詞匯。

但產品發布之後,我們的客戶總是把我們的trips稱為rides。很快,我們就在將客戶的請求轉換為我們需要做的事情上,產生了問題。痛定思痛,我們覺得是時候該把trips改變為rides、rides改變成carpools。這樣就解決了一個公司說著兩種不同語言的問題。

示例3:抽象級別 

一個人說,移動右腿然後左腿再右腿,另一個說走路。兩者都是一樣的意思,但後者說法更抽象。

理想情況下,當代碼越來越接近其公共API,它越接近於企業術語。隨著它接近數據庫和金屬,它會使用更多的計算機術語。在這之間,抽象程度逐步降低。

在一家公司中,一個業務人員會說postTweet,所以例如postTweet()這樣的一個名字,將在公共API比makeHttpRequest()更說得通。在一家擁有更多技術服務的公司中,後者的表達方式也足夠清楚。

二,考慮特異性。postTweet()是非常具體的,因為makeHttpRequest()是通用的,可以用於Facebook或基本上涉及HTTP的任何內容。一個通用名稱可以輕易地被重複使用,會導致含義不清晰。這就解釋了為什麼框架代碼與商業軟件代碼有如此大的區別。

Example 4:  概括

很久以前,一個CMS有數據庫表的新聞,曆史,視頻,文章,頁麵等。他們大多數有相同的列,標題,摘要,文本。Videostable有額外的屬性例如url(嵌入YouTube)和有adate屬性的曆史,所以網頁會顯示一年的曆史事件列表。所有這些表格看起來都是副本,在這裏和那裏有一些差異,如果要添加新功能,就需要重寫大量樣板。

我拆散了所有那些表,整合進一個類,名為contents,並用一個外部指針指向一個表格,名為section,包含新聞,曆史,視頻等清單。現在,一段代碼的contents就足夠了。幾年後,一個朋友不得不寫一個小的CMS,我建議他說,我把所有這些表折疊成一個被稱為contents的外鍵,指向一個叫做“sections”的表,其中列出了相同的方法。一旦管理content的表單完成,它花費1/N的時間來執行,因為對於同一類型的每個新的部分來說,都是一樣的。

通過命名的方式將過程通用化,會在很大程度上提高生產力。新聞是一個Content,文章是一個Content。曆史是一個Content。所有這些都可以共享相同的屬性嗎?是。那調查是不是Cintent?哦,不,不是Content。


方法三:分組標準

什麼時候使用:當名字很好,但他們不能很好地相配時。

組件可以通過各種標準進行分組,包括物理性質,經濟性,情感性,社會性和軟件中最常用的功能。相框根據情感方麵分組,而產品則根據經濟動機分組。沙發和電視留在同一個房間,根據功能標準分組在一起,因為它們具有相同的功能或提供休閑的相同目的。

在軟件中,我們傾向於按功能對組件進行分組。列出你的項目文件,你可能會看到像controller /,models /,adapters /,templates /等等。然而,有些時候,這些分組並不讓人舒服,這就是時候來重新評估模塊結構了。

示例:按策略分組

一個用於自動化文檔操作的庫(如API藍圖)根據代碼生成規範文件,lints所述文件(保證格式正確)並上傳到雲(如S3)。

根據文件格式,將自動進行各種後續決定。選擇API藍圖將會選擇不同的linter,不同的測試器和不同的API Elements轉換器。這裏的關鍵詞策略是基於一個輸入strategy來組合所有這些不同的功能。此後,該庫包括一個稱為strategy策略的模塊(或名稱空間),該模塊將文件格式,linter,文檔測試器和存儲供應商組合在一起。這使得庫可以將業務核心策略中的普通操作文件(如上傳者,解析器和命令行)分開。


利用上下文

每個應用程序都有不同的上下文,同樣的,其中的每個模塊,它們內的每個類,到每個功能也是這樣。User這個名字可以單獨表示係統用戶,也可能是數據庫表或者第三方服務憑證。lib/billing/user 與lib/booking/user不同,但仍然是用戶。

0?wx_fmt=png

想象一下,每個容器,如模塊,都是一個bucket。在其中,組件被封裝,與外界絕緣。你可以自由命名這些類,無需為一些尋常的事物去創建出生僻的名稱。

一個在整體式架構(一個大容器,其中有一些小容器)中的微服務(許多獨立的容器)的強有力的論據是,它強製限製每個服務中的責任,因為你無法輕鬆地將完全不相關的事情互相糾纏在一起。 BillingApp內部的幾張同能,BookingApp內預訂功能等等。在一個單一的架構中,這些相應的服務名稱可以是簡單的模塊名稱,但並不是每個人都會恪守原則保持代碼井井有條的。

示例:命名空間

馬克正在建立一個需要生產成千上萬條廣告的廣告平台,然後發送到AdWords(穀歌),臉書和必應,所有這些都通過圖形用戶界麵進行管理。

馬克從一個稱為Ad的實體開始,很快開始膨脹。AdWords的廣告有headline_part1和headline_part2,臉書不會,而必應隻有headline。他需要想辦法分裂他的實體。他思考不同的語境,以及如何利用語言的命名空間來表達這一點。他想出了以下結構:

  • csdcAdwords :: Ad:這表示Adwords中的廣告對象。它具有Adwords獨有的屬性,邏輯可以包含在此類中。

  • Facebook ::Ad:與上一個相同,除了它具有臉書的具體要求和邏輯。

  • 必應::Ad: 和上麵一樣.

  • RemoteAdService :: Ad:這是Adwords::Ad,Facebook::Ad,Bing::Ad和其餘部分之間的界麵。這意味著這三個類都將具有相同的公共API, 允許係統利用多態。

  • Database::Ad:這是廣告表的對象關係映射(ORM)。它使用ActiveRecord,DataMapper或任何自定義解決方案。

  • GUI :: Ad:這表示在UI中顯示廣告所需的屬性。它可能具有演示和國際化功能。

•API :: Ad:廣告的HTTP端點將具有自己的自定義屬性,因此序列化邏輯存在於此處。

詞語可以意味著不同的東西,這取決於上下文,當我們利用上下文時,我們可以為組件選擇更簡單的單詞。在這個例子中,我們不需要複雜的過程就能找到這些組件名稱,因為它們是一回事,廣告。


無意詞和新詞

年複一年,名字也在發展並被賦予獲得新聞的含義。新詞不斷湧現,填補空缺。

助手(helper):助手是支持應用程序的主要目標的功能。但是,那麼定義應用程序的主要目標是什麼?應用程序中的所有內容都支持應用程序的主要目標。

在實踐中,它們被集中在一個非自然的分組中,為一些其他混雜的,常用的操作提供可重用性。他們傾向於Feature Envy,他們需要訪問另一個組件的內部數據來工作。他們是找不到正確名稱東西的借口。

基礎:名為Base的類是很久以前在C#中指定繼承的慣例,缺少一個更好的名稱。例如,汽車和自行車的父類將是Base而不是Vehicle。

盡管微軟的建議避免了這個名字(Cwalina,2009),但它顯然通過ActiveRecord影響了了Ruby界。到目前為止,我們仍然將Base看作開發人員找不到名稱的類名。

Base的變體包括Common and Utils。例如,JSON Ruby gem 的Commonclass具有解析(parse),生成(generate),加載(load)和jj的方法,但是常見的是什麼意思呢?

任務:在Javascript社區有一個習慣來調用異步函數,叫tasks。它從task.js開始,即使原始庫不存在,該術語也被使用。

團隊中的每個人都明白嗎?如果真是那樣就好了。但是,當一個某人新加入團隊,遇到了和60年代以來存在的已然被拋棄在垃圾中命名法,會發生什麼情況呢?

我在一個項目中工作,猜一下這個類的名字,亞特蘭大。是的,亞特蘭大。真見鬼,沒人說的清楚為啥叫那名字。


溝通

“現實僅存在於人的思想,而非其他任何地方。”喬治·奧威爾(George Orwell)

我認為溝通交流的做法是一個利他主義的行為,我們提高技能的努力與我們對別人的關心有關。我們希望人們容易理解,我們想要消除矛盾和障礙。

其次,我們希望別人了解我們。令收到消息的人理解這條消息是發件人的責任,如果能夠接受這個觀點,我們營造一個共情的環境,一個雙贏的局麵。沒有任何借口不去刻意練習我們的溝通技巧 - 除非你住在叢林中。

隨著寫作,我們優化閱讀,練習共情等方麵的技巧可能是很辛苦的。但是,正如生活中的一切,熟能生巧是最樸素的真理。



原文發布時間為:2017-5-3

本文來自雲棲社區合作夥伴“大數據文摘”,了解相關信息可以關注“BigDataDigest”微信公眾號

最後更新:2017-05-17 00:45:44

  上一篇:go  護士上門醫療服務,輕鬆體驗在家就能就醫!
  下一篇:go  Python確實比較慢,但我不在乎