天天看點

軟體設計原則

軟體設計原則可以說是無數前輩在踩過無數坑之後總結出來的提醒後人遵循的一些基本思想、規範、模式。遵循這些原則,有利于我們做出良好的設計,比如達到高内聚低耦合、子產品劃厘清晰、源碼可讀性可維護性良好的效果。下面将對最廣為人知的八大原則進行說明,有些原則還會給出代碼示例。

6.1  KISS原則

KISS是“keep it simple, stupid”的簡稱,這種觀點認為,一個簡單的系統往往比複雜的系統運轉得更好,是以,在進行系統設計時應盡量保持簡單,避免不必要的複雜性。 

這裡強調的是不必要的複雜性,有時候一些複雜性不可避免,但我們無論在産品設計還是架構設計方面,在保持功能完整性的同時,應該盡量做減法,堅守KISS原則。

6.2  DRY原則

DRY是“Don't repeat yourself ”的簡稱,這個原則的目标在于通過抽象的手段來減少重複的模式,避免備援,包括設計、代碼等方面的備援,強調系統中的每個實體元素都是單一的、無歧義的。DRY原則運用好的話,系統中任何元素的修改都不會導緻邏輯上跟它無關的元素的修改,而邏輯上相關的元素即使被修改,也是以統一的可預測的方式被修改。

怎麼做到遵守DRY原則呢?本人認為要依靠抽象思維,我們在設計系統和資料結構時,盡量了解透徹系統中所存在的實體及其關系,将共性的内容抽出來成為一個抽象層,具體的實體再從抽象層派生,這裡其實就對應了面向對象裡面繼承的概念,繼承使得代碼可以被複用。

6.3  開閉原則

開閉原則也叫Open-Close原則,大緻思想是:軟體實體(包括類、子產品、函數等)應該對擴充開放,而對修改關閉。以類為例來說明,就是你可以從一個基類派生新的類作為子類,子類可以擴充基類的功能,但不能修改基類的代碼。開閉原則也是最基本的原則,這一章中所有其它原則本身都遵守了開閉原則。

現實中,很多軟體中的插件,就是開閉原則思想的具體實踐。通常允許你通過添加插件的方式來擴充軟體的行為,而不允許修改軟體的一些基礎功能。如果允許你修改軟體的基本功能,那麼有一天,這個軟體具備的功能和軟體原來聲稱的功能大相徑庭,想想都覺得是件詭異的事情。

6.4  裡氏替換原則

裡氏替換原則(Liskov substitution)可以表述為:所有出現基類的地方都可以由子類來代替,并且替換後不需要修改其它代碼系統還可以繼續正常工作。這樣看來,裡氏替換原則也符合開閉原則,不修改基類,而是将子類替換基類,就能夠達到擴充系統功能的目的。

怎麼運用這個原則呢?個人認為在設計系統時,要關注系統實體間的關系(比如是否是繼承關系)和擴充性,這兩個特性結合起來的地方可能就是使用了裡氏替換原則。

6.5  依賴倒置原則

依賴倒置原則是軟體子產品解耦的一種方式,其核心思想是:高層子產品不應該依賴于底層子產品,它們都應該依賴于抽象,也就是抽象不能依賴于細節,細節應該依賴抽象。

前後端分離架構就是一個例子,前端隻管使用ajax請求服務端的API,不在乎伺服器端API服務是如何實作和部署的,隻需要API接口遵守協定(比如HTTP)就好了。

6.6  單一職責原則

顧名思義,單一職責原則的核心思想是:一個類,隻做一件事情,隻有一個理由引發它的改變。這也和Unix的設計哲學之一“Do One Thing and Do It Well”有異曲同工之妙,即隻做一件事,并把它做好。

這個原則說起來最簡單,而實際上執行起來不容易,稍微不小心就容易違背它。隻做一件事,對應到軟體設計裡邊,就是一個子產品(類、函數)盡量少做事情,但要做好,這樣也符合我們常說的高内聚低耦合的設計要求,是以在單一原則裡邊,我們需要注意的是子產品(類、函數)的邊界和職責劃分,思考怎樣劃分才能符合該原則,這裡就不舉例子了。

6.7  接口隔離原則

接口隔離原則是面向對象設計的一個原則,倡導将接口分離,即使用者不需要實作他使用不到的接口。比如一個接口定義有兩個方法:

interface ShapeInterface {

  public function area();

  public function volume();

而有時候實作類隻使用到其中一個方法,但程式設計語言規範又迫使派生類去實作基類的兩個接口。如果遵循接口隔離原則,這時候應該将接口分解為兩個接口,然後實作類根據需要可以隻實作其中一個接口,如果需要兩個接口方法都實作,就去實作兩個接口好了。代碼如下:

