設計模式(一) —— 構建型模式

面向對象的特點是可維護、可複用、可擴展、靈活性好,它最強大的地方在於:隨着業務變得越來越複雜,面向對象依然能夠使得程序結構良好,而面向過程卻會導致程序越來越臃腫。

讓面向對象保持結構良好的祕訣就是設計模式,今天我們就一起來探索設計模式的世界!

設計模式的六大原則

設計模式的世界豐富多彩,比如生產一個個“產品”的工廠模式,銜接兩個不相關接口的適配器模式,用不同的方式做同一件事的策略模式,構建步驟穩定、根據構建過程的不同配置構建出不同對象的建造者模式等等。

面向對象結合設計模式,才能真正體會到程序變得可維護、可複用、可擴展、靈活性好。設計模式對於程序員而言並不陌生,每個程序員在編程時都會或多或少的接觸到設計模式。無論是在大型程序的架構中,亦或是在源碼的學習中,設計模式都扮演着非常重要的角色。設計模式基於六大原則:

  • 開閉原則:一個軟件實體如類、模塊和函數應該對修改封閉,對擴展開放。
  • 單一職責原則:一個類只做一件事,一個類應該只有一個引起它修改的原因。
  • 里氏替換原則:子類應該可以完全替換父類。也就是說在使用繼承時,只擴展新功能,而不要破壞父類原有的功能。
  • 依賴倒置原則:細節應該依賴於抽象,抽象不應依賴於細節。把抽象層放在程序設計的高層,並保持穩定,程序的細節變化由低層的實現層來完成。
  • 迪米特法則:又名“最少知道原則”,一個類不應知道自己操作的類的細節,換言之,只和朋友談話,不和朋友的朋友談話。
  • 接口隔離原則:客戶端不應依賴它不需要的接口。如果一個接口在實現時,部分方法由於冗餘被客戶端空實現,則應該將接口拆分,讓實現類只需依賴自己需要的接口方法。

所有的設計模式都是爲了程序能更好的滿足這六大原則。設計模式一共有23種,今天我們先來學習構建型模式,一共五種,分別是:

  • 工廠方法模式
  • 抽象工廠模式
  • 單例模式
  • 建造型模式
  • 原型模式

一、工廠模式

在平時編程中,構建對象最常用的方式是 new 一個對象。乍一看這種做法沒什麼不好,而實際上這也屬於一種硬編碼。每 new 一個對象,相當於調用者多知道了一個類,增加了類與類之間的聯繫,不利於程序的松耦合。其實構建過程可以被封裝起來,工廠模式便是用於封裝對象的設計模式。

1.1.簡單工廠模式

舉個例子,直接 new 對象的方式相當於當我們需要一個蘋果時,我們需要知道蘋果的構造方法,需要一個梨子時,需要知道梨子的構造方法。更好的實現方式是有一個水果工廠,我們告訴工廠需要什麼種類的水果,水果工廠將我們需要的水果製造出來給我們就可以了。這樣我們就無需知道蘋果、梨子是怎麼種出來的,只用和水果工廠打交道即可。

水果工廠:

public class FruitFactory {
    public Fruit create(String type){
        switch (type){
            case "蘋果": return new Apple();
            case "梨子": return new Pear();
            default: throw new IllegalArgumentException("暫時沒有這種水果");
        }
    }
}

調用者:

public class User {
    private void eat(){
        FruitFactory fruitFactory = new FruitFactory();
        Fruit apple = fruitFactory.create("蘋果");
        Fruit pear = fruitFactory.create("梨子");
        apple.eat();
        pear.eat();
    }
}

事實上,將構建過程封裝的好處不僅可以降低耦合,如果某個產品構造方法相當複雜,使用工廠模式可以大大減少代碼重複。比如,如果生產一個蘋果需要蘋果種子、陽光、水分,將工廠修改如下:

public class FruitFactory {
    public Fruit create(String type) {
        switch (type) {
            case "蘋果":
                AppleSeed appleSeed = new AppleSeed();
                Sunlight sunlight = new Sunlight();
                Water water = new Water();
                return new Apple(appleSeed, sunlight, water);
            case "梨子":
                return new Pear();
            default:
                throw new IllegalArgumentException("暫時沒有這種水果");
        }
    }
}

