設計的一些概念

南北東西東發表於2019-02-16

Q1 什麼是設計模式?

1、設計模式(Design pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性。設計模式使程式碼編制真正工程化,模組化。
2、任何設計模式的目的都是:減少程式碼冗餘度,提高程式碼複用性,解耦合。

Q2 軟體設計的七大原則怎麼理解?

“開-閉”原則(Open-Closed Principle)是物件導向的可複用設計(Object Oriented Design或OOD)的基石。其他設計原則(里氏代換原則、依賴倒轉原則、合成/聚合複用原則、迪米特法則、介面隔離原則)是實現“開-閉”原則的手段和工具。

1、開-閉原則(Open-Closed Principle, OCP)
“Software entities should be open for extension,but closed for modification”—軟體實體應當對擴充套件開放,對修改關閉。
可以這樣理解:軟體系統中包含的各種元件,例如模組(Modules)、類(Classes)以及功能(Functions)等等,應該在不修改現有程式碼的基礎上,引入新功能。開閉原則中“開”,是指對於元件功能的擴充套件是開放的,是允許對其進行功能擴充套件的;開閉原則中“閉”,是指對於原有程式碼的修改是封閉的,即不應該修改原有的程式碼。

1.1、 實現開閉原則
a、實現開閉原則的關鍵就在於“抽象”。把系統的所有可能的行為抽象成一個抽象底層,這個抽象底層規定出所有的具體實現必須提供的方法的特徵。作為系統設計的抽象層,要預見所有可能的擴充套件,從而使得在任何擴充套件情況下,系統的抽象底層不需修改;同時,由於可以從抽象底層匯出一個或多個新的具體實現,可以改變系統的行為,因此係統設計對擴充套件是開放的。
b、其它原則和方法如:里氏代換原則(LSP)、依賴倒轉原則(DIP)、介面隔離原則(ISP)以及抽象類(Abstract Class)、介面(Interace)等等,都可以看作是開閉原則的實現方法。

2、里氏代換原則(Liskov Substitution Principle,LSP)
a、LSP是繼承複用的基石,只有當衍生類可以替換掉基類,軟體單位的功能不受到影響時,基類才能真正被複用,而衍生類也能夠在基類的基礎上增加新的行為;
b、里氏代換原則是對“開-閉”原則的補充,實現“開-閉”原則的關鍵步驟就是抽象化,而基類與子類的繼承關係就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規範。
c、在進行設計的時候,我們儘量從抽象類繼承,而不是從具體類繼承。如果從繼承等級樹來看,所有葉子節點應當是具體類,而所有的樹枝節點應當是抽象類或者介面。

取個栗子:

//正方形
public class Square
{
//...

    public function setWidth($width)
    {
        $this->width = $width;

        $this->height = $width;
    }

    public function setHeight($height)
    {
        $this->setWidth($height);
    }
}

//長方形
public class RectangleController
{
    public $width;
    public $height;

    public function setWidth($width)
    {
        $this->width = $width;
    }

    public function setHeight($height)
    {
        $this->height = $height;
    }
}

如果讓正方形當做是長方形的子類,會出現什麼情況?
a、現在我們假設有個客戶類,其中有個方法,規則是這樣的,測試傳入的長方形的寬度是否大於高度,如果滿足就停止下來,否則就增加寬度的值。現在我們來看,如果傳入的是基類長方形,這個執行的很好。根據LSP,我們把基類替換成它的子類,結果應該也是一樣的,但是因為正方形類的width和height會同時賦值,這個方法沒有結束的時候,條件總是不滿足,也就是說,替換成子類後,程式的行為發生了變化,它不滿足LSP。
b、那麼我們用第一種方案進行重構,我們構造一個抽象的四邊形類,把長方形和正方形共同的行為放到這個四邊形類裡面,讓長方形和正方形都是它的子類,問題就OK了。對於長方形和正方形,取width和height是它們共同的行為,但是給width和height賦值,兩者行為不同,因此,這個抽象的四邊形的類只有取值方法,沒有賦值方法。上面的例子中那個方法只會適用於不同的子類,LSP也就不會被破壞。

3、依賴倒置原則(Dependence Inversion Principle,DIP)
3.1、 概念
a、依賴倒轉原則就是要依賴於抽象,不要依賴於實現。(Abstractions should not depend upon details. Details should depend upon abstractions.)要針對介面程式設計,不要針對實現程式設計。(Program to an interface, not an implementation.)也就是說應當使用介面和抽象類進行變數型別宣告、引數型別宣告、方法返還型別說明,以及資料型別的轉換等。而不要用具體類進行變數的型別宣告、引數型別宣告、方法返還型別說明,以及資料型別的轉換等。要保證做到這一點,一個具體類應當只實現介面和抽象類中宣告過的方法,而不要給出多餘的方法。
b、傳統的過程性系統的設計辦法傾向於使高層次的模組依賴於低層次的模組,抽象層次依賴於具體層次。倒轉原則就是把這個錯誤的依賴關係倒轉過來。物件導向設計的重要原則是建立抽象化,並且從抽象化匯出具體化,具體化給出不同的實現。繼承關係就是一種從抽象化到具體化的匯出。抽象層包含的應該是應用系統的商務邏輯和巨集觀的、對整個系統來說重要的戰略性決定,是必然性的體現。具體層次含有的是一些次要的與實現有關的演算法和邏輯,以及戰術性的決定,帶有相當大的偶然性選擇。具體層次的程式碼是經常變動的,不能避免出現錯誤。
c、從複用的角度來說,高層次的模組是應當複用的,而且是複用的重點,因為它含有一個應用系統最重要的巨集觀商務邏輯,是較為穩定的。而在傳統的過程性設計中,複用則側重於具體層次模組的複用。依賴倒轉原則則是對傳統的過程性設計方法的“倒轉”,是高層次模組複用及其可維護性的有效規範。
d、特例:物件的建立過程是違背“開—閉”原則以及依賴倒轉原則的,但通過工廠模式,能很好地解決物件建立過程中的依賴倒轉問題。

3.2、關係
a、“開-閉”原則與依賴倒轉原則是目標和手段的關係。如果說開閉原則是目標,依賴倒轉原則是到達”開閉”原則的手段。如果要達到最好的”開閉”原則,就要儘量的遵守依賴倒轉原則,依賴倒轉原則是對”抽象化”的最好規範。
b、里氏代換原則是依賴倒轉原則的基礎,依賴倒轉原則是里氏代換原則的重要補充。

3.3、耦合(依賴)關係的種類
零耦合(Nil Coupling)關係:兩個類沒有耦合關係
具體耦合(Concrete Coupling)關係:發生在兩個具體的(可例項化的)類之間,經由一個類對另一個具體類的直接引用造成。
抽象耦合(Abstract Coupling)關係:發生在一個具體類和一個抽象類(或介面)之間,使兩個必須發生關係的類之間存有最大的靈活性。

3.4、如何把握耦合
我們應該儘可能的避免實現繼承,原因如下:
a、失去靈活性,使用具體類會給底層的修改帶來麻煩。
b、耦合問題,耦合是指兩個實體相互依賴於對方的一個量度。程式設計師每天都在(有意識地或者無意識地)做出影響耦合的決定:類耦合、API耦合、應用程式耦合等等。在一個用擴充套件的繼承實現系統中,派生類是非常緊密的與基類耦合,而且這種緊密的連線可能是被不期望的。如B extends A ,當B不全用A中的所有methods時,這時候,B呼叫的方法可能會產生錯誤!
我們必須客觀的評價耦合度,系統之間不可能總是鬆耦合的,那樣肯定什麼也做不了。

3.5、怎樣做到依賴倒轉
以抽象方式耦合是依賴倒轉原則的關鍵。抽象耦合關係總要涉及具體類從抽象類繼承,並且需要保證在任何引用到基類的地方都可以改換成其子類,因此,里氏代換原則是依賴倒轉原則的基礎。
在抽象層次上的耦合雖然有靈活性,但也帶來了額外的複雜性,如果一個具體類發生變化的可能性非常小,那麼抽象耦合能發揮的好處便十分有限,這時可以用具體耦合反而會更好。
層次化:所有結構良好的物件導向構架都具有清晰的層次定義,每個層次通過一個定義良好的、受控的介面向外提供一組內聚的服務。
依賴於抽象:建議不依賴於具體類,即程式中所有的依賴關係都應該終止於抽象類或者介面。儘量做到:
a、任何變數都不應該持有一個指向具體類的指標或者引用。
b、任何類都不應該從具體類派生。
c、任何方法都不應該覆寫它的任何基類中的已經實現的方法。

3.6、依賴倒轉原則的優缺點
依賴倒轉原則雖然很強大,但卻最不容易實現。因為依賴倒轉的緣故,物件的建立很可能要使用物件工廠,以避免對具體類的直接引用,此原則的使用可能還會導致產生大量的類,對不熟悉物件導向技術的工程師來說,維護這樣的系統需要較好地理解物件導向設計。
依賴倒轉原則假定所有的具體類都是會變化的,這也不總是正確。有一些具體類可能是相當穩定,不會變化的,使用這個具體類例項的應用完全可以依賴於這個具體型別,而不必為此建立一個抽象型別。

4、介面隔離原則(Interface Segregation Principle, ISP)
在講介面隔離原則之前,先明確一下我們的主角——介面。介面分為兩種:
◇ 例項介面(Object Interface),在Java中宣告一個類,然後用new關鍵字產生的一個例項,它是對一個型別的事物的描述,這是一種介面,比如你定義Person這個類,然後使用Person zhangSan = new Person()產生了一個例項,這個例項要遵從的標準就是Person這個類,Person類就是zhangSan的介面,疑惑?看不懂?不要緊,那是因為讓Java語言浸染的時間太長了,只要知道從這個角度來看,Java中的類也是一種介面;
◇ 類介面(Class Interface),Java中經常使用的interface關鍵字定義的介面。
主角已經定義清楚了,那什麼是隔離呢?它有兩種定義,如下所示:
◇ “Clients should not be forced to depend upon interfaces that they don`t use”——客戶端不應該依賴它不需用的介面。
◇ “The dependency of one class to another one should depend on the smallest possible interface”——類間的依賴關係應該建立在最小的介面上。

◇新事物的定義一般都比較難理解,晦澀難懂是正常的。我們把這兩個定義剖析一下,先說第一種定義:“客戶端不應該依賴它不需要介面”,那依賴什麼?依賴它需要的介面,客戶端需要什麼介面就提供什麼介面,把不需要的介面剔除掉,那就需要對介面進行細化,保證其純潔性;再看第二個定義:“類間的依賴關係應該建立在最小的介面上”,它要求是最小的介面,也是要求介面細化,介面純潔,與第一個定義如出一轍,只是一個事物的兩種不同描述。

◇我們可以把這兩個定義概括為一句話:建立單一介面,不要建立臃腫龐大的介面。再通俗一點講:介面儘量細化,同時介面中的方法儘量少。看到這裡大家有可能要疑惑了,這與單一職責原則不是相同的嗎?錯,介面隔離原則與單一職責的審視角度是不相同的,單一職責要求的是類和介面職責單一,注重的是職責,這是業務邏輯上的劃分,而介面隔離原則要求介面的方法儘量少。例如一個介面的職責可能包含10個方法,這10個方法都放在一個介面中,並且提供給多個模組訪問,各個模組按照規定的許可權來訪問,在系統外通過文件約束“不使用的方法不要訪問”,按照單一職責原則是允許的,按照介面隔離原則是不允許的,因為它要求“儘量使用多個專門的介面”,專門的介面指什麼?就是指提供給每個模組都應該是單一介面,提供給幾個模組就應該有幾個介面,而不是建立一個龐大的臃腫的介面,容納所有的客戶端訪問。

5、合成/聚合複用原則(Composite/Aggregate Reuse Principle,CARP)
5.1、概念
定義:在一個新的物件裡面使用一些已有的物件,使之成為新物件的一部分;新的物件通過向這些物件的委派達到複用這些物件的目的。應首先使用合成/聚合,合成/聚合則使系統靈活,其次才考慮繼承,達到複用的目的。而使用繼承時,要嚴格遵循里氏代換原則。有效地使用繼承會有助於對問題的理解,降低複雜度,而濫用繼承會增加系統構建、維護時的難度及系統的複雜度。
如果兩個類是“Has-a”關係應使用合成、聚合,如果是“Is-a”關係可使用繼承。”Is-A”是嚴格的分類學意義上定義,意思是一個類是另一個類的”一種”。而”Has-A”則不同,它表示某一個角色具有某一項責任。

5.2、什麼是合成?什麼是聚合?
a、合成(Composition)和聚合(Aggregation)都是關聯(Association)的特殊種類。
b、聚合表示整體和部分的關係,表示“擁有”。如賓士S360汽車,對賓士S360引擎、賓士S360輪胎的關係是聚合關係,離開了賓士S360汽車,引擎、輪胎就失去了存在的意義。在設計中, 聚合不應該頻繁出現,這樣會增大設計的耦合度。
c、合成則是一種更強的“擁有”,部分和整體的生命週期一樣。合成的新的物件完全支配其組成部分,包括它們的建立和湮滅等。一個合成關係的成分物件是不能與另一個合成關係共享的。
換句話說,合成是值的聚合(Aggregation by Value),而一般說的聚合是引用的聚合(Aggregation by Reference)。
d、明白了合成和聚合關係,再來理解合成/聚合原則應該就清楚了,要避免在系統設計中出現,一個類的繼承層次超過3層,則需考慮重構程式碼,或者重新設計結構。當然最好的辦法就是考慮使用合成/聚合原則。

5.3、通過合成/聚合來進行復用的優缺點
優點:
1) 新物件存取成分物件的唯一方法是通過成分物件的介面。
2) 這種複用是黑箱複用,因為成分物件的內部細節是新物件所看不見的。
3) 這種複用支援包裝。
4) 這種複用所需的依賴較少。
5) 每一個新的類可以將焦點集中在一個任務上。
6) 這種複用可以在執行時間內動態進行,新物件可以動態的引用與成分物件型別相同的物件。
7) 作為複用手段可以應用到幾乎任何環境中去。
缺點:就是系統中會有較多的物件需要管理。

5.4 通過繼承來進行復用的優缺點
優點:
1)新的實現較為容易,因為超類的大部分功能可以通過繼承的關係自動進入子類。
2)修改和擴充套件繼承而來的實現較為容易。
缺點:
1) 繼承複用破壞包裝,因為繼承將超類的實現細節暴露給子類。由於超類的內部細節常常是對於子類透明的,所以這種複用是透明的複用,又稱“白箱”複用。
2) 如果超類發生改變,那麼子類的實現也不得不發生改變。
3)從超類繼承而來的實現是靜態的,不可能在執行時間內發生改變,沒有足夠的靈活性。
4)繼承只能在有限的環境中使用。

6、迪米特法則(Law of Demeter LoD)
6.1概述
定義:一個軟體實體應當儘可能少的與其他實體發生相互作用。 這樣,當一個模組修改時,就會盡量少的影響其他的模組。擴充套件會相對容易。這是對軟體實體之間通訊的限制。它要求限制軟體實體之間通訊的寬度和深度。

6.2迪米特法則的其他表述:
1)只與你直接的朋友們通訊。
2)不要跟“陌生人”說話。
3)每一個軟體單位對其他的單位都只有最少的知識,而且侷限於那些與本單位密切相關的軟體單位。

6.3、狹義的迪米特法則
如果兩個類不必彼此直接通訊,那麼這兩個類就不應當發生直接的相互作用。如果其中的一個類需要呼叫另一個類的某一個方法的話,可以通過第三者轉發這個呼叫。
朋友圈的確定
“朋友”條件:
1)當前物件本身(this)
2)以參量形式傳入到當前物件方法中的物件
3)當前物件的例項變數直接引用的物件
4)當前物件的例項變數如果是一個聚集,那麼聚集中的元素也都是朋友
5)當前物件所建立的物件
任何一個物件,如果滿足上面的條件之一,就是當前物件的“朋友”;否則就是“陌生人”。
缺點:會在系統裡造出大量的小方法,散落在系統的各個角落。
與依賴倒轉原則互補使用

6.4、狹義的迪米特法則的缺點:
在系統裡造出大量的小方法,這些方法僅僅是傳遞間接的呼叫,與系統的商務邏輯無關。
遵循類之間的迪米特法則會是一個系統的區域性設計簡化,因為每一個區域性都不會和遠距離的物件有直接的關聯。但是,這也會造成系統的不同模組之間的通訊效率降低,也會使系統的不同模組之間不容易協調。

6.5、迪米特法則與設計模式
門面(外觀)模式和調停者(中介者)模式實際上就是迪米特法則的具體應用。

6.6、廣義的迪米特法則
迪米特法則的主要用意是控制資訊的過載。在將迪米特法則運用到系統設計中時,要注意下面的幾點:
1)在類的劃分上,應當建立有弱耦合的類。
2)在類的結構設計上,每一個類都應當儘量降低成員的訪問許可權。
3)在類的設計上,只要有可能,一個類應當設計成不變類。
4)在對其他類的引用上,一個物件對其物件的引用應當降到最低。

6.7、廣義迪米特法則在類的設計上的體現
1)優先考慮將一個類設定成不變類
2)儘量降低一個類的訪問許可權
3)謹慎使用Serializable
4)儘量降低成員的訪問許可權
5)取代C Struct
迪米特法則又叫作最少知識原則(Least Knowledge Principle或簡寫為LKP),就是說一個物件應當對其他物件有儘可能少的瞭解。

6.8、如何實現迪米特法則
迪米特法則的主要用意是控制資訊的過載,在將其運用到系統設計中應注意以下幾點:
1) 在類的劃分上,應當建立有弱耦合的類。類之間的耦合越弱,就越有利於複用。
2) 在類的結構設計上,每一個類都應當儘量降低成員的訪問許可權。一個類不應當public自己的屬性,而應當提供取值和賦值的方法讓外界間接訪問自己的屬性。
3) 在類的設計上,只要有可能,一個類應當設計成不變類。
4) 在對其它物件的引用上,一個類對其它物件的引用應該降到最低。

7、單一職責原則(Simple responsibility pinciple SRP)
1) 一個類,應該只有一個職責。每一個職責都是變化的一個軸線,如果一個類有一個以上的職責,這些職責就耦合在了一起。這會導致脆弱的設計。當一個職責發生變化時,可能會影響其它的職責。另外,多個職責耦合在一起,會影響複用性。我們可能只需要複用該類的某一個職責,但這個職責跟其它職責耦合在了一起,很難分離出來。

2) SRP中,把職責定義為“變化的原因”。如果你能想到N個動機去改變一個類,那麼這個類就具有多於一個的職責。這裡說的“變化的原因”,只有實際發生時才有意義。可能預測到會有多個原因引起這個類的變化,但這僅僅是預測,並沒有真的發生,這個類仍可看做具有單一職責,不需要分離職責。如果分離,會帶來不必要的複雜性。

3) 如果發現一個類有多於一個的職責,應該儘量解耦。如果很難解耦,也要分離介面,在概念上解耦。

4) 眾所周知,OOPL可以提高程式的封裝性、複用性、可維護性。但僅僅是“可以”。能不能實現OOPL的這些優點,要看具體怎麼做。如果一個類的程式碼非常混亂,各種功能的程式碼都混在一起,封裝性、複用性、可維護性無從談起。SRP是所有原則中最簡單的,也是最基本的一個。運用這個原則,可以提高類的內聚性,有助於充分發揮OOPL的優勢。

參考:
http://www.cnblogs.com/kinglo…

相關文章