理解軟體設計模式

2019-08-10 08:09:00

設計模式可以幫助消除冗餘程式碼。學習如何利用 Java 使用單例模式、工廠模式和觀察者模式。

如果你是一名正在致力於電腦科學或者相關學科的程式設計師或者學生,很快,你將會遇到一條術語 “軟體設計模式software design pattern”。根據維基百科,“軟體設計模式是在平常的軟體設計工作中所遭遇的問題的一種通用的、可重複使用的解決方案”。我對該定義的理解是:當在從事於一個編碼專案時,你經常會思考,“嗯,這裡貌似是冗餘程式碼,我覺得是否能改變這些程式碼使之更靈活和便於修改?”因此,你會開始考慮怎樣分割那些保持不變的內容和需要經常改變的內容。

設計模式是一種通過分割那些保持不變的部分和經常變化的部分,讓你的程式碼更容易修改的方法。

不出意外的話,每個從事程式設計專案的人都可能會有同樣的思考。特別是那些工業級別的專案,在那裡通常工作著數十甚至數百名開發者;共同作業過程表明必須有一些標準和規則來使程式碼更加優雅並適應變化。這就是為什麼我們有了 物件導向程式設計(OOP)和 軟體框架工具。設計模式有點類似於 OOP,但它通過將變化視為自然開發過程的一部分而進一步發展。基本上,設計模式利用了一些 OOP 的思想,比如抽象和介面,但是專注於改變的過程。

當你開始開發專案時,你經常會聽到這樣一個術語重構,它意味著通過改變程式碼使它變得更優雅和可復用;這就是設計模式耀眼的地方。當你處理現有程式碼時(無論是由其他人構建還是你自己過去構建的),了解設計模式可以幫助你以不同的方式看待事物,你將發現問題以及改進程式碼的方法。

有很多種設計模式,其中單例模式、工廠模式和觀察者模式三種最受歡迎,在這篇文章中我將會一一介紹它們。

如何遵循本指南

無論你是一位有經驗的程式設計工作者還是一名剛剛接觸的新手,我想讓這篇教學讓每個人都很容易理解。設計模式概念並不容易理解,減少開始旅程時的學習曲線始終是首要任務。因此,除了這篇帶有圖表和程式碼片段的文章外,我還建立了一個 GitHub 倉庫,你可以克隆倉庫並在你的電腦上執行這些程式碼來實現這三種設計模式。你也可以觀看我建立的 YouTube視訊

必要條件

如果你只是想了解一般的設計模式思想,則無需克隆範例專案或安裝任何工具。但是,如果要執行範例程式碼,你需要安裝以下工具:

  • Java 開發套件(JDK):我強烈建議使用 OpenJDK
  • Apache Maven:這個簡單的專案使用 Apache Maven 構建;好的是許多 IDE 自帶了Maven。
  • 互動式開發編輯器(IDE):我使用 社群版 IntelliJ,但是你也可以使用 Eclipse IDE 或者其他你喜歡的 Java IDE。
  • Git:如果你想克隆這個工程,你需要 Git 用戶端。

安裝好 Git 後執行下列命令克隆這個工程:

git clone https://github.com/bryantson/OpensourceDotComDemos.git

然後在你喜歡的 IDE 中,你可以將 TopDesignPatterns 倉庫中的程式碼作為 Apache Maven 專案匯入。

我使用的是 Java,但你也可以使用支援抽象原則的任何程式語言來實現設計模式。

單例模式:避免每次建立一個物件

單例模式singleton pattern是非常流行的設計模式,它的實現相對來說很簡單,因為你只需要一個類。然而,許多開發人員爭論單例設計模式的是否利大於弊,因為它缺乏明顯的好處並且容易被濫用。很少有開發人員直接實現單例;相反,像 Spring Framework 和 Google Guice 等程式設計框架內建了單例設計模式的特性。

但是了解單例模式仍然有巨大的用處。單例模式確保一個類僅建立一次且提供了一個對它的全域性存取點。

單例模式:確保僅建立一個範例且避免在同一個專案中建立多個範例。

下面這幅圖展示了典型的類物件建立過程。當用戶端請求建立一個物件時,建構函式會建立或者範例化一個物件並呼叫方法返回這個類給呼叫者。但是每次請求一個物件都會發生這樣的情況:建構函式被呼叫,一個新的物件被建立並且它返回了一個獨一無二的物件。我猜物件導向語言的建立者有每次都建立一個新物件的理由,但是單例過程的支援者說這是冗餘的且浪費資源。

