1. 程式人生 > >面向物件設計原則:不要STUPID,堅持GRASP和SOLID

面向物件設計原則:不要STUPID,堅持GRASP和SOLID

不要STUPID,堅持GRASPSOLID

聽過SOLID編碼嗎?有人可能會說:這是描述設計原則的一個專業術語,由我們可愛的程式碼整潔之道傳教者鮑勃(羅伯特C. 馬丁)大叔提出,是一組用於指導我們如何寫出“好程式碼”的原則。

在程式設計界充滿了這樣由單詞首字母組成的縮略詞。其它類似的例子還有DRY(Don’t Repeat Yourself! 不要重複你自己!)和KISS(Keep It Simple, Stupid! 讓事情簡單化,傻瓜化)。但是,這些條條框框好像有點多,太多,超級多……

所以,可不可以換個角度來解決這些問題呢?看看是什麼原因導致我們寫出“壞程式碼”。

抱歉,你的程式碼就是那麼的
STUPID

沒有人喜歡聽到別人評價他的程式碼很愚蠢。而且這樣做也很容易冒犯別人。所以不要說出來。但平心而論:全世界中大部分程式碼都是不可維護的,因為它們都是亂糟糟一團的。

那些爛程式碼又有什麼特點呢?是什麼把程式碼變得如此STUPID?

  • Singleton - 單態
  • Tightcoupling - 緊密耦合
  • Untestability - 不可測試
  • Premature Optimization - 過早優化
  • Indescriptive Naming - 胡亂命名
  • Duplication - 重複程式碼

你同意上面的列表嗎?是?好極。不?OK,我會在下面的內容中逐一解釋每項觀點,這樣你就可以更好地明白為什麼我會選用這些模式。

單態

<?php
class DB {
    private static $instance;
 
    public static function getInstance() {
        if (!isset(self::$instance)) {
            self::$instance = new self;
        }
 
        return self::$instance;
    }
 
    final private function __construct() { /* something */ }
    final private function __clone() { }
 
    /* actual methods here */
}

上面的程式碼是一段典型的資料庫訪問實現,你可以從很多PHP教程中找到這樣的程式碼。實際上,不久之前我也在使用與此風格類似的程式碼。

你可能會感到奇怪:這段程式碼怎麼了?不論在哪,利用DB::getInstance()都可以很容易地訪問DB啊,並且它還保證一次只有一個數據庫連線。到底壞哪兒了?

嗯,很好,我之前也是這樣想滴^^。“我只需要一個連線”。當應用程式規模變得稍微大一些的時候,實事會證明我還需要另一個數據庫的連線。這就是混亂的開始。我把單態稍加修改,增加了一個->getSecondInstance()方法,這樣,單態就變成了——“雙單態”了。其實我本就應該意識到資料庫連線不是一個簡單的單態結構,壓根就不該用它來作為實現方案。同樣的單態用法你還可以找到很多。請求物件的確是單態!但聽過子請求嗎?日誌就是!難道你不需要換個方式記錄點別的什麼?

上面描述的只是問題之一。還有一個重大的問題就是在程式碼中使用DB::getInstance()會把程式碼與類名DB強行繫結。也就是說,你無法對類DB進行擴充套件。假設我需要把查詢效能資料以日誌的形式寫到APC(Alternative PHP Cache)中。但由於類名緊密耦合,根本無法實現這項功能。如果當初程式是採用依賴注入的方式實現的,我可以很輕鬆地對型別DB進行擴充套件,然後傳入新的例項物件。但單態已經不允許我這樣做了。現在我能做的就是用下面這種粗製的手段實現我的想法:

<?php
// original DB class
class _DB { /* ... */ }
 
// extending class
class DB extends _DB { /* ... */ }

一個字兒:醜。或許還有人會加入一些別的形容詞,駭客、不可維護、大米共,或STUPID。

最後還有一點要考慮:還記得我之前說過的那句,“不論在哪,利用DB::getInstance()都可以很容易地訪問DB”。不得不承認,其實這也是件糟糕的事情。一看到“無論在哪”,我們自然可以聯想到“全域性”,也可以理解為“單態就是一個具有特別命名的全域性變數”。在你學習PHP的時候,你可能早就被告知使用關鍵字global是一個很壞的習慣。但殊不知使用單態和使用全域性變數的影響是一樣的:它們都建立了全域性狀態物件。這種方法建立的是非顯式依賴,結果就是會讓程式變得難以重用與測試。

緊密耦合

