設計模式系列2--三大工廠模式

image

今天學習下最多見的工廠模式,工廠模式細分下來有三大類:javascript

1. 簡單工廠
 2. 工廠模式
 3. 抽象工廠模式複製代碼

他們的目標都是同樣的:封裝對象的建立。可是實現手段和使用場景倒是不相同。使用的時候三個模式也能夠互相替換使用,致使很容易混淆三者。java

下面咱們來具體看看三者的使用。sql


簡單工廠模式

準確的說簡單工廠不是一個模式,而是一種編程習慣。可是平時使用的很是多,咱們就把他歸到模式一類了。數據庫

一、定義

提供一個建立對象實例的功能,而無需關心具體實現。被建立的類型可使接口、抽象類、具體類。編程

二、UML結構圖及說明

image

obstractClass:能夠實現爲抽象類或者具體接口,看實際須要選擇,定義具體類須要實現的功能
concreteClass:實現抽象類所定義功能的具體類,可能會有多個
simpleFactory:簡單工廠,選擇合適的具體類來建立對象返回
client:經過simplefactory來獲取具體的對象設計模式

若是對UML圖不瞭解,能夠先看看這篇文章:UML類圖幾種關係的總結app

三、實際場景運用

3.一、需求

假設咱們要實現一個電腦組裝的功能,組裝電腦很重要的一個地方就是根據客戶指定的cpu類型來安裝。假設咱們有三種類型的cpu供客戶選擇:apple,intel,AMD。sqlserver

3.二、普通實現

在客戶端加入以下方法:學習

client.m文件
=====================

#import "simpleFactory.h"
#import "interCpu.h"
#import "appleCpu.h"
#import "AMDCpU.h"

@implementation client

-(Cpu *)selectCpuWithType:(NSString *)type{
    Cpu *cpu = nil;
    if ([type isEqualToString:@"intel"]) {
        cpu = [interCpu new];

    }else if([type isEqualToString:@"AMD"]){
        cpu = [AMDCpU new];

    }else{
        cpu = [appleCpu new];

    }
    return  cpu;
}

@end複製代碼

好比像使用inter類型的cpu,只須要以下代碼:ui

[self selectCpuWithType@"interCpu"];複製代碼

這裏我只是展示了核心代碼,忽略了其餘代碼。你須要建立一個CPU的父類,而後建立三個子類繼承它,分別是interCpu、AMDCpu、appleCpu。

上面的代碼能夠完成功能,根據客戶傳入的type類型來建立相應的cpu具體對象。

3.三、問題

雖然上述代碼能夠完成功能,可是有以下問題:

一、若是要加入其餘cpu類型,或者更改cpu類型,那麼必須修改客戶端代碼。違反了開閉原則(不瞭解的童鞋能夠去看設計模式開篇漫談

二、客戶端知道全部的具體cpu類,耦合度過高。客戶端必須知道全部具體的cpu類,那麼任何一個類的改動均可能會影響到客戶端。

3.四、解決問題

客戶端必須瞭解全部的具體cpu類才能建立對象,可是這會致使上述一系列問題。那麼解決辦法就是把這些對象的建立封裝起來,對客戶端不可見,那麼以後如何改動具體類都不會影響到客戶端。這能夠經過簡單工廠來實現。

下面咱們來看看使用簡單工廠重寫後的代碼

引入簡單工廠類:

simpleFactory.h文件

=======================

#import <Foundation/Foundation.h>
#import "Cpu.h"

@interface simpleFactory : NSObject
-(Cpu *)selectCpuWithType:(NSString *)type;

@end


~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

simpleFactory.m文件

=======================


#import "simpleFactory.h"
#import "interCpu.h"
#import "appleCpu.h"
#import "AMDCpU.h"

@implementation simpleFactory

-(Cpu *)selectCpuWithType:(NSString *)type{
    Cpu *cpu = nil;
    if ([type isEqualToString:@"intel"]) {
        cpu = [interCpu new];

    }else if([type isEqualToString:@"AMD"]){
        cpu = [AMDCpU new];

    }else{
        cpu = [appleCpu new];

    }
    return  cpu;
}

@end複製代碼

客戶端調用代碼:

#import <Foundation/Foundation.h>
#import "simpleFactory.h"
#import "Cpu.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        simpleFactory *factory = [simpleFactory new];
        Cpu *cpu = [factory selectCpuWithType:@"interCpu"];
        [cpu installCpu];
    }
    return 0;
}複製代碼