Normal class instantiation

下面這幅圖使用單例模式建立物件。這裡,建構函式僅當物件首次通過呼叫預先設計好的 getInstance() 方法時才會被呼叫。這通常通過檢查該值是否為 null 來完成,並且這個物件被作為私有變數儲存在單例類的內部。下次 getInstance() 被呼叫時,這個類會返回第一次被建立的物件。而沒有新的物件產生;它只是返回舊的那一個。

Singleton pattern instantiation

下面這段程式碼展示了建立單例模式最簡單的方法:

package org.opensource.demo.singleton;public class OpensourceSingleton {    private static OpensourceSingleton uniqueInstance;    private OpensourceSingleton() {    }    public static OpensourceSingleton getInstance() {        if (uniqueInstance == null) {            uniqueInstance = new OpensourceSingleton();        }        return uniqueInstance;    }}

在呼叫方,這裡展示了如何呼叫單例類來獲取物件:

Opensource newObject = Opensource.getInstance();

這段程式碼很好的驗證了單例模式的思想:

  1. getInstance() 被呼叫時,它通過檢查 null 值來檢查物件是否已經被建立。
  2. 如果值為 null,它會建立一個新物件並把它儲存到私有域,返回這個物件給呼叫者。否則直接返回之前被建立的物件。

單例模式實現的主要問題是它忽略了並行進程。當多個進程使用執行緒同時存取資源時,這個問題就產生了。對於這種情況有對應的解決方案,它被稱為雙重檢查鎖,用於多執行緒安全,如下所示:

package org.opensource.demo.singleton;public class ImprovedOpensourceSingleton {    private volatile static ImprovedOpensourceSingleton uniqueInstance;    private ImprovedOpensourceSingleton() {}    public static ImprovedOpensourceSingleton getInstance() {        if (uniqueInstance == null) {            synchronized (ImprovedOpensourceSingleton.class) {                if (uniqueInstance == null) {                    uniqueInstance = new ImprovedOpensourceSingleton();                }            }        }        return uniqueInstance;    }}

再強調一下前面的觀點,確保只有在你認為這是一個安全的選擇時才直接實現你的單例模式。最好的方法是通過使用一個製作精良的程式設計框架來利用單例功能。

工廠模式:將物件建立委派給工廠類以隱藏建立邏輯

工廠模式factory pattern是另一種眾所周知的設計模式,但是有一小點複雜。實現工廠模式的方法有很多,而下列的程式碼範例為最簡單的實現方式。為了建立物件,工廠模式定義了一個介面,讓它的子類去決定範例化哪一個類。

工廠模式:將物件建立委派給工廠類,因此它能隱藏建立邏輯。

下列的圖片展示了最簡單的工廠模式是如何實現的。

Factory pattern

用戶端請求工廠類建立型別為 x 的某個物件,而不是用戶端直接呼叫物件建立。根據其型別,工廠模式決定要建立和返回的物件。

在下列程式碼範例中,OpensourceFactory 是工廠類實現,它從呼叫者那裡獲取型別並根據該輸入值決定要建立和返回的物件:

package org.opensource.demo.factory;public class OpensourceFactory {    public OpensourceJVMServers getServerByVendor(String name) {        if(name.equals("Apache")) {            return new Tomcat();        }        else if(name.equals("Eclipse")) {            return new Jetty();        }        else if (name.equals("RedHat")) {            return new WildFly();        }        else {            return null;        }    }}

OpenSourceJVMServer 是一個 100% 的抽象類(即介面類),它指示要實現的是什麼,而不是怎樣實現:

package org.opensource.demo.factory;public interface OpensourceJVMServers {    public void startServer();    public void stopServer();    public String getName();}

這是一個 OpensourceJVMServers 類的實現範例。當 RedHat 被作為型別傳遞給工廠類,WildFly 伺服器將被建立:

package org.opensource.demo.factory;public class WildFly implements OpensourceJVMServers {    public void startServer() {        System.out.println("Starting WildFly Server...");    }    public void stopServer() {        System.out.println("Shutting Down WildFly Server...");    }    public String getName() {        return "WildFly";    }}