調用者的代碼則完全不需要變化,而且調用者不需要在每次需要蘋果時,自己去構建蘋果種子、陽光、水分以獲得蘋果。蘋果的生產過程再複雜,也只是工廠的事。這就是封裝的好處,假如某天科學家發明了讓蘋果更香甜的肥料,要加入蘋果的生產過程中的話,也只需要在工廠中修改,調用者完全不用關心。

不知不覺中,我們就寫出了簡單工廠模式的代碼。工廠模式一共有三種:

  • 簡單工廠模式
  • 工廠方法模式
  • 抽象工廠模式

注:在 GoF 所著的《設計模式》一書中,簡單工廠模式被劃分爲工廠方法模式的一種特例,沒有單獨被列出來。

總而言之,簡單工廠模式就是讓一個工廠類承擔構建所有對象的職責。調用者需要什麼產品,讓工廠生產出來即可。它的弊端也顯而易見:

  • 一是如果需要生產的產品過多,此模式會導致工廠類過於龐大,承擔過多的職責,變成超級類。當蘋果生產過程需要修改時,要來修改此工廠。梨子生產過程需要修改時,也要來修改此工廠。也就是說這個類不止一個引起修改的原因。違背了單一職責原則。
  • 二是當要生產新的產品時,必須在工廠類中添加新的分支。而開閉原則告訴我們:類應該對修改封閉。我們希望在添加新功能時,只需增加新的類,而不是修改既有的類,所以這就違背了開閉原則。

1.2.工廠方法模式

爲了解決簡單工廠模式的這兩個弊端,工廠方法模式應運而生,它規定每個產品都有一個專屬工廠。比如蘋果有專屬的蘋果工廠,梨子有專屬的梨子工廠,代碼如下:

蘋果工廠:

public class AppleFactory {
    public Fruit create(){
        return new Apple();
    }
}

梨子工廠:

public class PearFactory {
    public Fruit create(){
        return new Pear();
    }
}

調用者:

public class User {
    private void eat(){
        AppleFactory appleFactory = new AppleFactory();
        Fruit apple = appleFactory.create();
        PearFactory pearFactory = new PearFactory();
        Fruit pear = pearFactory.create();
        apple.eat();
        pear.eat();
    }
}

有讀者可能會開噴了,這樣和直接 new 出蘋果和梨子有什麼區別?上文說工廠是爲了減少類與類之間的耦合,讓調用者儘可能少的和其他類打交道。用簡單工廠模式,我們只需要知道 FruitFactory,無需知道 Apple 、Pear 類,很容易看出耦合度降低了。但用工廠方法模式,調用者雖然不需要和 Apple 、Pear 類打交道了,但卻需要和 AppleFactory、PearFactory 類打交道。有幾種水果就需要知道幾個工廠類,耦合度完全沒有下降啊,甚至還增加了代碼量!

這位讀者請先放下手中的大刀,仔細想一想,工廠模式的第二個優點在工廠方法模式中還是存在的。當構建過程相當複雜時,工廠將構建過程封裝起來,調用者可以很方便的直接使用,同樣以蘋果生產爲例:

public class AppleFactory {
    public Fruit create(){
        AppleSeed appleSeed = new AppleSeed();
        Sunlight sunlight = new Sunlight();
        Water water = new Water();
        return new Apple(appleSeed, sunlight, water);
    }
}

調用者無需知道蘋果的生產細節,當生產過程需要修改時也無需更改調用端。同時,工廠方法模式解決了簡單工廠模式的兩個弊端。

  • 當生產的產品種類越來越多時,工廠類不會變成超級類。工廠類會越來越多,保持靈活。不會越來越大、變得臃腫。如果蘋果的生產過程需要修改時,只需修改蘋果工廠。梨子的生產過程需要修改時,只需修改梨子工廠。符合單一職責原則。
  • 當需要生產新的產品時,無需更改既有的工廠,只需要添加新的工廠即可。保持了面向對象的可擴展性,符合開閉原則。

1.3.抽象工廠模式