此時無論是增長仍是減小或者修改cpu類型,客戶端代碼都不用改動,下降了客戶端和具體cpu類的耦合,也遵循了開閉原則

四、反思

細心的一點的童鞋可能發現,你這不是逗我嗎,僅僅是把原本客戶端的代碼移到了簡單工廠類而已,有什麼改變嗎?

理解這個問題的關鍵在於理解簡單工廠所在的位置。

前面咱們把建立具體cpu對象的代碼放在客戶端,致使一系列問題。咱們的目標就是讓客戶端從建立具體對象中解耦出來,讓客戶端不知道對象建立的具體過程。而簡單工廠就是和具體對象封裝在一塊兒,算是一個封裝體內,因此簡單工廠知道具體的實現類是沒有關係的。如今客戶端只要知道簡單工廠和一個抽象類cpu,就能夠建立具體對象了,實現瞭解耦。

五、改進

雖然上面使用簡單工廠後,讓客戶端實現瞭解耦,可是若是實現類改變了,咱們仍是須要需改簡單工廠。有沒有什麼辦法作到即便實現類改變也不須要改變簡單工廠的代碼呢?

在java中可使用反射或者IoC/DI來實現,在iOS種咱們有更簡單的方法,一個方法足矣,具體見代碼

-(Cpu *)selectCpuWithType:(NSString *)type{
    Cpu *cpu = (Cpu *)[NSClassFromString(type)new];
    if ([cpu isKindOfClass:[Cpu class]] && cpu) {
        return  cpu;
    }else{
        return nil;
    }
}複製代碼

客戶端代碼不須要改動,是否是簡單了不少?

六、簡單工廠優缺點

  • 優勢

    1. 幫助封裝

      簡單工廠雖然簡單,可是幫咱們實現了封裝對象建立的過程,讓咱們能夠實現面向接口編程。

    2. 解耦

      客戶端不須要知道具體實現類,也不須要知道建立過程。只須要知道簡單工廠類就能夠建立具體對象,實現瞭解耦

  • 缺點

    1.增長客戶端複雜度

    若是是經過參數來選擇建立具體的對象,那麼客戶端就必須知道每一個參數的含義,也就暴露了內部實現

    2.不方便擴展

    若是實現類改變,那麼仍是須要修改簡單工廠,能夠經過文中的方法來避免這個問題。或者使用下節咱們講的工廠方法來解決

七、簡單工廠本質

簡單工廠的本質:選擇實現

簡單的工廠的本質在於選擇,而不是實現,實現是由具體類完成的,不要在簡單工廠完成。簡單工廠的目的是讓客戶端經過本身這個中介者來選擇具體的實現,從而讓客戶端和具體實現解耦,任何實現方面的變化都被簡單工廠屏蔽,客戶端不會知道。

簡單工廠的實現難點在於如何「選擇實現」,前面講到的是靜態傳遞參數。其實還能夠在運行過程當中從內存或者數據庫動態選擇參數來實現,具體代碼就不演示了,只是讀取參數的方式不一樣,其餘都同樣。

八、什麼時候使用簡單工廠

  1. 想徹底封裝隔離具體實現

讓外部只能經過抽象類或者接口來操做,上面的例子中,就是隻能操做抽象類cpu,而不能操做具體類。此時可使用簡單工廠,讓客戶端經過簡單工廠來選擇建立具體的類,不須要建立的具體過程。

  1. 想把建立對象的職責集中管理起來

一個簡單工廠能夠建立許多相關或者不相關的對象,因此能夠把對象的建立集中到簡單工廠來集中管理。

完整代碼見文末。


工廠模式

一、問題

讓咱們回到最原始的代碼:

client.m文件
=====================

#import "simpleFactory.h"
#import "interCpu1179.h"
#import "appleCpu1179.h"
#import "AMDCpU1179.h"

@implementation client

-(Cpu *)selectCpuWithType:(NSString *)type{
    Cpu *cpu = nil;
    if ([type isEqualToString:@"intel1179"]) {
        cpu = [interCpu1179 new];

    }else if([type isEqualToString:@"intel753"]){
        cpu = [interCpu753 new];

    }else if([type isEqualToString:@"AMD1179"]){
        cpu = [AMDCpU1179 new];

    }else if([type isEqualToString:@"AMD753"]){
        cpu = [AMDCpu753 new];

    }else if([type isEqualToString:@"apple1179"]){
        cpu = [appleCpu1179 new];

    }else if([type isEqualToString:@"apple753"]){
        cpu = [appleCpu753 new];

    }else{
        return nil;
    }return  cpu;
}

@end複製代碼

仔細看這段代碼,就會發現一個問題:依賴於具體類。由於必須在這裏完成對象建立,因此不得不依賴於具體類:interCpu、appleCpu、AMDCpu。

這會致使什麼問題呢?簡單來講就是違反了依賴倒置原則,讓高層組件client依賴於底層組件cpu。違反這個原則的後果就是一旦底層組件改動,那麼高層組件也就必須改動,違反了開閉原則。聯繫到上面的這個例子就是若是增長或者修改一個cpu子類,那麼就必須改動上面的代碼,即便使用了簡單工廠模式,仍是要修改簡單工廠的代碼。

咱們先來看看什麼是依賴致使原則:

定義:

要依賴抽象,不要依賴具體

展開來講就是:不能讓高層組件依賴低層組件,並且無論高層仍是低層組件,都應該依賴於抽象。

那麼如何才能避免違反這一原則呢?下面有三條建議能夠參考下:

  • 變量不能夠持有具體類的引用,好比new一個對象
  • 不要讓類派生自具體類,否則就會依賴於具體類,最好派生自抽象類
  • 不要覆蓋基類中已經實現的方法,若是覆蓋了基類方法,就說明該類不適合作基類,基類方法應該是被子類共享而不是覆蓋。

可是要徹底遵照上面三條,那就無法寫代碼了。因此合適變通才是,而工廠模式就是爲了遵循依賴倒置原則而生的。

下面就來看看使用工廠模式如何解決這個問題。


二、定義

定義了一個建立對象的接口,由子類決定實例化哪個類,讓類的實例化延遲到子類執行。

三、UML結構圖及說明

image

先記住工廠模式實現了依賴倒置原則,至於如何實現的,暫且按下不表,咱們先來看代碼

四、實際場景運用

仍是和簡單工廠的一樣的需求,可是咱們根據cpu的針腳個數增長了cpu的分類,好比intelCpu117九、intelCpu753。另外兩個類型的cpu也是如此,分爲1179和753兩個類型的cpu。可是此次咱們用工廠模式來實現。

定義一個工廠基類,定義一個工廠方法

#import <Foundation/Foundation.h>
#import "Cpu.h"

@interface factory : NSObject
-(Cpu*)createCpuWithType:(NSInteger)type;

@end


=============================
#import "factory.h"

@implementation factory
-(Cpu *)createCpuWithType:(NSInteger)type{
    @throw ([NSException exceptionWithName:@"繼承錯誤" reason:@"子類必須重寫該方法" userInfo:nil]);
    return nil;
}
@end複製代碼

下面是具體工廠,繼承自工廠基類,實現工廠方法來建立具體的cpu對象

#import <Foundation/Foundation.h>
#import "factory.h"

@interface intelFactory : factory

@end

===========================

#import "intelFactory.h"
#import "interCpu753.h"
#import "interCpu1179.h"
#import "Cpu.h"

@implementation intelFactory
-(Cpu *)createCpuWithType:(NSInteger)type{
    Cpu *cpu = nil;
    if (type == 753) {
        cpu = [interCpu753 new];
    }else{
        cpu = [interCpu1179 new];
    }
    return cpu;
}
@end複製代碼

上面演示的是intelCpu工廠,另外的AMD和apple的cpu具體工廠類相似,就不貼代碼了。

客戶端調用:

#import <Foundation/Foundation.h>
#import "factory.h"
#import "Cpu.h"
#import "intelFactory.h"
#import "appleFactory.h"
#import "AMDFactory.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        factory *factory = nil;
        factory = [intelFactory new];
        Cpu *cpu1 = [factory createCpuWithType:753];
        [cpu1 installCpu];
        Cpu *cpu2 = [factory createCpuWithType:1179];
        [cpu2 installCpu];

        factory = [AMDFactory new];
        Cpu *cpu3 = [factory createCpuWithType:753];
        [cpu3 installCpu];
        Cpu *cpu4 = [factory createCpuWithType:1179];
        [cpu4 installCpu];


    }
    return 0;
}複製代碼

若是此時又多了一個cpu類型,好比高通的cpu,那麼只須要新建一個高通cpu的工廠類,繼承自factory類,而後實現工廠方法,就能夠了。客戶端也能夠根據本身的須要選擇使用哪一個工廠,不用修改原有代碼。符合開閉原則:對修改關閉,對擴展開放。

五、如何遵循依賴倒置原則

咱們先來看看沒有使用工廠方法,各個類之間的依賴關係

image

能夠看到高層組件client依賴於具體的低層組件cpu類,違反了依賴倒置原則。通常咱們把功能的使用者歸到高層組件,把功能的提供者歸到低層組件。

再來看看使用工廠方法後各個類之間的依賴關係

image

能夠看到高層組件client依賴於抽象類cpu,低層組件也就是各類cpu具體類也依賴於抽象類factory,符合依賴倒置原則。其實說白了,就是要針對接口編程,而不是針對實現編程

那麼倒置在哪裏呢?

對比兩個圖,就會發現具體cpu類的箭頭從原來向下變成了向上,也就是說依賴關係發生了倒置。咱們來看看爲何會這樣。

第一個圖裏面,由於咱們直接在client裏面去初始化各個cpu類,倒置client就必須依賴這些具體類,依賴關係向下。

第二個圖裏面,每一個cpu具體類,都繼承自抽象cpu類,而且實現了抽象cpu的方法installCpu,此時具體cpu類就依賴於抽象cpu類,依賴關係向上。

如今明白爲何叫作依賴倒置了吧?這一切都是工廠方法的功勞。

有人要說,這個用簡單工廠也能夠實現的呀。是的沒錯,簡單工廠也能實現,其實若是直接在工廠方法的抽象cpu類裏面實現對象的建立,那麼此時工廠模式就是簡單工廠。可是工廠模式有一個簡單工廠模式沒有的功能:遵循開閉原則。若是此時要增長或者修改一個cpu具體類,那麼簡單工廠的代碼就必須修改,而工廠方法只須要擴展就好了,不用修改原有代碼。

六、工廠模式優缺點

  • 優勢

    1. 能夠在不知道具體實現的狀況下編程

      工廠模式可讓你在實現功能時候,不須要關心具體對象,只須要使用對象的抽象接口便可,上面例子中client使用的就是cpu抽 象類,而不是具體的cpu類。

    2. 更容易擴展新版本

      若是須要加入新的實現,只須要擴展一個新類,而後繼承抽象接口實現工廠方法便可。遵循了開閉原則。

  • 缺點

    具體產品和工廠方法耦合,由於在工廠方法中須要建立具體實例,因此它們會耦合

七、什麼時候使用工廠模式

經過工廠模式定義咱們知道,工廠模式主要是把對象的建立延遲到子類執行。如何實現的呢?

拿上面的例子來講,當咱們調用抽象類factory的方法createCpuWithType的時候,真正執行的是factory的子類,好比intelFactory。作到這點是面嚮對象語言的基本特徵之一:多態,它能夠實現父類的同一個方法在不一樣的子類中有不一樣的表現。

瞭解了工廠模式的本質,咱們就知道在上面狀況下可使用它了

  • 一個類不想知道它所須要建立的對象所屬的類,好比client不須要知道intelCpu1179這個具體類
  • 一個類但願由他的子類來指定它所建立的對象,好比factory但願IntelFactory建立具體cpu對象

抽象工廠

一、業務場景

假設咱們寫了一套系統,底層使用了兩套數據庫:sqlserver和access數據庫。可是針對業務邏輯的代碼不可能寫兩套,這樣很是麻煩,也不方便擴展新的數據庫。咱們須要提供一個統一的接口給業務層操做,切換數據庫也不須要修改業務層邏輯。

簡化下需求,假設咱們每一個數據庫都有user和department兩張表,業務邏輯代碼以下:

//業務邏輯
        [user insert:@"張三"];
        [user getUser];
        [deparment insert:@"財務"];
        [deparment getDepartment];複製代碼