觀察者模式:訂閱主題並獲取相關更新的通知

最後是觀察者模式observer pattern。像單例模式那樣,很少有專業的程式設計師直接實現觀察者模式。但是,許多訊息佇列和資料服務實現都藉用了觀察者模式的概念。觀察者模式在物件之間定義了一對多的依賴關係,當一個物件的狀態發生改變時,所有依賴它的物件都將被自動地通知和更新。

觀察者模式:如果有更新,那麼訂閱了該話題/主題的用戶端將被通知。

理解觀察者模式的最簡單方法是想象一個郵寄清單,你可以在其中訂閱任何主題,無論是開源、技術、名人、烹飪還是您感興趣的任何其他內容。每個主題維護者一個它的訂閱者列表,在觀察者模式中它們相當於觀察者。當某一個主題更新時,它所有的訂閱者(觀察者)都將被通知這次改變。並且訂閱者總是能取消某一個主題的訂閱。

如下圖所示,用戶端可以訂閱不同的主題並新增觀察者以獲得最新資訊的通知。因為觀察者不斷的監聽著這個主題,這個觀察者會通知用戶端任何發生的改變。

Observer pattern

讓我們來看看觀察者模式的程式碼範例,從主題/話題類開始:

package org.opensource.demo.observer;public interface Topic {    public void addObserver(Observer observer);    public void deleteObserver(Observer observer);    public void notifyObservers();}

這段程式碼描述了一個為不同的主題去實現已定義方法的介面。注意一個觀察者如何被新增、移除和通知的。

這是一個主題的實現範例:

package org.opensource.demo.observer;import java.util.List;import java.util.ArrayList;public class Conference implements Topic {    private List<Observer> listObservers;    private int totalAttendees;    private int totalSpeakers;    private String nameEvent;    public Conference() {        listObservers = new ArrayList<Observer>();    }    public void addObserver(Observer observer) {        listObservers.add(observer);    }    public void deleteObserver(Observer observer) {        int i = listObservers.indexOf(observer);        if (i >= 0) {            listObservers.remove(i);        }    }    public void notifyObservers() {        for (int i=0, nObservers = listObservers.size(); i < nObservers; ++ i) {            Observer observer = listObservers.get(i);            observer.update(totalAttendees,totalSpeakers,nameEvent);        }    }    public void setConferenceDetails(int totalAttendees, int totalSpeakers, String nameEvent) {        this.totalAttendees = totalAttendees;        this.totalSpeakers = totalSpeakers;        this.nameEvent = nameEvent;        notifyObservers();    }}

這段程式碼定義了一個特定主題的實現。當發生改變時,這個實現呼叫它自己的方法。注意這將獲取觀察者的數量,它以列表方式儲存,並且可以通知和維護觀察者。

這是一個觀察者類:

package org.opensource.demo.observer;public interface Observer {    public void update(int totalAttendees, int totalSpeakers, String nameEvent);}

這個類定義了一個介面,不同的觀察者可以實現該介面以執行特定的操作。

例如,實現了該介面的觀察者可以在會議上列印出與會者和發言人的數量:

package org.opensource.demo.observer;public class MonitorConferenceAttendees implements Observer {    private int totalAttendees;    private int totalSpeakers;    private String nameEvent;    private Topic topic;    public MonitorConferenceAttendees(Topic topic) {        this.topic = topic;        topic.addObserver(this);    }    public void update(int totalAttendees, int totalSpeakers, String nameEvent) {        this.totalAttendees = totalAttendees;        this.totalSpeakers = totalSpeakers;        this.nameEvent = nameEvent;        printConferenceInfo();    }    public void printConferenceInfo() {        System.out.println(this.nameEvent + " has " + totalSpeakers + " speakers and " + totalAttendees + " attendees");    }}

接下來

現在你已經閱讀了這篇對於設計模式的介紹引導,你還可以去尋求瞭解其他設計模式,例如外觀模式,模版模式和裝飾器模式。也有一些並行和分散式系統的設計模式如斷路器模式和錨定模式。

可是,我相信最好的磨礪你的技能的方式首先是通過在你的業餘專案或者練習中實現這些設計模式。你甚至可以開始考慮如何在實際專案中應用這些設計模式。接下來,我強烈建議你檢視 OOP 的 SOLID 原則。之後,你就準備好了解其他設計模式。