通過上面對單態問題的認識,你可能已經學會舉一反三,把問題推向使用更為廣泛的static方法和屬性。不管在什麼時候,只要編寫Foo::bar()這樣的程式碼,就是把程式碼和Foo類耦合在一起。這種耦合使得對Foo類的功能擴充套件幾乎變得不可能,進而導致程式碼很難被重用和測試。

類似的情況還有包括普通類名的使用,它們也同樣會帶來程式碼臭味。其中包括new操作符的使用:

<?php
class House {
    public function __construct() {
        $this->door   = new Door;
        $this->window = new Window;
    }
}

在上面的程式碼,你怎麼替換房子中的門和窗呢?答案很簡單:不可以。作為一個技法嫻熟的開發者,你可能一眼就會看出怎麼用些下流的駭客手法來替換門或窗。但是,或許下面的方式更為簡單一些:

<?php
class House {
    public function __construct(Door $door, Window $window) { // Door, Window are interfaces
        $this->door   = $door;
        $this->window = $window;
    }
}

採用這種方法可以很方便地把不同的門和窗加到房子裡。另外,這份程式碼同時還具有良好的擴充套件性、重用性和可測試性。你還有什麼可以奢求的嗎?

上面的程式碼概括起來說就是使用了“依賴注入(DI=DependencyInjection)”。而一講到DI,許多人就會把它和Symfony(一款PHP開發框架)這樣的DIC(Dependency Injection Container=依賴注入容器)聯絡起來,而實際上DI的概念是非常簡單的。

不可測試

單元測試很重用。如果你沒有對你的程式碼進行過測試,你也就登上駛往破壞程式碼的戰船。但即使是這樣,還是有很多人沒有很好地完成他們的測試。為什麼?大多數原因可以歸結於難以測試的程式碼。那又是什麼原因使得程式碼難以測試呢?主要是列表中前一點內容:緊密耦合。單元測試——看似清晰明瞭——就是測試一個程式碼單元(通常是各種類)。但如果類與類之間緊密結合,又怎麼可能針對每一個類進行測試呢?這時你可能會使用更多的駭客技術。但是,通常情況下大多數人都不會在此花費這麼多力氣,程式碼仍舊保持在原先無法測試的狀態,並任它慢慢腐敗。

每當你決定不編寫測試用例時,多時會把原因歸結於“沒有時間”,而真正導致這個結果的原因,其實是你的程式碼裡有太多垃圾。如果程式碼結構組織良好,測試不會花費你多少時間的。只有在程式碼雜亂無章的時候,單元測試才會成為負擔。

過早優化

下面的程式碼片段源自於我之前編寫的一個網站:

<?php
if (isset($frm['title_german'][strcspn($frm['title_german'], '<>')])) {
    // ...
}

猜一下它是幹什麼用的!

其實它只是檢查德語標題中是否包含字元“<”或“>”,可以說下你用了多久才看明白的嗎?你完全看明白了嗎?

我來做一下解釋:如果“<”和“>”都沒有的話,strcspn會返回字串的本身長度。所以這段程式碼可以簡單地看成isset($str[strlen($str)])。由於字串所允許的最大偏移量為字串本身的長度減一,所以上面程式碼的結果就永遠為false。假如目標字串中包含前面所述的兩個字元中的任何一個,函式就會返回一個小於字串長度的數字,這樣一來,整個表示式的結果就為true。

我為什麼要寫這麼一段難以理解的程式碼呢?為什麼不改用下面的方式呢:

<?php

if (strlen($frm['title_german']) == strcspn($frm['title_german'], '<>'))) {
    // ...
}
因為之前我曾讀到過isset要比strlen快許多……但這樣寫會使程式碼看起來很隱晦,因為它需要程式設計師必須精確瞭解函式strcspn語義(而大多數PHP程式設計師可能不是特別瞭解)。所以為什麼不改寫成:
<?php

if (preg_match('(<|>)', $frm['title_german'])) {
    // ...
}

因為我曾聽說使用正則表示式有點慢……(這樣說可能有謊話的嫌疑:實際上正則表示式要比我們想象的快得多。)

看看,除了令人費解的程式碼外,這些所謂的“優化”給我們帶來的還有什麼呢?一無是處。即便是現在,這個網站每月已經達到四千萬左右的訪問量(當然,在我剛寫下上面程式碼的時候還遠遠沒有達到這個數字),這個細小的優化基本上是微不足道的。因為這塊根本就不是程式的瓶頸。而實際的瓶頸是訪問最為頻繁的控制器中的三重JOIN(你的程式可能也會有這樣的瓶頸)。

從網際網路上你可以找到很多這樣的微優化(micro-optimization)。如“使用單引號,因為它們比較快一些”。別信這個。這樣的建議中大部分都是錯誤的,即使沒錯,它也不會讓你的程式碼執行速度有質的飛躍,反而只會浪費你的寶貴時間。