下面咱們就來看看如何使用抽象工廠來實現這個需求

二、需求實現

2.一、建立抽象工廠接口

咱們先建立一個抽象接口,在iOS裏面咱們使用協議實現。

IFactory.h文件
========================
@class IUser;
@class IDepartment;

@protocol IFactory <NSObject>
@required
-(IUser*)createUser;
-(IDepartment *)createDepartment;

@end複製代碼

2.二、建立具體工廠

下面咱們來建立兩個具體的工廠,分別針對兩個數據庫,實現抽象工廠的方法,來建立具體的表對象

#import <Foundation/Foundation.h>
#import "IFactory.h"
#import "IUser.h"

@interface SqlServerFactory : NSObject<IFactory>

@end

======================

#import "SqlServerFactory.h"
#import "SqlServerUser.h"
#import "SqlServerDepartment.h"

@implementation SqlServerFactory

-(IUser *)createUser{
    return [SqlServerUser new];
}

-(IDepartment *)createDepartment{
    return [SqlServerDepartment new];
}
@end複製代碼

AccessFactory類建立方法相似。

2.三、建立產品

如今咱們須要建立具體工廠須要的產品,這裏是兩張表:user和department。可是這兩張表有分爲兩個體系,sqlserver的user和department表,access的user和department表。

咱們把user表抽象爲基類,下面分別實現sqlserver和access的子類user表。department表同理,再也不貼代碼了。

抽象產品類

#import <Foundation/Foundation.h>

@interface IUser : NSObject
-(void)insert:(NSString *)user;
-(void)getUser;
@end

=======================
#import "IUser.h"

@implementation IUser
-(void)insert:(NSString *)user{
    @throw ([NSException exceptionWithName:@"繼承錯誤" reason:@"子類沒有實現父類方法" userInfo:nil]);
}

-(void)getUser{
    @throw ([NSException exceptionWithName:@"繼承錯誤" reason:@"子類沒有實現父類方法" userInfo:nil]);
}
@end複製代碼

具體產品類

#import <Foundation/Foundation.h>
#import "IUser.h"

@interface SqlServerUser : IUser

@end

==================


#import "SqlServerUser.h"

@implementation SqlServerUser
-(void)insert:(NSString *)user{
    NSLog(@"向sqlserver數據庫插入用戶:%@", user);
}

-(void)getUser{
    NSLog(@"從sqlserver數據庫獲取到一條用戶數據");
}
@end複製代碼
#import <Foundation/Foundation.h>
#import "IUser.h"

@interface AccessUser : IUser

@end

=========================

#import "AccessUser.h"

@implementation AccessUser
-(void)insert:(NSString *)user{
    NSLog(@"向access數據庫插入用戶:%@", user);
}

-(void)getUser{
    NSLog(@"從access數據庫獲取到一條用戶數據");
}

@end複製代碼

2.四、客戶端調用

#import <Foundation/Foundation.h>
#import "IFactory.h"
#import "IUser.h"
#import "IDepartment.h"
#import "SqlServerFactory.h"
#import "AccessFactory.h"


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        id<IFactory> DBFactory = [AccessFactory new];
        IUser *user = [DBFactory createUser];
        IDepartment *deparment = [DBFactory createDepartment];

        //業務邏輯
        [user insert:@"張三"];
        [user getUser];
        [deparment insert:@"財務"];
        [deparment getDepartment];

    }
    return 0;
}複製代碼

輸出:

2016-11-22 17:38:30.667 抽象工廠模式[56330:792839] 向access數據庫插入用戶:張三
2016-11-22 17:38:30.668 抽象工廠模式[56330:792839] 從access數據庫獲取到一條用戶數據
2016-11-22 17:38:30.668 抽象工廠模式[56330:792839] 向access數據庫插入部門:財務
2016-11-22 17:38:30.668 抽象工廠模式[56330:792839] 從access數據庫獲取到一條部門數據複製代碼

此時若是須要切換到sqlserver數據庫,只須要更改以下代碼

id<IFactory> DBFactory = [AccessFactory new];
改成:
id<IFactory> DBFactory = [SqlServerFactory new];複製代碼

