要了解設計模式,首先得清楚什麼是模式。什麼是模式?模式即解決一類問題的方法論,簡單得來講,就是將解決某類問題的方法概括總結到理論高度,就造成了模式。java
設計模式就是將代碼設計經驗概括總結到理論高度而造成的。其目的就在於:1)可重用代碼,2)讓代碼更容易爲他人理解,3)保證代碼的可靠性。c++
使用面向對象的語言很容易,可是作到面向對象卻很難。更多人用的是面向對象的語言寫出結構化的代碼,想一想本身編寫的代碼有多少是不用修改源碼能夠真正實現重用,或者能夠實現拿來主義。這是一件很正常的事,我在學習過程中,老師們老是在說c到c++的面向對象是一種巨大的進步,面向對象也是極爲難以理解的存在;而在開始的學習過程當中,我發現c++和c好像差異也不大,不就是多了一個類和對象嗎?但隨着愈發深刻的學習使我發現,事實並非那麼簡單,老師們舉例時老是喜歡用到簡單的對象羣體,好比:人,再到男人、女人,再到擁有具體家庭身份的父親、母親、孩子。用這些來講明類、對象、繼承......彷佛都顯得面向對象是一件垂手可得的事。程序員
但事實真是如此嗎?封裝、粒度、依賴關係、靈活性、性能、演化、複用等等,當這些在一個系統當中交錯相連,互相耦合,甚至有些東西還互相沖突時,你會發現本身可能連將系統對象化都是那麼的困難。面試
而在解決這些問題的過程中,也就慢慢造成了一套被反覆使用、爲多數人知曉、再由人分類編目的代碼設計經驗總結——設計模式。算法
模式既然做爲一套解決方案,天然不多是沒有規律而言的,而其所遵循的內在規律就是設計原則。在學習設計模式的過程中,不能脫離原則去看設計模式,而是應該透過設計模式去理解設計原則,只有深深地把握了設計原則,才能寫出真正的面向對象代碼,甚至創造本身的模式。編程
開閉原則(Open Close Principle)設計模式
開閉原則的意思是:對擴展開放,對修改關閉。在程序須要進行拓展的時候,不要去修改原有的代碼。這樣是爲了使程序的擴展性更好,更加易於維護和升級。而想要達到這樣的效果,就須要使用接口和抽象類。數據結構
里氏替換原則(Liskov Substitution Principle)架構
里氏替換原則中說,任何基類能夠出現的地方,子類必定能夠出現。也就是說只有當派生類能夠替換掉基類,且軟件單位的功能不受到影響時,基類才能真正被複用,而派生類也可以在基類的基礎上增長新的行爲。里氏代換原則是對開閉原則的補充。實現開閉原則的關鍵步驟就是抽象化,而基類與子類的繼承關係就是抽象化的具體實現,因此里氏代換原則是對實現抽象化的具體步驟的規範。app
依賴倒置原則(Dependence Inversion Principle)
依賴倒置原則是開閉原則的基礎,具體內容:抽象不該該依賴具體,而是具體應當依賴抽象;高層模塊不該該依賴底層模塊,而是高層和底層模塊都要依賴抽象。由於抽象纔是穩定的,這個原則想要說明的就是針對接口編程。
接口分離原則(Interface Segregation Principle)
這個原則的意思是:使用多個隔離的接口,比使用單個接口要好。它還有另一個意思是:下降類之間的耦合度。這個原則所要求的就是儘可能將接口最小化,避免一個接口當中擁有太多不相關的功能。
迪米特法則,又稱最少知道原則(Demeter Principle)
最少知道原則是指:若是兩個軟件實體無須直接通訊,那麼就不該當發生直接的相互調用,能夠經過第三方轉發該調用。其目的是下降類之間的耦合度,提升模塊的相對獨立性。迪米特法則在解決訪問耦合方面有着很大的做用,可是其自己的應用也有着一個很大的缺點,就是對象之間的通訊形成性能的損失,這是在使用過程當中,須要去折衷考慮的。
組合複用原則(Composite Reuse Principle)
組合複用原則或者說組合優先原則,也就是在進行功能複用的過程中,組合每每是比繼承更好的選擇。這是由於繼承的形式會使得父類的實現細節對子類可見,從而違背了封裝的目的。
單一職責原則(Single Responsibility Principle)
一個類只容許有一個職責,即只有一個致使該類變動的緣由。類職責的變化每每就是致使類變化的緣由:也就是說若是一個類具備多種職責,就會有多種致使這個類變化的緣由,從而致使這個類的維護變得困難。
設計模式是設計原則在應用體現,設計原則是解決面向對象問題處理方法。在面對訪問耦合的狀況下,有針對接口編程、接口分離、迪米特法則;處理繼承耦合問題,有里氏替換原則、優先組合原則;在保證類的內聚時,能夠採用單一職責原則、集中類的信息與行爲。這一系列的原則都是爲了一個目的——儘量的實現開閉。設計模式不是萬能的,它是設計原則互相取捨的成果,而學習設計模式是如何抓住變化和穩定的界線纔是設計模式的真諦。
從目的來看,即模式是用來完成什麼工做的;能夠劃分爲建立型、結構型和行爲型。建立型模式與對象的建立有關,結構型模式處理類或對象的組合,行爲型模式對類和對象怎樣分配職責進行描述。
從範圍來看,即模式是做用於類仍是對象;能夠劃分爲類模式和對象模式。類模式處理類和子類之間的關係,這些關係經過繼承創建,是靜態的,在編譯時刻就肯定下來了;對象模式處理對象間的關係,這些關係能夠在運行時刻變化,更加具備動態性。
組合之下,就產生了如下六種模式類別:
類建立型模式:將對象的建立工做延遲到子類中。
對象建立型模式:將對象的建立延工做遲到另外一個對象的中。
類結構型模式:使用繼承機制來組合類。
對象建立型模式:描述對象的組裝形式。
類行爲型模式:使用繼承描述算法和控制流。
對象行爲型模式:描述了一組對象怎樣協做完成單個對象所沒法完成的任務。
GOF(「四人組」)對設計模式的分類更多的是從用途方法進行劃分,而如今,咱們但願從設計模式中變化和穩定結構分隔上來理解全部的設計模式,或許有着不一樣的收穫。
首先要明白的是,得到最大限度複用的關鍵在於對新需求和已有需求發生變化的預見性,這也就要求系統設計可以相應地改進。而設計模式能夠確保系統以特定的方式變化,從而避免系統的從新設計,而且設計模式一樣容許系統結構的某個方面的變化獨立於其餘方面,這樣就在必定程度上增強了系統的健壯性。
根據封裝變化,能夠將設計模式劃分爲:組件協做、單一職責、對象建立、對象性能、接口隔離、狀態變化、數據結構、行爲變化以及領域問題等等。
現代軟件專業分工以後的第一個結果就是「框架與應用程序的劃分」,「組件協做」就是經過晚期綁定,來實現框架與應用程序之間的鬆耦合,是兩者之間協做時經常使用的模式。其典型模式就是模板方法、策略模式和觀察者。
定義一個操做中的算法的骨架,並將其中一些步驟的實現延遲到子類中。模板方法使得子類能夠重定義一個算法的步驟而不會改變算法的結構。
程序開發庫和應用程序之間的調用。假設如今存在一個開發庫,其內容是實現對一個文件或信息的操做,操做包含:open、read、operation、commit、close。可是呢!只有open、commit、close是肯定的,其中read須要根據具體的operation來肯定讀取方式,因此這兩個方法是須要開發人員本身去實現的。
那咱們第一次的實現可能就是這種方式:
//標準庫實現
public class StdLibrary {
public void open(String s){
System.out.println("open: "+s);
}
public void commit(){
System.out.println("commit operation!");
}
public void close(String s){
System.out.println("close: "+s);
}
}複製代碼
//應用程序的實現
public class MyApplication {
public void read(String s,String type){
System.out.println("使用"+type+"方式read: "+s);
}
public void operation(){
System.out.println("operation");
}
}
//或者這樣實現
public class MyApplication extends StdLibrary{
public void read(String s,String type){
System.out.println("使用"+type+"方式read: "+s);
}
public void operation(){
System.out.println("operation");
}
}複製代碼
//這裏兩種實現方式的代碼調用寫在一塊兒,就不分開了。
public class MyClient {
public static void main(String[] args){
//方式1
String file = "ss.txt";
StdLibrary lib = new StdLibrary();
MyApplication app = new MyApplication();
lib.open(file);
app.read(file,"STD");
app.operation();
lib.commit();
lib.close(file);
//方式2
MyApplication app = new MyApplication();
app.open(file);
app.read(file,"STD");
app.operation();
app.commit();
app.close(file);
}
}複製代碼
這種實現,不管是方式1仍是方式2,對於僅僅是做爲應用來講,固然是能夠的。其問題主要在什麼地方呢?就方式1 而言,他是必需要使用者瞭解開發庫和應用程序兩個類,纔可以正確的去應用。
方式2相較於方式1,使用更加的簡單些,可是仍然有不完善的地方,就是調用者,須要知道各個方法的執行順序,這也是1和2共同存在的問題。而這恰好就是Template Method發揮的時候了,一系列操做有着明確的順序,而且有着部分的操做不變,剩下的操做待定。
//按照Template Method結構能夠將標準庫做出以下修改
public abstract class StdLibrary {
public void open(String s){
System.out.println("open: "+s);
}
public abstract void read(String s, String type);
public abstract void operation();
public void commit(){
System.out.println("commit operation!");
}
public void close(String s){
System.out.println("close: "+s);
}
public void doOperation(String s,String type){
open(s);
read(s,"STD");
operation();
commit();
close(s);
}
}複製代碼
在修改過程當中,將原來的類修改爲了抽象類,而且新增了兩個抽象方法和一個doOperation()
。經過使用抽象操做定義一個算法中的一些步驟,模板方法肯定了它們的前後順序,但它容許Library和Application子類改變這些具體的步驟以知足它們各自的需求,而且還對外隱藏了算法的實現。固然,若是標準庫中的不變方法不能被重定義,那麼就應該將其設置爲private或者final。
//修改事後的Appliaction和Client
public class MyApplication extends StdLibrary {
@Override
public void read(String s, String type){
System.out.println("使用"+type+"方式read: "+s);
}
@Override
public void operation(){
System.out.println("operation");
}
}
public class MyClient {
public static void main(String[] args){
String file = "ss.txt";
MyApplication app = new MyApplication();
app.doOperation(file,"STD");
}
}複製代碼
模板方法的使用在類庫當中極爲常見,尤爲是在c++的類庫當中,它是一種基本的代碼複用技術。這種實現方式,產生了一種反向的控制結構,或者咱們稱之爲「好萊塢法則」,即「別找咱們,咱們找你」;換名話說,這種反向控制結構就是父類調用了子類的操做(父類中的doOperation()
調用了子類實現的read()
和operation()
),由於在平時,咱們的繼承代碼複用更多的是調用子類調用父類的操做。
結構
參與者
AbstractClass(StdLibrary)
定義抽象的原語操做(可變部分)。
實現一個模板方法(templateMethod()
),定義算法的骨架。
ConcreteClass(具體的實現類,如MyApplication)
實現原語操做以完成算法中與特定子類相關的步驟。
除了以上參與者以外,還能夠有OperatedObject這樣一個參與者即被操做對象。好比對文檔的操做,文檔又有不一樣的類型,如pdf、word、txt等等;這種狀況下,就須要根據不一樣的文檔類型,定製不一樣的操做,即一個ConcreteClass對應一個OperatedObject,至關於對結構當中由一個特定操做對象,擴展到多個操做對象,而且每一個操做對象對應一個模板方法子類。
適用性
對於模板方法的特性,其能夠應用於下列狀況:
相關模式
Factory Method常常被Template Method所調用。好比在參與者當中提到的,若是須要操做不一樣的文件對象,那麼在操做的過程當中就須要read()
方法返回不一樣的文件對象,而這個read()
方法不正是一個Factory Method。
Strategy:Template Method使用繼承來改變算法的一部分,而Strategy使用委託來改變整個算法。
思考
意圖
定義一系列的算法,把它們一個個封裝起來,而且使它們可相互替換。Strategy使得算法能夠獨立於使用它的客戶而變化。
實例
策略模式是一種很是經典的設計模式,可能也是你們常常所見到和使用的設計模式;重構過程當中選擇使用策略模式的一個很是明顯的特徵,就是代碼當中出現了多重條件分支語句,這種時候爲了代碼的擴展性,就能夠選擇使用策略模式。
好比正面這樣的代碼,實現一個加減乘除運算的操做。
public class Operation {
public static void main(String[] args) {
binomialOperation(1,1,'+');
binomialOperation(1,3,'-');
binomialOperation(1,2,'*');
binomialOperation(1,1,'/');
binomialOperation(1,0,'/');
}
public static int binomialOperation(int num1,int num2,char ch){
switch(ch){
case '+':
return num1+num2;
case '-':
return num1+num2;
case '*':
return num1*num2;
case '/':
if(num2!=0){return num1/num2;}
else {
System.out.println("除數不能爲0!");
}
}
return num2;
}
}複製代碼
上面的代碼徹底能夠實現咱們想要的功能,可是若是如今需求有變,須要再增長一個‘與’和‘或’的二目運算;那在這種狀況下,勢必須要去修改源碼,這樣就違背了開閉原則的思想。所以,使用策略模式,將上面代碼修改成下列代碼。
//Strategy
public interface BinomialOperation {
public int operation(int num1,int num2);
}
public class AddOperation implements BinomialOperation {
@Override
public int operation(int num1, int num2) {
return num1+num2;
}
}
public class SubstractOperation implements BinomialOperation {
@Override
public int operation(int num1, int num2) {
return num1-num2;
}
}
public class MultiplyOperation implements BinomialOperation {
@Override
public int operation(int num1, int num2) {
return num1*num2;
}
}
public class DivideOperation implements BinomialOperation {
@Override
public int operation(int num1, int num2) {
if(0!=num2){
return num1/num2;
}else{
System.out.println("除數不能爲0!");
return num2;
}
}
}
//Context
public class OperatioContext {
BinomialOperation binomialOperation;
public void setBinomialOperation(BinomialOperation binomialOperation) {
this.binomialOperation = binomialOperation;
}
public int useOperation(int num1,int num2){
return binomialOperation.operation(num1,num2);
}
}
public class Client {
public static void main(String[] args) {
OperatioContext oc = new OperatioContext();
oc.setBinomialOperation(new AddOperation());
oc.useOperation(1,2);
//......
}
}複製代碼
代碼很簡單,就是將運算類抽象出來,造成一種策略,每一個不一樣的運算符對應一個具體的策略,而且實現本身的操做。Strategy和Context相互做用以實現選定的算法。當算法被調用時,Context能夠將自身做爲一個參數傳遞給Strategy或者將所須要的數據都傳遞給Strategy,也就是說 `OperationContext`中`useOperation()`的`num1`和`num2`能夠做爲爲`OperationContext`類的屬性,在使用過程當中直接將`OperationContext`的對象做爲一個參數傳遞給`Strategy`類便可。
經過策略模式的實現,使得增長新的策略變得簡單,可是其缺點就在於客戶必須瞭解 不一樣的策略。複製代碼
*結構 *
參與者
Strategy (如BinomialOperation)
定義全部支持的算法的公共接口。Context使用這個接口來調用某具體的Strategy中定義的算法。
ConcreteStrategy(如AddOperation...)
根據Strategy接口實現具體算法。
Context(如OperationContext)
適用性
當存在如下狀況時,可使用策略模式:
相關模式
Flyweight(享元模式)的共享機制能夠減小須要生成過多Strategy對象,由於在使用過程當中,策略每每是能夠共享使用的。
思考
Strategy和Context之間的通訊問題。在Strategy和Contex接口中,必須使得ConcreteStrategy可以有效的訪問它所須要的Context中的任何數據,反之亦然。這種實現通常有兩種方式:
1)讓Context將數據放在參數中傳遞給Strategy——也就是說,將數據直接發送給Strategy。這可使得Strategy和Context之間解耦(印記耦合是能夠接受的),但有可能會有一些Strategy不須要的數據。
2)將Context自身做爲一個參數傳遞給Strategy,該Strategy顯示的向Context請求數據,或者說明在Strategy中保留一個Context的引用,這樣便不須要再傳遞其餘的數據了。
讓Strategy成爲可選的。換名話說,在有些實現過程當中,客戶能夠在不指定具體策略的狀況下使用Context完成本身的工做。這是由於,咱們能夠爲Context指定一個默認的Strategy的存在,若是有指定Strategy就使用客戶指定的,若是沒有,就使用默認的。
意圖
定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都獲得通知並被自動更新。
實例
觀察者模式很常見於圖形用戶界面當中,好比常見的Listener。觀察者模式可使得應用數據的類和負責界面表示的類能夠各自獨立的複用。好比,當界面當中存在一個輸入表單,在咱們對錶單進行輸入的時候,界面上又會顯示這樣一個數據的柱狀圖,以些來對比各項數據。其僞碼能夠描述成下列這種形式:Histogram
做爲柱狀圖類只須要負責接收數據而且顯示出來,InputForm
做爲一個輸入表單。在這個 過程當中,只要InputForm
中的數據發生變化,就相應的改變Histogram
的顯示。
這種實現方式,明顯在InputForm
中產生了一種強耦合,若是顯現圖形發生變化,如今不須要顯示爲一個柱狀圖而是一個餅狀圖,勢必又要去修改源碼。
public class Histogram {
public void draw(int[]nums){
for (int i:nums ) {
System.out.print(i+" ");
}
}
}
public class InputForm {
private int[] data;
Histogram histogram;
public InputForm(Histogram histogram){
this.histogram = histogram;
show();
}
public void change(int... data){
this.data = data;
show();
}
public void show(){
histogram.draw(data);
}
}
public class Client {
public static void main(String[] args) {
InputForm inputForm = new InputForm(new Histogram());
inputForm.change(3,4,5);
inputForm.change(5,12,13);
}
}複製代碼
同時,InputForm
和顯示圖形之間的關係,恰好符合觀察者模式所說的一個對象的狀態變化,引發其餘對象的更新,同時兼顧考慮開閉問題,能夠將Histogram
和PieChart
公共特性提取出來,造成一個Graph
接口。另外,有可能InputFrom
不僅須要顯示一種圖表,而是須要同時將柱狀圖和餅狀圖顯示出來,所以在InputFrom
中定義的是一個List的結構來存放全部的相關顯示圖形。
//Observer
public interface Graph {
public void update(Input input);
public void draw();
}
public class Histogram implements Graph {
private InputForm inputForm;
public Histogram(InputForm inputForm){
this.inputForm = inputForm;
}
@Override
public void update(Input inputForm) {
if(this.inputForm == inputForm){
draw();
}
}
@Override
public void draw(){
System.out.println("柱狀圖:");
for (int i: inputForm.getData()) {
System.out.println(i+" ");
}
System.out.println();
}
}
public class PieChart implements Graph {
private InputForm inputForm;
public PieChart(InputForm inputForm){
this.inputForm = inputForm;
this.inputForm.addGraph(this);
draw();
}
@Override
public void update(Input inputForm) {
if(this.inputForm == inputForm){
draw();
}
}
@Override
@Override
public void draw(){
System.out.println("餅狀圖:");
for (int i: inputForm.getData()) {
System.out.println(i+" ");
}
System.out.println();
}
}
複製代碼
在實際的應用過程當中,既然有輸入表單的形式,也有可能以其餘的形式輸入數據,爲了之後的擴展,能夠將輸入形式抽象出來,造成一個Input
接口,以便後續的擴展。
//Subject 目標對象
public interface Input {
public void addGraph(Graph graph);
public void removeGraph(Graph graph);
public void notifyGraphs();
}
public class InputForm implements Input {
private int[] data;
private List<Graph graphs = new List;
public void change(int...data){
this.data = data;
notifyGraphs();
}
public int[] getData() {
return data;
}
@Override
public void addGraph(Graph graph){
graphs.add(graph);
}
@Override
public void removeGraph(Graph graph){
graphs.remove(graph);
}
@Override
public void notifyGraphs(){
for (Graph g:graphs ) {
g.update(this);
}
}
}複製代碼
public class Client {
public static void main(String[] args) {
InputForm inputForm = new InputForm();
Histogram h = new Histogram(inputForm);
PieChart p = new PieChart(inputForm);
inputForm.change(1,5,6,9,8);
inputForm.change(2,4,6,8);
}
}複製代碼
結構
參與者
Subject(目標,如Input)
Observer(觀察者,如Graph)
爲那些在目標發生變化時須要獲取通知的對象定義一個更新接口。
ConcreteSubject(具體目標,如InputForm)
ConcreteObserver(具體觀察者,如Histogram)
適用性
在如下任一狀況下可使用觀察者模式:
相關模式
Mediator(中介者模式):經過封裝複雜的更新語義,可使用一個ChangeManager來充當目標和觀察者之間的中介。在目標的狀態變化過程當中,有些狀態變化可能只是中間臨時變化,而還未到最終結果,但這可能引發觀察者的更新,這種頻繁的更新形成的就是通訊代價和性能損失。所以,採用一個ChangeManager能夠更好去管理更新操做。
Singleton(單例模式):ChangeManager可使用Singleton模式來保證它是惟一的而且是可全局訪問的。
思考
目標與觀察者之間的映射。一個目標對象跟蹤它應通知的觀察者的最簡單方法是顯式地在目標當中保存對它們的引用,但當目標過多而觀察者少時,這樣存儲的結構可能代價太高。其一個解決辦法就是用時間換空間,用一個關聯查找機制(例如一個hash表的形式)來維護目標到觀察者的映射。這樣沒有觀察者的目標天然不會產生存儲上的開銷,可是因爲關聯機制的存在,就至關於在訪問觀察者的過程當中多了一個步驟,就增長了訪問觀察者的開銷。
一個目標能夠有不少觀察者,一個觀察者也一樣能夠觀察不少目標。這種狀況下,就須要多觀察者的update
接口做出必定的改變,使得觀察者可以知道是那個目標對象發來通知。
誰來觸發更新。一是在對目標狀態值進行設定時,自動去調用通知信息。這樣客戶就不須要去調用Notify()
,缺點就在於多個連續的操做就會產生連續的更新,形成效率低下。二是客戶本身選擇合適的狀況下去調用Notify()
,這種觸發方式優勢在於客戶能夠在操做完成目標對象以後,一次性更新,避免了中間無用的更新。缺點在於一旦客戶可能沒有調用通知,就容易出錯。
如何保證發出通知前目標的狀態自身是一致的。確保發出通知前目標狀態一致這很重要,由於觀察者在更新狀態時,須要查詢目標的當前狀態。這就須要在代碼序列中,保證通知是在目標狀態修改完成以後進行的,這時就能夠採用Template Method來固定操做的順序。
在這篇文章當中,沒有按照GOF對設計模式的分類來對設計模式進行描述,而是在實例的基礎上,運用重構的技巧:從靜態到動態、從早綁定到晚綁定、從繼承到組合、從編譯時依賴到運行時依賴、從緊耦合到鬆耦合。經過這樣一種方式來理解設計模式,尋找設計模式中的穩定與變化。
在上面提到的三種模式中,它們對象間的綁定關係,都是動態的,能夠變化的,經過這樣的方式來實現協做對象之間的鬆耦合,這也是「組件協做」一個特色。
還有就是關於耦合的理解,有的時候耦合是不可避免的,耦合的接受程度是相對而言的,這取決於咱們在實現過程中對變化的封裝和穩定的抽象折衷,這也是咱們學習設計模式的目的,就是如何利用設計模式來實現這樣一種取捨。
對設計模式細節描述過程,體現的是我在學習設計模式過程當中的一種思路。學習一個設計模式,首先要了解它是要幹什麼的。而後從一個例子出發,去理解它,思考它的一個實現過程。再而後,概括它的結構,這個結構不只僅是類圖,還包括類圖中的各個協做者是須要完成什麼的功能、提供什麼樣的接口、要保存哪些數據以及各各協做者之間是如何協做的,或者說是依賴關係是怎樣的。最後,再考慮與其餘模式的搭配,思考模式的實現細節。
這裏呢,暫時只寫出了三種模式,後續的過程當中,將會一一地介紹其餘的模式。
最後,最近不少小夥伴找我要Linux學習路線圖,因而我根據本身的經驗,利用業餘時間熬夜肝了一個月,整理了一份電子書。不管你是面試仍是自我提高,相信都會對你有幫助!目錄以下:
免費送給你們,只求你們金指給我點個贊!
也但願有小夥伴能加入我,把這份電子書作得更完美!
推薦閱讀: