面對複雜業務架構,阿里架構師是如何做的?

語言: CN / TW / HK

大家好,我是Tom哥~

面對複雜的業務場景,千變萬化的客戶需求,如何以一變應萬變,以最小的開發成本快速落地實現,同時保證系統有著較低的複雜度,能夠保證系統後續de持續迭代能力,讓系統擁有較高的可擴充套件性。

​這些是一個合格的架構師必須修煉的基礎內功,但是如何修煉這門神功???​

不要著急,慢慢看下去。學了真本事,拿了阿里、頭條的offer,女神還會遠嗎!:heart:????????

接下來我們來系統性彙總下,軟體架構設計需要知曉的設計模式,主要是提煉精髓、核心設計思路、程式碼示例、以及應用場景等。

CRUD很多人都會,不懂設計模式也可以開發軟體,但是當開發及維護大型軟體系統過程中就痛苦不堪,懂了人自然聽得懂我在說什麼,不懂的人說了你也不會懂。

我將常用的軟體設計模式,做了彙總,目錄如下:

考慮到內容篇幅較大,為了便於大家閱讀,將軟體設計模式系列(共23個)拆分成四篇文章,每篇文章講解六個設計模式,採用不同的顏色區分,便於快速消化記憶

本文是首篇,主要講解​ ​單例模式​ ​​、​ ​建造者模式​ ​​、​ ​抽象工廠​ ​​、​ ​工廠方法​ ​​、​ ​原型模式​ ​​、​ ​介面卡模式​ ​,共6個設計模式。

1、單例模式

定義:

單例模式(Singleton)允許存在一個和僅存在一個給定類的例項。它提供一種機制讓任何實體都可以訪問該例項。

核心思路:

:one: 保證一個類只有一個例項。如果該物件已經被建立, 則返回已有的物件。為什麼要這樣設計呢?因為某些業務場景要控制共享資源 (例如資料庫或檔案) 的訪問許可權。

:two: 為該例項提供一個全域性訪問入口, 提供一個​ ​static​ ​訪問方法。

程式碼示例:

/**
 * @author 微信公眾號:微觀技術
 */
public class Singleton {

    private static Singleton instance = new Singleton();

    // 讓建構函式為 private,這樣該類就不會被例項化
    private Singleton() {}

    // 獲取唯一可用的物件
    public static Singleton getInstance() {
        return instance;
    }
}

在類中新增一個私有靜態成員變數用於儲存單例例項,宣告一個公有靜態構建方法用於獲取單例例項。

注意事項:

多個業務場景,多個執行緒訪問同一個類例項的全域性變數,頻發的寫操作,可能會引發執行緒安全問題。另外,為了防止其他物件使用單例類的 ​ ​new​ ​ 運算子,編碼時需要將預設建構函式設為私有。

如果想要採用​ ​延遲初始化物件​ ​,多執行緒併發初始化時,可能會有併發安全問題。假如:執行緒A,執行緒B都阻塞在了獲取鎖的步驟上,其中執行緒A獲得鎖---例項化了物件----釋放鎖;之後執行緒B---獲得鎖---例項化物件,此時違反了我們單例模式的初衷。

如何解決?

採用​ ​雙重判空檢查​ ​。首先保證了安全,且在多執行緒情況下能保持高效能,第一個if判斷避免了其他無用執行緒競爭鎖造成效能浪費,第二個if判斷能攔截除第一個獲得物件鎖執行緒以外的執行緒。

/**
 * @author 微信公眾號:微觀技術
 */
public class SingleonLock {

    private static SingleonLock doubleLock;

    private SingleonLock() {}

    // 雙重校驗鎖
    public static SingleonLock getInstance() {
        if (doubleLock == null) {
            synchronized (SingleonLock.class) {
                if (doubleLock == null) {
                    doubleLock = new SingleonLock();
                }
            }
        }
        return doubleLock;
    }
}

2、建造者模式

定義:

建造者模式,也稱 ​ ​Builder​ ​ 模式。

將複雜物件的構造與其表示分離,以便同一構造過程可以建立不同的表示。

簡單來說,建造者模式就是如何一步步構建一個包含多個組成部件的物件,相同的構建過程可以建立不同的產品

核心思路:

角色

類別

說明

Builder

介面或抽象類

抽象的建造者, 不是必須的

ConcreteBuilder

具體的建造者

可以有多個「因為每個建造風格可能不一樣」, 必須要有

Product

普通類

最終構建的物件, 必須要有

Director

指揮者

統一指揮建造者去建造目標, 不是必須的

程式碼示例:

/**
 * @author 微信公眾號:微觀技術
 */
public class Person {
    private String name;
    private int age;
    private String address;

    public static PersonBuilder builder() {
        return new PersonBuilder();
    }

    private Person(PersonBuilder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.address = builder.address;
    }

    // 建造者
    static class PersonBuilder {

        private String name;
        private int age;
        private String address;

        public PersonBuilder() {
        }

        public PersonBuilder name(String name) {
            this.name = name;
            return this;
        }

        public PersonBuilder age(int age) {
            this.age = age;
            return this;
        }

        public PersonBuilder address(String address) {
            this.address = address;
            return this;
        }

        public Person build() {
            return new Person(this);
        }
    }

}
  • ​Person​ ​​ 中建立一個靜態內部類 ​ ​PersonBuilder​ ​​,然後將 ​ ​Person​ ​​ 中的引數都複製到 ​ ​PersonBuilder​ ​類中。
  • ​Person​ ​​中建立一個private的建構函式,入參為 ​ ​PersonBuilder​ ​型別
  • ​PersonBuilder​ ​中建立一個public的建構函式
  • ​PersonBuilder​ ​​中建立設定函式,對​ ​Person​ ​​ 中那些可選引數進行賦值,返回值為​ ​PersonBuilder​ ​型別的例項
  • ​PersonBuilder​ ​​中建立一個build()方法,在其中構建​ ​Person​ ​​ 的例項並返回
/**
 * @author 微信公眾號:微觀技術
 */
public class PersonBuilderTest {

    public static void main(String[] args) {
        Person person = Person.builder()
                .name("Tom哥")
                .age(18)
                .address("杭州")
                .build();
        System.out.println(JSON.toJSONString(person));
    }

}

客戶端使用鏈式呼叫,一步一步的把物件構建出來。

適用場景:

​StringBuilder​

使用建造者模式能更方便地幫助我們按需進行物件的例項化,避免寫很多不同引數的建構函式,同時還能解決同一型別引數只能寫一個建構函式的弊端。

最後,實際專案中,為了簡化編碼,通常可以直接使用​ ​lombok​ ​​的 ​ ​@Builder​ ​​ 註解實現​ ​類自身的建造者模式​ ​。

3、抽象工廠模式

定義:

抽象工廠模式圍繞一個超級工廠建立其他工廠,又稱為其他工廠的工廠。是一種建立型設計模式,它能建立一系列相關的物件,而無需指定其具體類。

抽象工廠模式的關鍵點:如何找到正確的抽象。

對於軟體呼叫者來說,他們更關心軟體提供了什麼功能。至於內部如何實現的,他們並不關心。另外,考慮到安全問題,一般內部具體的實現細節通常會隱藏掉。

我們以電視、冰箱、洗衣機等家用電器生產為例,很多廠商像​ ​Haier​ ​​、​ ​Sony​ ​​、​ ​小米​ ​​、​ ​Hisense​ ​​等能生產上述電器,不過在外觀、效能、功率、智慧化、特色功能等方面會有差異。面對這樣的需求,我們如何藉助​ ​抽象工廠模式​ ​來實現編碼。

抽象工廠模式體現為定義一個抽象工廠類,多個不同的具體工廠繼承這個抽象工廠類後,再各自實現相同的抽象功能,從而實現程式碼上的多型性

程式碼示例:

/**
 * @author 微信公眾號:微觀技術
 */
public abstract class AbstractFactory {
    // 生產電視
    abstract Object createTV();
    // 生產洗衣機
    abstract Object createWasher();
    // 生產冰箱
    abstract Object createRefrigerator();

}

public class HaierFactory extends AbstractFactory {
    @Override
    Object createTV() {
        return null;
    }

    @Override
    Object createWasher() {
        return null;
    }

    @Override
    Object createRefrigerator() {
        return null;
    }
}


public class XiaomiFactory extends AbstractFactory {
    @Override
    Object createTV() {
        return null;
    }

    @Override
    Object createWasher() {
        return null;
    }

    @Override
    Object createRefrigerator() {
        return null;
    }
}

​AbstractFactory​ ​​是抽象工廠類,能夠建立電視、洗衣機、冰箱抽象產品;而​ ​HaierFactory​ ​​和​ ​XiaomiFactory​ ​​ 是具體的工廠,負責生產具體的產品。當我們要生產具體的產品時,只需要告訴​ ​AbstractFactory​ ​即可。

解決問題:

  • 對於不同產品系列有比較多共性特徵時,可以使用抽象工廠模式,有助於提升元件的複用性。
  • 當需要提升程式碼的擴充套件性並降低維護成本時,把物件的建立和使用過程分開,能有效地將程式碼統一到一個級別上。

適用場景:

  • 解決跨平臺相容性的問題。當一個應用程式需要支援Windows、Mac、Linux等多套作業系統。
  • 電商的商品、訂單、物流系統,需要根據區域政策、使用者的購買習慣,差異化處理
  • 不同的資料庫產品,JDBC 就是對於資料庫增刪改查建立的抽象工廠類,無論使用什麼型別的資料庫,只要具體的資料庫元件能夠支援 JDBC,就能對資料庫進行讀寫操作。

4、工廠方法模式

工廠方法模式與抽象工廠模式類似。工廠方法模式因為只圍繞著一類介面來進行物件的建立與使用,使用場景更加單一,專案中更常見些。

定義

定義一個建立物件的介面,讓其子類自己決定例項化哪一個類,工廠模式使其建立過程延遲到子類進行。

核心點:封裝物件建立的過程,提升建立物件方法的可複用性。

工廠方法模式包含三個關鍵角色:抽象產品、具體產品、工廠類。

定義一個抽象產品介面​ ​ITV​ ​​,​ ​HaierTV​ ​​和​ ​XiaomiTV​ ​​是具體產品類,​ ​TVFactory​ ​是工廠類,負責生產具體的物件例項。

程式碼示例:

/**
 * @author 微信公眾號:微觀技術
 */
public interface ITV {
    // 描述
    Object desc();
}

public class HaierTV implements ITV {
    @Override
    public Object desc() {
        return "海爾電視";
    }
}

public class XiaomiTV implements ITV {
    @Override
    public Object desc() {
        return "小米電視";
    }
}

public class TVFactory {
    public static ITV getTV(String name) {
        switch (name) {
            case "haier":
                return new HaierTV();
            case "xiaomi":
                return new XiaomiTV();
            default:
                return null;
        }
    }
}

public class Client {
    public static void main(String[] args) {
        ITV tv = TVFactory.getTV("xiaomi");
        Object result = tv.desc();
        System.out.println(result);
    }
}

工廠方法模式是圍繞著特定的抽象產品(介面)來封裝物件的建立過程,​ ​Client​ ​只需要通過工廠類來建立具體物件例項,然後就可以使用其功能。

工廠方法模式將物件的建立和使用過程分開,降低程式碼耦合性。

5、原型模式

原型模式是建立型模式的一種,其特點在於通過“複製”一個已經存在的例項來返回新的例項,而不是新建例項。被複制的例項就是我們所稱的“原型”,這個原型是可定製的。

定義:

使用原型例項指定建立物件的種類,然後通過拷貝這些原型來建立新的物件。

程式碼示例:

/**
 * @author 微信公眾號:微觀技術
 */
public interface Prototype extends Cloneable {
    public Prototype clone() throws CloneNotSupportedException;
}

public class APrototype implements Prototype {
    @Override
    public Prototype clone() throws CloneNotSupportedException {
        System.out.println("開始克隆《微觀技術》物件");
        return (APrototype) super.clone();
    }
}

public class Client {
    @SneakyThrows
    public static void main(String[] args) {
        Prototype a = new APrototype();
        Prototype b = a.clone();
        System.out.println("a的物件引用:" + a);
        System.out.println("b的物件引用:" + b);
    }
}

執行結果:

開始克隆《微觀技術》物件
a的物件引用:[email protected]
b的物件引用:[email protected]

打印出兩個物件的地址,發現不相同,在記憶體中為兩個物件。

Cloneable 介面本身是空方法,呼叫的 clone() 方法其實是 Object.clone() 方法

優點:

  • 效能優良。不用重新初始化物件,而是動態地獲取物件執行時的狀態。
  • 可以擺脫建構函式的約束。

特別注意:

​clone()​ ​​是​ ​淺複製​ ​,也就是基本型別資料,會給你重新複製一份新的。但是引用型別(物件中包含物件),他就不會重新複製份新的。引用型別如:bean例項引用、集合等一些引用型別。

如何解決?

你需要在執行完​ ​super.clone()​ ​​ 獲得淺複製物件後,再手動對其中的全域性變數重新構造物件並賦值。當然,經過這個過程,得到的物件我們稱之為​ ​深複製​ ​。

適用場景:

  • 反序列化,比如 fastjson的JSON.parseObject() ,將字串轉變為物件
  • 每次建立新物件資源損耗較大
  • 物件中的屬性非常多,通過get和set方法建立物件,複製黏貼非常痛苦

加餐:

Spring 框架中提供了一個工具類,​ ​BeanUtils.copyProperties​ ​​ 可以方便的完成物件屬性的拷貝,其實也是​ ​淺複製​ ​​,只能對​ ​基本型別資料​ ​​、​ ​物件引用​ ​拷貝。使用時特別要注意,如果全域性變數有物件型別,原型物件和克隆的物件會二次修改,要特殊處理,採用深複製,否則會引發安全問題。

6、介面卡模式

我們都知道美國的電壓是110V,而中國是220V,如果你去要美國旅行時,一定要記得帶電源介面卡,將不同國家使用的電源電流標準轉化為適合我們自己電器的標準,否則很容易燒壞電子裝置。

定義:

將類的介面轉換為客戶期望的另一個介面,介面卡可以讓不相容的兩個類一起協同工作。核心點在於轉換!

核心思路:

在原有的介面或類的外層封裝一個新的介面卡層,以實現擴充套件物件結構的效果,並且這種擴充套件可以無限擴充套件下去。

  • Adaptee:源介面,需要適配的介面
  • Target:目標介面,暴露出去的介面
  • Adapter:介面卡,將源介面適配成目標介面

適用場景:

​防腐層​

案例:

比如查物流資訊,由於物流公司的系統都是各自獨立,在程式語言和互動方式上有很大差異,需要針對不同的物流公司做單獨適配,同時結合不同公司的系統性能,配置不同的響應超時時間

介面卡模式號稱為“最好用打補丁模式”,就是因為只要是一個介面,都可以用它來進行適配。

寫在最後

設計模式很多人都學習過,但專案實戰時總是暈暈乎乎,原因在於沒有了解其核心是什麼,底層邏輯是什麼,《設計模式:可複用面向物件的基礎》有講過,

在設計中思考什麼應該變化,並封裝會發生變化的概念。

軟體架構的精髓:找到變化,封裝變化。

業務千變萬化,沒有固定的編碼答案,千萬不要硬套設計模式。無論選擇哪一種設計模式,儘量要能滿足​ ​SOLID​ ​原則,自我review是否滿足業務的持續擴充套件性。有句話說的好,“不論白貓黑貓,能抓老鼠就是好貓。”

關於我:前阿里架構師,出過專利,競賽拿過獎,CSDN部落格專家,負責過電商交易、社群生鮮、營銷、金融等業務,多年團隊管理經驗,愛思考,喜歡結交朋友