工廠方法模式可以進一步優化,提取出工廠接口:

public interface IFactory {
    Fruit create();
}

然後蘋果工廠和梨子工廠都實現此接口:

public class AppleFactory implements IFactory {
    @Override
    public Fruit create(){
        return new Apple();
    }
}
public class PearFactory implements IFactory {
    @Override
    public Fruit create(){
        return new Pear();
    }
}

此時,調用者可以將 AppleFactory 和 PearFactory 統一作爲 IFactory 對象使用,調用者代碼如下:

public class User {
    private void eat(){
        IFactory appleFactory = new AppleFactory();
        Fruit apple = appleFactory.create();
        IFactory pearFactory = new PearFactory();
        Fruit pear = pearFactory.create();
        apple.eat();
        pear.eat();
    }
}

可以看到,我們在創建時指定了具體的工廠類後,在使用時就無需再關心是哪個工廠類,只需要將此工廠當作抽象的 IFactory 接口使用即可。這種經過抽象的工廠方法模式被稱作抽象工廠模式。

由於客戶端只和 IFactory 打交道了,調用的是接口中的方法,使用時根本不需要知道是在哪個具體工廠中實現的這些方法,這就使得替換工廠變得非常容易。

例如:

public class User {
    private void eat(){
        IFactory factory = new AppleFactory();
        Fruit fruit = factory.create();
        fruit.eat();
    }
}

如果需要替換爲喫梨子,只需要更改一行代碼即可:

public class User {
    private void eat(){
        IFactory factory = new PearFactory();
        Fruit fruit = factory.create();
        fruit.eat();
    }
}

IFactory 中只有一個抽象方法時,或許還看不出抽象工廠模式的威力。實際上抽象工廠模式主要用於替換一系列方法。例如將程序中的 SQL Server 數據庫整個替換爲 Access 數據庫,使用抽象方法模式的話,只需在 IFactory 接口中定義好增刪改查四個方法,讓 SQLFactory 和 AccessFactory 實現此接口,調用時直接使用 IFactory 中的抽象方法即可,調用者無需知道使用的什麼數據庫,我們就可以非常方便的整個替換程序的數據庫,並且讓客戶端毫不知情。

抽象工廠模式很好的發揮了開閉原則、依賴倒置原則,但缺點是抽象工廠模式太重了,如果 IFactory 接口需要新增功能,則會影響到所有的具體工廠類。使用抽象工廠模式,替換具體工廠時只需更改一行代碼,但要新增抽象方法則需要修改所有的具體工廠類。所以抽象工廠模式適用於增加同類工廠這樣的橫向擴展需求,不適合新增功能這樣的縱向擴展。

二、單例模式

單例模式非常常見,某個對象全局只需要一個實例時,就可以使用單例模式。它的優點也顯而易見:

  • 它能夠避免對象重複創建,節約空間並提升效率
  • 避免由於操作不同實例導致的邏輯錯誤

單例模式有兩種實現方式:餓漢式和懶漢式。

2.1.餓漢式

  • 餓漢式:變量在聲明時便初始化。
public class Singleton {
  
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

可以看到,我們將構造方法定義爲 private,這就保證了其他類無法實例化此類,必須通過 getInstance 方法才能獲取到唯一的 instance 實例,非常直觀。但餓漢式有一個弊端,那就是即使這個單例不需要使用,它也會在類加載之後立即創建出來,佔用一塊內存,並增加類初始化時間。就好比一個電工在修理燈泡時,先把所有工具拿出來,不管是不是所有的工具都用得上。就像一個飢不擇食的餓漢,所以稱之爲餓漢式。

2.2.懶漢式

  • 懶漢式:先聲明一個空變量,需要用時才初始化。例如:
public class Singleton {
  
    private static Singleton instance = null;
  
    private Singleton() {
    }
    