胡亂命名

還有一件事情需要提一下,你知道PHP的strpbrk函式是幹什麼的嗎?不知道?你甚至沒聽過有這個函式?好吧,也沒什麼可奇怪的。沒有人會去為了搜尋字串中的字元列表而去專門查詢一個名為strpbrk的函式。那這個名字究竟從哪裡來?其實它是繼承於C語言,它的名字代表“string pointer break”。耶,真是太好了,我們在不支援指標的語言中找到指標了(我意思是PHP沒有指標,不是指C)。

對了,在讀前一節的程式碼時,你能一下子反應出函式strcspn是幹什麼的嗎?不能?好吧,還是沒有什麼奇怪的。它是“stringcomplement span”的縮略形式,這樣寫是防止你搞不清楚它的含義。

到這裡為止我們得到的教訓就是:勞您大駕,在對類、方法、變數命名的時候,請多斟酌一下,儘量讓別人知道您真正的意圖。對$i這樣的變數我不想爭論什麼,因為它們太短了,其中的含義不言而喻。真正的問題是出現在像上面那樣命名的函式中。像函式strpbrk和變數$yysstk對於作者本人來講可能很直觀,但也就僅限於他本人了。

重複程式碼

我相信每個人都同意一種說法,短小精煉且直奔主題的程式碼都可以稱為上等佳作(提一下,我說的不是語法風格像Perl/Ruby那樣的“精簡”)。換個角度來講,冗長繁瑣的程式碼自然就是醜陋不堪了。其實這也就是前面提到的DRY(不要重複你自己)和KISS(讓事情簡單化,傻瓜化)原則所教授我們的。

那麼,重複程式碼從何而來?程式設計師們都是懶散的動物,所以少敲程式碼是它們的天性。這也就是為什麼反覆重複的程式碼至今還在盛行。

我個人認為,產生重複程式碼最常見的原因就是STUPID原則中的第二條:緊密耦合。如果你的程式碼彼此之間耦合的很緊,你就不可能重用它們。這就會導致重複性程式碼的出現。

不要STUPID,堅持GRASP和SOLID

那如何避免編寫STUPID程式碼呢?簡單,堅持GRASP和SOLID原則。

SOLID的解釋為:

  • Single responsibility principle
  • Open/closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

GRASP代表GeneralResponsibility Assignment Software Principles(通用職責分配軟體原則),它包括以下內容:

  • Information Expert
  • Creator
  • Controller
  • Low Coupling
  • High Cohesion
  • Polymorphism
  • Pure Fabrication
  • Indirection
  • Protected Variations

編碼快樂,新年快樂!

PS:如果你要問STUPID的出處:我可以告訴你,這些想法產自於StackOverflow的PHP聊天室,文章的每個組成部分都是由edorian、James和我搞出來的。而標題是由Gordon想出的。

譯註:
原文連線:http://nikic.github.io/2011/12/27/Dont-be-STUPID-GRASP-SOLID.html
GRASP:UML和模式應用
SOLID:http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

相關推薦

面向物件設計原則不要STUPID堅持GRASPSOLID

不要STUPID,堅持GRASP和SOLID 聽過SOLID編碼嗎?有人可能會說:這是描述設計原則的一個專業術語,由我們可愛的程式碼整潔之道傳教者鮑勃(羅伯特C. 馬丁)大叔提出,是一組用於指導我們如何寫出“好程式碼”的原則。 在程式設計界充滿了這樣由單詞首字母組成的縮略詞

面向物件設計原則實踐:之五.迪米特原則介面隔離原則

六、迪米特(第三者互動)原則 1. 定義 每一個軟體單位對其他的單位都只有最少的知識,而且侷限於那些與本單位密切相關的軟體單位。   2. 分析 1) 迪米特法則就是指一個軟體實體應當儘可能少的與其他實體發生相互作用。 這樣,當一個模組修改時,就會盡量少的影響其他的

(轉) 面向物件設計原則(二)開放-封閉原則(OCP)

原文:https://blog.csdn.net/tjiyu/article/details/57079927 面向物件設計原則(二):開放-封閉原則(OCP)        開放-封閉原則(Open-closed principle,OCP)也

java程式猿應該瞭解的10個面向物件設計原則(每次看都很有感悟特意拿來大家共享)