    public function area();

}

interface SolidShapeInterface {

    public function volume();

class Cuboid implements ShapeInterface,  SolidShapeInterface {

    public function area() { }

    public function volume() {}

原則是一種思想,并不局限于某種程式設計語言,上面以Java代碼為例來說明,其他語言中可以根據各個語言的不同規範采用不同的方式來是接口符合這個原則。

6.8  最少知識原則

最少知識原則(Least Knowledge Principle)也叫迪米特法則(Law of Demeter),其來源于1987年荷蘭大學的一個叫做Demeter的項目。Craig Larman把Law of Demeter又稱作“不要和陌生人說話”,其核心思想是提倡一個子產品隻知道自己子產品内部的東西,對其它子產品知之甚少,與其它子產品的互動都是通過接口來實作,這個原則可以引導我們完成低耦合的系統設計。

上面說到的是子產品,那其實我們可以小到一個類,一個函數,都可以遵循最少知識原則。比如我們盡量将類的成員變量聲明為私有的,隻能通過類的公共方法來通路該變量,外部不能直接使用該變量。在函數裡面,我們盡量使用局部變量,少用全局變量,因為局部變量隻在這個函數内有效,全局變量可以共用,使得函數的耦合性變高了。是以,提倡使用局部變量,也是遵循了最少知識原則。

6.9  好萊塢明星原則

好萊塢明星原則叫IOC(Inversion of control)原則或者控制反轉原則,英文裡比較形象的說法就是“Don’t call me, I will call you back”,源于這麼一個現象:好萊塢的經紀人們一般不希望你去聯系他們,而是他們會在需要的時候來聯系你,因為主動權确實是掌握在他們手上,耍大牌不用商量。   

傳統的程式設計方式是,使用者代碼直接調用底層庫函數,而控制反轉原則是底層架構反過來調用使用者編寫的代碼,類似基于事件的程式設計。當事件觸發時,底層架構代碼被調用,然後再反過來調用使用者自定義的事件處理程式。這樣來看的話,就是架構提供了程式的引擎和接口定義,使用者代碼負責接口實作,應用程式的開發模式就掌控在架構開發者的手中。引用台灣著名架構師高煥堂的話,就是好萊塢明星原則保證了強盛,保證了底層架構的龍頭地位。    

這裡我們舉個簡單的例子吧,比如我們在進行GUI程式設計時,通常要為控件編寫一個OnCreate函數,但是這個函數我們從沒有主動去調用它,而是架構在适當的時機來調用,這就是将控制權交給了底層架構,完成了控制反轉。

6.10  面向接口原則

面向對象設計模式中有一個模式叫橋接模式(Bridge pattern),提倡基于接口程式設計,英文裡邊的說法是:Program to an interface, not an implementation。因為同一個接口可以衍生出各種不同的實作,接口和實作分離使得使用者程式可以根據不同的情況選擇不同的實作,實作的修改獨立于接口的調用者,這樣當需要修改實作代碼時,接口調用者是無感覺的,這樣也就降低了軟體的耦合度。

下面我們以Java代碼為例來看一下如何面向接口程式設計,首先,我們有一個顯示接口,定義如下:

interface displayModule {

  public void display();

顯示器類,實作displayModule接口

public class Monitor implements displayModule{

   public void display() {  

    System.out.println(“Display through   Monitor”);

   }

投影儀類,實作displayModule接口

public class Projector implements displayModule

{

    public void display(){

  System.out.println(“Display through projector”);

    }

主機類,聚合了顯示接口

public class Computer

   // 面向接口程式設計

   displayModule dm;   

   Public void setDisplayModule

   (displayModule dm)

   {

      this.dm=dm;

   public void display()

       dm.display();

程式主函數,類似于一個裝配器

public static void main(String args[]) 

  Computer cm = new Computer();

  displayModule dm = new Monitor();

  displayModule dm1=new Projector();

  cm.setDisplayModule(dm);

  cm.display();

  cm.setDisplayModule(dm1);

上面的例子中,Computer類聚合了displayModule 接口,然後通過一個方法setDisplayModule來設定具體的接口實作對象,比如顯示器或者投影儀。主程式就類似于裝配器,程式作者就像裝配勞工,根據需要裝配不同的插件(顯示器或者投影儀對象)。如果Computer類聚合的不是接口而是某個實作類,也就是直接聚合某個插件,這樣就不需要方法setDisplayModule了,但是失去了靈活性,裝配勞工也就無法根據需要裝配不同的插件了。

節選自本人作品:《漫談中小企業研發技術棧》第六章。

歡迎關注公衆号:

軟體設計原則