    public static Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

我們先聲明瞭一個初始值爲 null 的 instance 變量,當需要使用時判斷此變量是否已被初始化,沒有初始化的話才 new 一個實例出來。就好比電工在修理燈泡時,開始比較偷懶,什麼工具都不拿,當發現需要使用螺絲刀時,才把螺絲刀拿出來。當需要用鉗子時,再把鉗子拿出來。就像一個不到萬不得已不會行動的懶漢,所以稱之爲懶漢式。

懶漢式解決了餓漢式的弊端,好處是按需加載,避免了內存浪費,減少了類初始化時間。

上述代碼的懶漢式單例乍一看沒什麼問題,但其實它不是線程安全的。如果有多個線程同一時間調用 getInstance 方法,instance 變量可能會被實例化多次。爲了保證線程安全,我們需要給判空過程加上鎖:

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

這樣就能保證多個線程調用 getInstance 時,一次最多隻有一個線程能夠執行判空並 new 出實例的操作,所以 instance 只會實例化一次。但這樣的寫法仍然有問題,當多個線程調用 getInstance 時,每次都需要執行 synchronized 同步化方法,這樣會嚴重影響程序的執行效率。所以更好的做法是在同步化之前,再加上一層檢查:

public class Singleton {
    
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

這樣增加一種檢查方式後,如果 instance 已經被實例化,則不會執行同步化操作,大大提升了程序效率。上面這種寫法也就是我們平時較常用的雙檢鎖方式實現的線程安全的單例模式。

有讀者可能會有疑問,我們在外面檢查了 instance == null, 那麼鎖裏面的空檢查是否可以去掉呢?

答案是不可以。如果裏面不做空檢查,可能會有兩個線程同時通過了外面的空檢查,然後在一個線程 new 出實例後,第二個線程進入鎖中又 new 出一個實例,導致創建多個實例。

除了雙檢鎖方式外,還有一種比較常見的靜態內部類方式保證懶漢式單例的線程安全:

public class Singleton {
    
    private static class SingletonHolder {
        public static Singleton instance = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

雖然我們經常使用這種靜態內部類的懶加載方式,但其中的原理不一定每個人都清楚。接下來我們便來分析其原理,搞清楚兩個問題:

  • 靜態內部類方式是怎麼實現懶加載的
  • 靜態內部類方式是怎麼保證線程安全的

Java 類的加載過程包括:加載、驗證、準備、解析、初始化。初始化階段即執行類的 clinit 方法(clinit = class + initialize),包括爲類的靜態變量賦初始值和執行靜態代碼塊中的內容。但不會立即加載內部類,內部類會在使用時才加載。所以當此 Singleton 類加載時,SingletonHolder 並不會被立即加載,所以不會像餓漢式那樣佔用內存。

另外,Java 虛擬機規定,當訪問一個類的靜態字段時,如果該類尚未初始化,則立即初始化此類。當調用Singleton 的 getInstance 方法時,由於其使用了 SingletonHolder 的靜態變量 instance,所以這時纔會去初始化 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 對象。這就實現了懶加載。

第二個問題的答案是 Java 虛擬機的設計是非常穩定的,早已經考慮到了多線程併發執行的情況。虛擬機在加載類的 clinit 方法時,會保證 clinit 在多線程中被正確的加鎖、同步。即使有多個線程同時去初始化一個類,一次也只有一個線程可以執行 clinit 方法,其他線程都需要阻塞等待,從而保證了線程安全。

懶加載方式在平時非常常見,比如打開我們常用的美團、餓了麼、支付寶 app,應用首頁會立刻刷新出來,但其他標籤頁在我們點擊到時纔會刷新。這樣就減少了流量消耗,並縮短了程序啓動時間。再比如遊戲中的某些模塊,當我們點擊到時纔會去下載資源,而不是事先將所有資源都先下載下來,這也屬於懶加載方式,避免了內存浪費。

但懶漢式的缺點就是將程序加載時間從啓動時延後到了運行時,雖然啓動時間縮短了,但我們瀏覽頁面時就會看到數據的 loading 過程。如果用餓漢式將頁面提前加載好,我們瀏覽時就會特別的順暢,也不失爲一個好的用戶體驗。比如我們常用的 QQ、微信 app,作爲即時通訊的工具軟件,它們會在啓動時立即刷新所有的數據,保證用戶看到最新最全的內容。著名的軟件大師 Martin 在《代碼整潔之道》一書中也說到:不提倡使用懶加載方式,因爲程序應該將構建與使用分離,達到解耦。餓漢式在聲明時直接初始化變量的方式也更直觀易懂。所以在使用餓漢式還是懶漢式時,需要權衡利弊。

一般的建議是:對於構建不復雜,加載完成後會立即使用的單例對象,推薦使用餓漢式。對於構建過程耗時較長,並不是所有使用此類都會用到的單例對象,推薦使用懶漢式。

三、建造型模式

建造型模式用於創建過程穩定,但配置多變的對象。在《設計模式》一書中的定義是:將一個複雜的構建與其表示相分離,使得同樣的構建過程可以創建不同的表示。

經典的“建造者-指揮者”模式現在已經不太常用了,現在建造者模式主要用來通過鏈式調用生成不同的配置。比如我們要製作一杯珍珠奶茶。它的製作過程是穩定的,除了必須要知道奶茶的種類和規格外,是否加珍珠和是否加冰是可選的。使用建造者模式表示如下:

public class MilkTea {
    private final String type;
    private final String size;
    private final boolean pearl;
    private final boolean ice;

