摘要 :編寫高效優質的代碼一直是程序員所追求的目標之一,那么什么樣的代碼才叫優質呢?其中最重要的莫過于易維護、易修改。本文作者從面向對象和SOLID兩大方面,非常詳細地總結了如何編寫出易修改的代碼,絕對讓你受益匪淺。
?
在實際的開發中,編寫出易維護和易接受變化的代碼并非易事,想要實現可能更加困難重重:源碼難于理解、依賴關系指向不明、耦合也很令人頭疼。難道就真的就沒有辦法了嗎?本文中我們一起探討幾個技術原則和一些編碼理念,讓你的代碼跟著需求走,而且易維護易拓展。
?
介紹些面向對象方法
?
面向對象編程(OOP)是一種很受歡迎的編程思想,它保證了代碼的組織性和重用性。軟件公司采用OOP思想編程已經好多年了,如今仍然在項目開發中使用這一思想。OOP擁有一系列非常好的編程原則,如果使用恰當,它會讓你的代碼更好、更整潔和更易維護。
?
1.內聚力
?
這里的內聚力是指擁有一些共同的特征的東西而逐漸凝聚到一起,而不能在一起的東西則會被移除出去。可以用一個類來說明內聚力:
class ANOTCohesiveClass { private $firstNumber; private $secondNumber; private $length; private $width; function __construct($firstNumber, $secondNumber) { $this->firstNumber = $firstNumber; $this->secondNumber = $secondNumber; } function setLength($length) { $this->length = $length; } function setHeight($height) { $this->width = $height; } function add() { return $this->firstNumber + $this->secondNumber; } function subtract() { return $this->firstNumber - $this->secondNumber; } function area() { return $this->length * $this->width; } }?
?
該例定義了一個類以及一些表示數字和大小的字段。而這些屬性通過他們的名稱來判斷是否應該在一起。add()和substract()方法來對兩個number進行操作,此外還定義了area()來操作length和width這兩個字段。
?
這個類只負責各個獨立的群體信息,顯然,內聚力很低。重構上面的例子:
class ACohesiveClass { private $firstNumber; private $secondNumber; function __construct($firstNumber, $secondNumber) { $this->firstNumber = $firstNumber; $this->secondNumber = $secondNumber; } function add() { return $this->firstNumber + $this->secondNumber; } function subtract() { return $this->firstNumber - $this->secondNumber; } }?
?
重構以后,該類明顯變成了高內聚特征的類。為什么?因為這個類里的每個部分都與另外一部分彼此聯系。雖然在實際開發中編寫出高內聚的類比較困難,但開發人員應該堅持這樣做,堅持就是勝利。
?
2.正交性
?
就簡單而言,正交是指隔離或排除副作用。一個方法、類或者模塊改變了其他無關的方法、類或模塊就不是正交。例如,飛機的黑匣子就具有正交性,它自身就具備電源、麥克風和傳感器等這些功能。而它對外在的其他東西沒有任何影響,它只提供一種機制,用來保存和檢索飛行數據。
?
一個典型的非正交系統例子就是汽車電子設備。提高汽車的速度也存在些負面影響,比如會增加無線電音量,然而對汽車來說,速度并不是正交。
class Calculator { private $firstNumber; private $secondNumber; function __construct($firstNumber, $secondNumber) { $this->firstNumber = $firstNumber; $this->secondNumber = $secondNumber; } function add() { $sum = $this->firstNumber + $this->secondNumber; if ($sum > 100) { (new AlertMechanism())->tooBigNumber($sum); } return $sum; } function subtract() { return $this->firstNumber - $this->secondNumber; } } class AlertMechanism { function tooBigNumber($number) { echo $number . 'is too big!'; } }?
?
在這個例子中,Calculator類里的add()方法里列了幾個意想不到的行為:它生成AlertMechanism對象并調用其中的一個方法。實際上,該庫的使用者并不希望消息被打印到屏幕上,相反,他們則是要計算數字之和。
class Calculator { private $firstNumber; private $secondNumber; function __construct($firstNumber, $secondNumber) { $this->firstNumber = $firstNumber; $this->secondNumber = $secondNumber; } function add() { return $this->firstNumber + $this->secondNumber; } function subtract() { return $this->firstNumber - $this->secondNumber; } } class AlertMechanism { function checkLimits($firstNumber, $secondNumber) { $sum = (new Calculator($firstNumber, $secondNumber))->add(); if ($sum > 100) { $this->tooBigNumber($sum); } } function tooBigNumber($number) { echo $number . 'is too big!'; } }
?
?
這樣明顯好多了,AlertMechanish在Calculator中沒有任何負面影響,相反,在任何需要彈出警告的地方都可以使用AlertMechanish。
?
3.依賴和耦合
?
大多數情況下,這兩個單詞是可以互換的,但是在某些情況下,又存在優先級關系。
?
那么,什么是依賴呢?當對象A需要使用對象B時,為了執行其規定的行為,我們說A依賴B。在OOP中,依賴是極其常見的。對象之間經常互相依賴才發揮功效。因此消除依賴是一項崇高的追求,這樣做幾乎是不可能的。控制依賴和減少依賴則是非常完美的。
?
就緊耦合(heavy-coupling)和松耦合(loose-coupling)而言,通常是指一個對象依賴于其他對象的程度。
?
在一個松耦合系統中,一個對象的變化會減少對其依賴對象的影響。在這樣的系統中,類取決于接口而不是具體的實現(將會在下面提到)。這就是為什么松耦合系統對修改更加開放的原因。
?
Coupling in a Field
?
讓我們看下面這個例子:
class Display { private $calculator; function __construct() { $this->calculator = new Calculator(1,2); } }
?
?
這段代碼很常見,在該例中,Display類依賴Calculator類并直接引用該類。Display類里的 $calculator字段屬于Calculator類型。該對象和字段直接調用Calculator的構造函數。
?
通過訪問其他類方法進行耦合
?
大家可以先看下面的代碼:
class Display { private $calculator; function __construct() { $this->calculator = new Calculator(1, 2); } function printSum() { echo $this->calculator->add(); } }?
?
Display類調用Calculator對象的add()方法。這是另外一種耦合方式,一個類訪問另外一個類的方法。
?
通過方法引用進行耦合
?
你也可以通過方法引用進行耦合:
class Display { private $calculator; function __construct() { $this->calculator = $this->makeCalculator(); } function printSum() { echo $this->calculator->add(); } function makeCalculator() { return new Calculator(1, 2); } }
?
?
需引起注意的是,makeCalculator()方法返回一個Calculator對象,這也是一種依賴。
?
利用多態進行耦合
?
遺傳可能是依賴里的最強表現形式。
class AdvancedCalculator extends Calculator { function sinus($value) { return sin($value); } }
?
?
通過依賴注入降低耦合
?
開發人員可以通過依賴注入來降低耦合度,例如:
class Display { private $calculator; function __construct(Calculator $calculator = null) { $this->calculator = $calculator ? : $this->makeCalculator(); } // ... //. }?
?
利用Display的構造函數對Calculator對象進行注入,從而減少了Display對Calculator類產生的依賴。
?
利用接口降低耦合
?
例如:
interface CanCompute { function add(); function subtract(); } class Calculator implements CanCompute { private $firstNumber; private $secondNumber; function __construct($firstNumber, $secondNumber) { $this->firstNumber = $firstNumber; $this->secondNumber = $secondNumber; } function add() { return $this->firstNumber + $this->secondNumber; } function subtract() { return $this->firstNumber - $this->secondNumber; } } class Display { private $calculator; function __construct(CanCompute $calculator = null) { $this->calculator = $calculator ? : $this->makeCalculator(); } function printSum() { echo $this->calculator->add(); } function makeCalculator() { return new Calculator(1, 2); } }?
?
該代碼定義了一個CanCompute接口,在OOP中,接口可以看作一個抽象類型,它所定義的成員必須由類或結構來實現。在上述代碼中,Calculator類來實現CanCompute接口。
?
Display構造函數期望有個對象來實現Cancompute 接口,這時,Display的依賴對象Calculator被打破。然而,我們可以創建另一個類對象來實現Cancompute,并且傳遞一個對象到 Display的構造函數中。Display現在只依賴于Cancompute接口,但即使這樣依賴關系仍然是可選的。如果我們不傳遞任何參數給 Display的構造函數,那么它將通過調用makeCalculator()方法來創建一個Calculator對象。這種技術經常被開發者們使用,尤 其對驅動測試開發(TDD)極其有幫助。
?
SOLID原則
?
SOLID是一套代碼編寫守則,也就是大家常常說的敏捷開發原則,最初由Robert C. Martin所提出。使用它編寫出來的代碼不僅干凈整潔,而且易維護、易修改和易擴展。實踐表明,其在可維護性上有著非常積極的影響,更多資料大家可以閱讀: Agile Software Development, Principles, Patterns, and Practices
?
SOLID所涵蓋的話題非常廣,下面我將會針對本文的主旨介紹一些簡單易學的方法。
?
1.單一責任原則(SRP)
?
一個類只干一件事。聽起來簡單,但在實踐中卻可能相當難。
class Reporter { function generateIncomeReports(); function generatePaymentsReports(); function computeBalance(); function printReport(); }
?
查看上面的代碼,你認為該類的受益者會是哪個部門?會計部是用于收支平衡、財政部可能用來編寫收入/支出報告,甚至歸檔部來打印和存檔報告。然而每個部門都希望有屬于自己的方法,并且根據自身需求來做些自定義的方法。
?
這樣的類往往都是高內聚低耦合的。
?
2.Open-Closed原則(OCP)
?
類(和模塊)應具備很好的功能擴展性,以及對現有功能具有一定的保護能力。讓我們一起來看下典型的電風扇例子,你有一個開關來控制風扇:
class Switch_ { private $fan; function __construct() { $this->fan = new Fan(); } function turnOn() { $this->fan->on(); } function turnOff() { $this->fan->off(); } }
?
這段代碼創建了Switch_類,用來創建和控制Fan對象。注意這里的下劃線,在PHP中是不允許把類名定義為Switch的。
這時,你的老板希望能利用該開關控制電風扇上的電燈,那么你就不得不修改Switch_這個類。
?
對現有代碼進行修改存在一部分風險,很有可能對系統其他部分產生影響。所以在添加新功能時的最好的方法是避開現有功能。
?
在OOP中,你可以發現Switch_對Fan類有很強的依賴性。這正是我們的問題所在,基于此,做出如下修改:
interface Switchable { function on(); function off(); } class Fan implements Switchable { public function on() { // code to start the fan. } public function off() { // code to stop the fan. } } class Switch_ { private $switchable; function __construct(Switchable $switchable) { $this->switchable = $switchable; } function turnOn() { $this->switchable->on(); } function turnOff() { $this->switchable->off(); } }
?
該代碼定義了一個Switchable接口,它里面所定義的方法需要開關啟用選項來實現。Fan對象實現Switchable和Switch_并且接受一個參數到Switchable對象的構造函數里。
?
這樣做有哪些好處?
?
首先,該解決方案打破了Switch_和Fan之間的依賴關系。Switch_不知道它要開啟風扇,并且也不關心。其次引進的Light類不會影響Switch_或Switchable。難道你想用Switch_類來控制Light對象嗎?代碼如下:
class Light implements Switchable { public function on() { // code to turn ligh on. } public function off() { // code to turn light off. } } class SomeWhereInYourCode { function controlLight() { $light = new Light(); $switch = new Switch_($light); $switch->turnOn(); $switch->turnOff(); } }
?
3.Liskov替換原則(LSP)
?
LSP是指子類永不打破父類的功能,這點是非常重要的。用戶定義一個子類只是希望能實現其自有功能,而不是去影響原來的功能。
?
乍看有點困惑,還是讓我們一起來看看代碼吧:
class Rectangle { private $width; private $height; function setWidth($width) { $this->width = $width; } function setHeigth($heigth) { $this->height = $heigth; } function area() { return $this->width * $this->height; } }
?
定義一個簡單的Rectangle類,我們可以設置它的高度和寬度,并且area()方法可以計算出該矩形的面積。再看下面例子:
class Geometry { function rectArea(Rectangle $rectangle) { $rectangle->setWidth(10); $rectangle->setHeigth(5); return $rectangle->area(); } }
?
rectArea()方法接受一個Rectangle對象作為一個參數,設置其高度和寬度并且返回該圖形的面積。
?
正方形乃是矩形中的一個特殊圖形,我們定義Square類來繼承Rectangle:
class Square extends Rectangle { // What code to write here?. }
?
我們有好幾種方法來重寫area()方法并且返回該正方形的寬度:
class Rectangle { protected $width; protected $height; // ... //. } class Square extends Rectangle { function area() { return $this->width ^ 2; } }
?
把Rectangle的字段改為protected,好讓Square有訪問的權限。從幾何的角度來看是非常合理的,因為正方形的邊長是相等的,所以返回正方形的寬度是非常合理的。
?
然而從編程的角度來看又存在一個問題;如果Square是一個 Rectangle,把它饋入到Geometry類是沒有任何問題的,但這樣做以后,Geometry的代碼就顯的多余,毫無意義可言。它設置了高度和寬 度兩個值,這也就是為什么square不是rectangle編程。LSP正很好是說明了這一點。
?
4.接口隔離原則(ISP )
?
該原則主要集中用在把大接口分成多個小接口和特殊的接口。基本思路是在同一個類中,不同的用戶不應該知道不同的接口——除非該用戶需要用到那個接口。即使一個用戶不需要使用該類的所有方法,但它仍然依賴于這些方法。所以為什么不根據用戶需要定義相應的接口呢?
?
想象下,如果我們要實現一個股票市場應用,我們要有一個經紀人 (Broker)來購買和出售股票,并且報告每天的收益和損失。一個簡單的實現方法是定義一個Broker接口,一個NYSEBroker類用來實現 Broker和一些用戶的接口類:創建交易(TransactionUI)和寫報告(DailyReporter)。代碼可以類似下面這樣:
interface Broker { function buy($symbol, $volume); function sell($symbol, $volume); function dailyLoss($date); function dailyEarnings($date); } class NYSEBroker implements Broker { public function buy($symbol, $volume) { // implementsation goes here. } public function currentBalance() { // implementsation goes here. } public function dailyEarnings($date) { // implementsation goes here. } public function dailyLoss($date) { // implementsation goes here. } public function sell($symbol, $volume) { // implementsation goes here. } } class TransactionsUI { private $broker; function __construct(Broker $broker) { $this->broker = $broker; } function buyStocks() { // UI logic here to obtain information from a form into $data. $this->broker->buy($data['sybmol'], $data['volume']); } function sellStocks() { // UI logic here to obtain information from a form into $data. $this->broker->sell($data['sybmol'], $data['volume']); } } class DailyReporter { private $broker; function __construct(Broker $broker) { $this->broker = $broker; } function currentBalance() { echo 'Current balace for today ' . date(time()) . "\n"; echo 'Earnings: ' . $this->broker->dailyEarnings(time()) . "\n"; echo 'Losses: ' . $this->broker->dailyLoss(time()) . "\n"; } }
?
雖然這段代碼可以正常工作,但它違反了ISP。 DailyReporter和TransactionUI都依賴Broker接口。然而,它們只使用接口的一部分。TransactionUI使用 buy()和sell()方法,而DailyReporter只用到dailyEarnings()和dailyLoss()方法。
?
你懷疑Broker沒有內聚力,因為它的一些方法沒有任何相關性。也許你說的對,但是具體答案還得由Broker說了算;銷售和購買可能與當前的盈余有相當大的關系。例如當虧本的時候有可能就不會執行購買操作。
?
此時,你可能會說Broker違反了SRP,因為有兩個類以不同的方式在使用它,可能有兩個不同的執行者。好吧,其實它并沒有違反SRP。唯一的執行者就是Broker。他會根據當前的形式做出購買/出售操作,其最終的依賴對象是整個系統和業務。
?
毫無疑問,上述代碼肯定是違反了ISP,兩個UI類都依賴于整個Broker。這是很常見的問題,改變下觀點,代碼可以這樣修改:
interface BrokerTransactions { function buy($symbol, $volume); function sell($symbol, $volume); } interface BrokerStatistics { function dailyLoss($date); function dailyEarnings($date); } class NYSEBroker implements BrokerTransactions, BrokerStatistics { public function buy($symbol, $volume) { // implementsation goes here. } public function currentBalance() { // implementsation goes here. } public function dailyEarnings($date) { // implementsation goes here. } public function dailyLoss($date) { // implementsation goes here. } public function sell($symbol, $volume) { // implementsation goes here. } } class TransactionsUI { private $broker; function __construct(BrokerTransactions $broker) { $this->broker = $broker; } function buyStocks() { // UI logic here to obtain information from a form into $data. $this->broker->buy($data['sybmol'], $data['volume']); } function sellStocks() { // UI logic here to obtain information from a form into $data. $this->broker->sell($data['sybmol'], $data['volume']); } } class DailyReporter { private $broker; function __construct(BrokerStatistics $broker) { $this->broker = $broker; } function currentBalance() { echo 'Current balace for today ' . date(time()) . "\n"; echo 'Earnings: ' . $this->broker->dailyEarnings(time()) . "\n"; echo 'Losses: ' . $this->broker->dailyLoss(time()) . "\n"; } }
?
修改后的代碼明顯變的有意義而且尊重了ISP。 DailyReporter只依賴BrokerStatistics,它無需關心和知道出售和購買這兩個操作。另一方面,TransactionUI只關 心購買和出售。NYSEBroker和先前的定義是一樣的,實現BrokerTransactions和BrokerStatistics接口。
?
更復雜的例子你可以前往Rober C.Martin博客上查看 The Interface Segregation Principle里的首篇論文。
?
5.依賴倒置原則(DIP )
?
這條原則指出高層模塊不應該依賴低層模塊,兩者都應該依賴于抽象。抽象不應該依賴細節,細節反過來應依賴于抽象。簡單地說,你應該盡可能的依賴于抽象而不是實現。
?
DIP的訣竅是你想反轉依賴,但是又想一直保持著整個控制流。回顧下OCP(Switch和Light類),在原始實現中是直接利用開關來控制燈的。
?
?
你會看到整個依賴和控制流都是由Switch流向Light。當不想直接控制Light時,你可以引進接口這一概念。
?
?
非常神奇!引進接口后,代碼同時滿足了DIP和OCP兩大原則。正如你上圖所看到的,倒置了依賴,但整個控制流是不變的。
?
高級設計
?
關于代碼的另一重要方面是高級設計和通用體系結構。一個混亂的架構所產生的代碼往往是很難修改的,所以保持一個干凈整潔的架構是必不可少的,第一步就是理解如何根據不同的內容分離代碼。
?
?
這張圖中,最主要的部分是業務邏輯,它能夠如預期那樣正常有效的工作并且與其他部分不存在任何瓜葛。站在高級設計角度可以看作為正交性。
?
從右邊的“main”開始看,箭頭進入應用程序——創建對象工廠。一個理想的解決方案是從各個特定的工廠中得到相應的對象,但這有點不切實際。不過當有機會這樣做的時候還是要使用,并且讓它們保持在業務邏輯之外。
?
再看底部,定義持久層(數據庫、文件訪問、網絡通信)用來保證信息的持久性。業務邏輯層是沒有對象知道持久層是如何工作的。
?
左邊則是交互機制。MVC比如Laravel、CakePHP,只能是交付機制而已。
?
當你看到應用程序架構或目錄時,你應該注意其架構是說明程序將要做什么,而不是使用什么技術或數據庫。
?
最后,為了確保所有的依賴項都指向業務邏輯層。用戶接口、工廠、數據庫則是具體的實現,而你永遠不要只依賴于它們。依賴倒置指向業務邏輯模塊,無需修改業務邏輯的依賴關系即可允許我們改變依賴。
?
關于設計模型
?
在使代碼變得易于修改和理解的過程中,設計模型扮演著非常重要的角色。從結構的角度來看,設計模式顯然是很有好處的,它們是行之有效并且深思熟慮的解決方案。更多關于設計模式內容,可以前往 Tuts+ Premium course 。
?
測試的力量
?
測試驅動開發(TDD)所編寫出來的代碼是很容易測試的。TDD迫使你尊重以上原則來編寫代碼,從而使你的程序更易被測試。單元測試運行速度很快,應該非常快,當你在一個類里使用10個對象來測試一個單獨方法時,你的代碼很有可能是有問題的。
?
總結
?
俗話說,實踐乃是檢驗真理的唯一標準,所以開發者只有在平時的工作中堅持使用這些原則才能編寫出理想的代碼。與此同時,不要輕易滿足于自己所編寫出的代碼,要努力讓你的代碼易于維護、干凈并且擁抱變化
?
原文: http://www.uml.org.cn/codeNorms/201303182.asp
?
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