可是抽象工廠有個缺點:你想下,若是此時我想增長一張工資表,那麼就必須修改抽象工廠接口類IFactory和每一個具體工廠類SqlServerFactory、AccessFactory,違反了開閉原則。可是整體來瑕不掩瑜。

三、 實現原理分析

經過上面的例子,我想你們已經認識到抽象工廠的優雅之處,那麼它是如何完成的呢?

咱們來把上面的例子作成UML圖,這樣看的更加清晰。

image

能夠看到咱們建立了兩個具體工廠,分別是sqlserverFactory和AccessFactory。咱們的產品有兩個user和department,每一個產品也分爲兩個體系:sqlserver的access的。

若是選擇sqlserverFactory,那麼對應的兩個工廠方法就生成sqlserver的user和department表。選擇accessFactory也是如此。

因此咱們能夠很方便在兩個數據庫之間切換,而不影響業務邏輯,由於業務邏輯都是面向抽象編程。再看下業務邏輯的代碼

id<IFactory> DBFactory = [AccessFactory new];
        IUser *user = [DBFactory createUser];
        IDepartment *deparment = [DBFactory createDepartment];

        //業務邏輯
        [user insert:@"張三"];
        [user getUser];
        [deparment insert:@"財務"];
        [deparment getDepartment];複製代碼

能夠看到業務邏輯都是針對抽象類IUesr和IDepartment編程,因此他們的子類如何變化,不會影響到業務邏輯。

###四、 抽象工廠定義

提供一個建立一系列相關或者相互依賴的接口,而無需依賴具體類。

好好分析這句話,關鍵的地方就是:一系列相關或者相互依賴的接口。這決定了咱們使用抽象工廠的初衷,抽象工廠定義了一系列接口,這些接口必須是相互依賴或者相關的,而不是把一堆沒有什麼關聯的接口放到一塊兒。

回頭看看咱們上面的抽象工廠類IFactory定義的接口,是用來建立兩張表,這兩張表是屬於同一個數據庫的,他們之間是相互關聯和依賴的。

後面一句「無需依賴具體類」是怎麼作到的呢?

能夠看到抽象工廠類只是定義了接口,而真正去實現這些接口產生具體對象的是具體工廠。客戶端面向的也是抽象工廠類編程,因此無需依賴具體類。

咱們能夠把抽象工廠的定義的方法看作工廠方法,而後具體工廠去實現這些工廠方法,這不就是工廠模式嗎?
因此說抽象工廠包含了具體工廠。

五、思考

工廠模式和抽象工廠模式最大的區別在於,後者的一系列工廠方法是相互依賴或者相關的,而工廠模式雖然也能夠定義一些列工廠方法,可是他們之間是沒有關聯的。這是區分他們的重要依據。

其實若是抽象工廠裏面只定義一個工廠方法,也就是隻實現一個產品,那麼久退換爲工廠方法了。

記住:

工廠模式建立一種類型的產品,抽象工廠建立一些列相關的產品家族。

六、什麼時候使用抽象工廠

  • 客戶端只但願知道抽象接口,而不關心具體產品的實現的時候
  • 一個系統須要有多個產品系列中的一個來配置的時候。也就是說能夠動態切換產品系列,好比上面的切換兩個數據庫
  • 須要強調一系列產品的接口有關聯的時候,以便聯合使用它們。

三個模式對比

  • 抽象工廠模式和工廠模式

    工廠模式針對單獨產品的建立,而抽象工廠注重一個產品系列的建立。若是產品系列只有一個產品的 話,那麼抽象工廠就退換到工廠模式了。在抽象工廠中使用工廠方法來提供具體實現,這個時候他們聯 合使用。

  • 工廠模式和簡單工廠

    二者很是相似,都是用來作選擇實現的。不一樣的地方在於簡單工廠在自身就作了選擇實現。而工廠模式 則是把實現延遲到子類執行。若是把工廠方法的選擇實現直接在父類實現,那麼此時就退化爲簡單工廠 模式了。

  • 簡單工廠和抽象工廠
    簡單工廠用於作選擇實現,每一個產品的實現之間沒有依賴關係。而抽象工廠實現的一個產品系列,相互 之間有關聯。這是他們的區別


Demo下載地址

簡單工廠

工廠模式

抽象工廠模式

相關文章
相關標籤/搜索