	private MilkTea() {}
	
    private MilkTea(Builder builder) {
        this.type = builder.type;
        this.size = builder.size;
        this.pearl = builder.pearl;
        this.ice = builder.ice;
    }

    public String getType() {
        return type;
    }

    public String getSize() {
        return size;
    }

    public boolean isPearl() {
        return pearl;
    }
    public boolean isIce() {
        return ice;
    }

    public static class Builder {

        private final String type;
        private String size = "中杯";
        private boolean pearl = true;
        private boolean ice = false;

        public Builder(String type) {
            this.type = type;
        }

        public Builder size(String size) {
            this.size = size;
            return this;
        }

        public Builder pearl(boolean pearl) {
            this.pearl = pearl;
            return this;
        }

        public Builder ice(boolean cold) {
            this.ice = cold;
            return this;
        }

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

可以看到,我們將 MilkTea 的構造方法設置爲私有的,所以外部不能通過 new 構建出 MilkTea 實例,只能通過 Builder 構建。對於必須配置的屬性,通過 Builder 的構造方法傳入,可選的屬性通過 Builder 的鏈式調用方法傳入,如果不配置,將使用默認配置,也就是中杯、加珍珠、不加冰。根據不同的配置可以製作出不同的奶茶:

public class User {
    private void buyMilkTea() {
        MilkTea milkTea = new MilkTea.Builder("原味").build();
        show(milkTea);

        MilkTea chocolate =new MilkTea.Builder("巧克力味")
                .ice(false)
                .build();
        show(chocolate);
        
        MilkTea strawberry = new MilkTea.Builder("草莓味")
                .size("大杯")
                .pearl(false)
                .ice(true)
                .build();
        show(strawberry);
    }