Java程式設計最基本的原則就是要追求高內聚和低耦合的解決方案和程式碼模組設計。檢視Apache和Sun的開放原始碼能幫助你發現其他Java設計原則在這些程式碼中的實際運用。 面向物件設計原則是OOPS(Object-Oriented Programming System,

C++設計模式面向物件設計原則

面向物件設計 變化是複用的天敵,而面向物件設計的最大優勢就是抵禦變化 面向物件設計原則 1.依賴倒置關係(DIP) 高層模組(穩定)不應該依賴於底層模組(變化),二者都應該依賴於抽象(穩定) 抽象(穩定)不應該依賴於實現細節,實現

基本設計模式學習筆記(一)常見的七種面向物件設計原則

0.概述      面向物件設計原則為支援可維護性複用而誕生,這些原則蘊含在很多設計模式中,他們是從許多設計方案中總結出來的指導性原則1.單一原則     一個類只負責一個功能領域中的相應職責,或者說:就一個類而言,應該只有一個引起它變化的原因。個人總結:將不同職責的方法放在

面向物件設計原則實踐:之四.里氏代換原則

五、里氏代換原則(LSP--Liskov Substitution Principle) 1. 定義 a). 如果對每一個型別為S的物件o1,都有型別為T的物件o2, 使得以T定義的所有程式P在所有的物件o1都代換成o2時,程式P的行為沒有變化, 那麼型別S是型別T的子型別。 b

面向物件設計原則 開放封閉原則(Open Closed Principle)

開放封閉原則(OCP,Open Closed Principle)是所有面向物件原則的核心。   軟體設計本身所追求的目標就是封裝變化、降低耦合,而開放封閉原則正是對這一目標的最直接體現。 其他的設計原則,很多時候是為實現這一目標服務的,例如以里氏替換原則實現最佳的、正確的繼承層次,就能保證不

面向物件設計原則 依賴倒置原則(Dependency Inversion Principle)

依賴倒置原則(Dependence Inversion Principle)是程式要依賴於抽象介面,不要依賴於具體實現。 簡單的說就是要求對抽象進行程式設計,不要對實現進行程式設計,這樣就降低了客戶與實現模組間的耦合。   面向過程的開發

面向物件設計原則 介面分離原則(Interface Segregation Principle)

介面隔離原則   使用多個專門的介面,而不使用單一的總介面,即客戶端不應該依賴那些它不需要的介面。   從介面隔離原則的定義可以看出,他似乎跟SRP有許多相似之處。 是的其實ISP和SRP都是強調職責的單一性, 介面隔離原則告訴我們在定義介面的時候要根據職責定義“較小”的介面

面向物件設計原則之合成複用原則

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

面向物件設計原則之介面隔離原則

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

面向物件設計原則之依賴倒轉原則

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

面向物件設計原則之迪米特法則

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

C#設計模式前奏-面向物件設計原則

          在學習設計模式之前,面向物件設計原則是必須要了解的東西。因為大多數設計模式都遵循這些設計原則中的一種或者多種。今天就帶大家一起去學習學習七類面向設計原則。首先列出常用的7中面向物件

java程式設計師應當知道的10個面向物件設計原則

面向物件設計原則是OOPS程式設計的核心, 但我見過的大多數Java程式設計師熱心於像Singleton (單例) 、 Decorator(裝飾器)、Observer(觀察者) 等設計模式,而沒有把足夠多的注意力放在學習面向物件的分析和設計上面。學習面向物件程式設計像“抽象”

面向物件設計原則-類庫設計原則

1.共同重用原則(CCP):一組介面中應該是共同重用的。如果重用了這組中 的一個類,那麼就要重用包中的所有的類。相互之間沒有緊密聯絡的類不應該在同一組中。 這個原則強調了分類,就具有相同關係或者關聯比較緊密的類應該分到一組中,方便修改和客戶端的呼叫。 2.共同封閉原則:類庫中的類

設計模式-面向物件設計原則

通過閱讀《大話設計模式》並記錄以下設計原則 單一職責原則 就一個類而言,應該僅有一個引起它變化的原因。 開閉原則 軟體實體應該可擴充套件,但是不可修改。 對於擴充套件是開放的,而對於修改

設計模式》劉偉主編【第2、3章 面向物件設計原則設計模式概述】

按照面向物件設計原則設計軟體,目標是提高軟體的可維護性和可複用性。而設計模式就是在工程實踐中總結出來的一套符合面向物件設計原則的軟體開發模式。 七大面向物件設計原則 設計原則名稱 設計原則簡介 備註 單一職責原則(Single Responsib

java面向物件——設計原則

1. 開閉原則 (open close principle) 對擴充套件開放,對修改關閉。在程式需要進行擴充套件的時候,不能去修改原有的程式碼,即實現一個熱拔插效果。 為了使程式的擴充套件性更好,易