如何編寫出擁抱變化的程式碼?

csdn發表於2013-02-26

  在實際的開發中,編寫出易維護和易接受變化的程式碼並非易事,想要實現可能更加困難重重:原始碼難於理解、依賴關係指向不明、耦合也很令人頭疼。難道就真的就沒有辦法了嗎?本文中我們一起探討幾個技術原則和一些編碼理念,讓你的程式碼跟著需求走,而且易維護易擴充。

  介紹些物件導向方法

  物件導向程式設計(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個物件來測試一個單獨方法時,你的程式碼很有可能是有問題的。

  總結

  俗話說,實踐乃是檢驗真理的唯一標準,所以開發者只有在平時的工作中堅持使用這些原則才能編寫出理想的程式碼。與此同時,不要輕易滿足於自己所編寫出的程式碼,要努力讓你的程式碼易於維護、乾淨並且擁抱變化。

  英文來自: net.tutsplus

相關文章