    private void show(MilkTea milkTea) {
        String pearl;
        if (milkTea.isPearl())
            pearl = "加珍珠";
        else
            pearl = "不加珍珠";
        String ice;
        if (milkTea.isIce()) {
            ice = "加冰";
        } else {
            ice = "不加冰";
        }
        System.out.println("一份" + milkTea.getSize() + "、"
                + pearl + "、"
                + ice + "的"
                + milkTea.getType() + "奶茶");
    }
}

運行程序,輸出如下:

一份中杯、加珍珠、不加冰的原味奶茶
一份中杯、加珍珠、不加冰的巧克力味奶茶
一份大杯、不加珍珠、加冰的草莓味奶茶

使用建造者模式的好處是不用擔心忘了指定某個配置,保證了構建過程是穩定的。在 OkHttp、Retrofit 等著名框架的源碼中都使用到了建造者模式。

四、原型模式

原型模式:用原型實例指定創建對象的種類,並且通過拷貝這些原型創建新的對象。

定義看起來有點繞口,實際上在 Java 中,Object 的 clone() 方法就屬於原型模式,不妨簡單的理解爲:原型模式就是用來克隆對象的。

舉個例子,比如有一天,周杰倫到奶茶店點了一份不加冰的原味奶茶,你說我是周杰倫的忠實粉,我也要一份跟周杰倫一樣的。用程序表示如下:

奶茶類:

public class MilkTea {
    public String type;
    public boolean ice;
}

下單:

private void order(){
    MilkTea milkTeaOfJay = new MilkTea();
    milkTeaOfJay.type = "原味";
    milkTeaOfJay.ice = false;
    
    MilkTea yourMilkTea = milkTeaOfJay;
}

好像沒什麼問題,將周杰倫的奶茶直接賦值到你的奶茶上就行了,看起來我們並不需要 clone 方法。但是這樣真的是複製了一份奶茶嗎?

當然不是,Java 的賦值只是引用傳遞,而不是值傳遞。這樣賦值之後,yourMilkTea 仍然指向的周杰倫的奶茶,並不會多一份一樣的奶茶。

那麼我們要怎麼做才能點一份一樣的奶茶呢?將程序修改如下就可以了:

private void order(){
    MilkTea milkTeaOfJay = new MilkTea();
    milkTeaOfJay.type = "原味";
    milkTeaOfJay.ice = false;
    
    MilkTea yourMilkTea = new MilkTea();
    yourMilkTea.type = "原味";
    yourMilkTea.ice = false;
}

只有這樣,yourMilkTea 纔是 new 出來的一份全新的奶茶。我們設想一下,如果有一千個粉絲都需要點和周杰倫一樣的奶茶的話,按照現在的寫法就需要 new 一千次,併爲每一個新的對象賦值一千次,造成大量的重複。

更糟糕的是,如果周杰倫臨時決定加個冰,那麼粉絲們的奶茶配置也要跟着修改:

private void order(){
    MilkTea milkTeaOfJay = new MilkTea();
    milkTeaOfJay.type = "原味";
    milkTeaOfJay.ice = true;
    
    MilkTea yourMilkTea = new MilkTea();
    yourMilkTea.type = "原味";
    yourMilkTea.ice = true;
    
    // 將一千個粉絲的 ice 都修改爲 true
    ...
}

大批量的修改無疑是非常醜陋的做法,這就是我們需要 clone 方法的理由!

運用原型模式,在 MilkTea 中新增 clone 方法:

public class MilkTea{
    public String type;
    public boolean ice;

    public MilkTea clone(){
        MilkTea milkTea = new MilkTea();
        milkTea.type = this.type;
        milkTea.ice = this.ice;
        return milkTea;
    }
}

下單:

private void order(){
    MilkTea milkTeaOfJay = new MilkTea();
    milkTeaOfJay.type = "原味";
    milkTeaOfJay.ice = false;
    
    MilkTea yourMilkTea = milkTeaOfJay.clone();
    
    // 一千位粉絲都調用 milkTeaOfJay 的 clone 方法即可
    ...
}

這就是原型模式,Java 中有一個語法糖,讓我們並不需要手寫 clone 方法。這個語法糖就是 Cloneable 接口,我們只要讓需要拷貝的類實現此接口即可。

public class MilkTea implements Cloneable{
    public String type;
    public boolean ice;

    @NonNull
    @Override
    protected MilkTea clone() throws CloneNotSupportedException {
        return (MilkTea) super.clone();
    }
}

值得注意的是,Java 自帶的 clone 方法是淺拷貝的。也就是說調用此對象的 clone 方法,只有基本類型的參數會被拷貝一份,非基本類型的對象不會被拷貝一份,而是繼續使用傳遞引用的方式。如果需要實現深拷貝,必須要自己手動修改 clone 方法纔行。

總結

設計模式在面試中的考點通常是介紹其原理並說出優缺點。或者對比幾個比較相似的模式的異同點。在筆試中可能會出現畫出某個設計模式的 UML 圖這樣的題。雖說面試中佔的比重不大,但並不代表它不重要。恰恰相反,設計模式於程序員而言相當重要,它是我們寫出優秀程序的保障。設計模式與程序員的架構能力與閱讀源碼的能力息息相關,非常值得我們深入學習。

今天我們學習了設計模式的 5 種構建型模式,除此之外還有 11 種行爲型模式和 7 種結構型模式,我們將在以後的文章中繼續學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章