設計模式系列5--代理模式

西木柚子發表於2016-11-29

今天我們來學習下什麼是代理模式和如何運用它去解決一些常見的問題,代理模式大概分為如下幾大類:

  1. 遠端代理(Remote Proxy):為一個位於不同的地址空間的物件提供一個本地的代理物件,這個不同的地址空間可以是在同一臺主機中,也可是在另一臺主機中,遠端代理又稱為大使(Ambassador)。
  2. 虛擬代理(Virtual Proxy):如果需要建立一個資源消耗較大的物件,先建立一個消耗相對較小的物件來表示,真實物件只在需要時才會被真正建立。
  3. 保護代理(Protect Proxy):控制對一個物件的訪問,可以給不同的使用者提供不同級別的使用許可權.
  4. 緩衝代理(Cache Proxy):為某一個目標操作的結果提供臨時的儲存空間,以便多個客戶端可以共享這些結果。
  5. 智慧引用代理(Smart Reference Proxy):當一個物件被引用時,提供一些額外的操作,例如將物件被呼叫的次數記錄下來等。

今天我們學習下其中幾個常用的:虛擬代理、保護代理、智慧引用代理。


1、虛擬代理

1.1、延遲載入

虛擬代理主要是用來做延遲載入用的,以一個簡單的示例來闡述使用代理模式實現延遲載入的方法及其意義。假設某客戶端軟體有根據使用者請求去資料庫查詢資料的功能。在查詢資料前,需要獲得資料庫連線,軟體開啟時初始化系統的所有類,此時嘗試獲得資料庫連線。當系統有大量的類似操作存在時 (比如 XML 解析等),所有這些初始化操作的疊加會使得系統的啟動速度變得非常緩慢。為此,使用代理模式的代理類封裝對資料庫查詢中的初始化操作,當系統啟動時,初始化這個代理類,而非真實的資料庫查詢類,而代理類什麼都沒有做。因此,它的構造是相當迅速的。

在系統啟動時,將消耗資源最多的方法都使用代理模式分離,可以加快系統的啟動速度,減少使用者的等待時間。而在使用者真正做查詢操作時再由代理類單獨去載入真實的資料庫查詢類,完成使用者的請求。這個過程就是使用代理模式實現了延遲載入。

延遲載入的核心思想是:如果當前並沒有使用這個元件,則不需要真正地初始化它,使用一個代理物件替代它的原有的位置,只要在真正需要的時候才對它進行載入。使用代理模式的延遲載入是非常有意義的,首先,它可以在時間軸上分散系統壓力,尤其在系統啟動時,不必完成所有的初始化工作,從而加速啟動時間;其次,對很多真實主題而言,在軟體啟動直到被關閉的整個過程中,可能根本不會被呼叫,初始化這些資料無疑是一種資源浪費。例如使用代理類封裝資料庫查詢類後,系統的啟動過程這個例子。若系統不使用代理模式,則在啟動時就要初始化 DBQuery 物件,而使用代理模式後,啟動時只需要初始化一個輕量級的物件 DBQueryProxy。

在iOS開發一個典型的開發場景就是:一個tableview列表需要從網路下載很多圖片顯示,如果等到全部下載完畢再顯示,使用者體驗會很不好。一個替代方法就是首次載入時顯示一個佔點陣圖,當後臺執行緒下載完圖片,再用真實圖片去替代原來的佔點陣圖。這就是上面說的延遲載入。

1.2、需求分析

我們來看一個實際需求,假設我們有一個列表需要一次顯示100個人的資訊,但是列表顯示的只有使用者的名字和性別,當使用者點選某個具體的人的時候,才會顯示該人的完整的資訊。

常規做法是把這些資訊全部從資料庫查詢出來,然後臨時儲存到陣列進行顯示。如果每個人的資訊非常多,那麼就會導致記憶體消耗嚴重和比較長的查詢時間。

其實我們分析下就知道,使用者很少會檢視所有的個人全部資訊,使用者感興趣的可能就只有幾個人,那麼一次把使用者全部資訊都載入到記憶體會導致浪費,而且每個人的資訊比較多的情況,全部查詢這些資訊頁會導致查詢時間過長。

解決方案就是:我們可以在首次載入的時候只從資料庫查詢到name和sex資訊儲存起來給使用者展示,當使用者點選某個人的時候才會再次請求資料庫去獲取完整的資訊。

這個需求就可以通過代理模式來實現

1.3、代理模式定義

為其他物件提供一種代理來提供對這個物件的控制訪問

通過上面的定義可以發現代理模式其實就是建立一個代理物件,用這個代理物件去代替真實物件,客戶端操作這個代理物件進行的操作,最終會被代理物件轉發到真實物件。

但是真因為在客戶端和真實物件之間加上了代理物件,那麼此時我們就可以乾點別的事,比如控制許可權等。這就是代理的主要作用。

根據代理不同的用途,可以分為文字開頭的幾大類。再分析下上面的需求如何使用代理模式來實現:

首次載入我們只顯示一個代理物件,這個代理物件只載入name和sex,當使用者需要檢視該人的全部資訊的時候,我們才會把請求轉發到真實物件,去顯示所有個人資訊。

1.4、UML結構圖及說明

設計模式系列5--代理模式
image

1.5、程式碼實現

下面我們來看看使用代理模式的如何實現上述功能

建立代理和真實類的介面

subject.h檔案
=====================

@protocol subject <NSObject>

-(NSString *)getName;
-(NSInteger)getAge;
-(NSString *)getSex;
-(NSString *)getAddress;
-(NSString *)getCountry;

//首次載入獲取簡單資訊:name和sex
-(void)getSimpleInfo;
//當使用者點選了某個人,去資料庫獲取該人的全部資訊
-(void)getCompleteInfo;

@end複製程式碼

建立代理類


#import <Foundation/Foundation.h>
#import "subject.h"
#import "realSubject.h"

@interface proxy : NSObject<subject>
- (instancetype)initWithRealSubject:(realSubject *)subject;

@end


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

#import "proxy.h"

@interface proxy()
@property(strong,nonatomic)realSubject *realSubject;
@property(assign,nonatomic)BOOL isReload;
@end

@implementation proxy
- (instancetype)initWithRealSubject:(realSubject *)subject
{
    self = [super init];
    if (self) {
        self.realSubject = subject;
    }
    return self;
}


-(NSString *)getSex{
   NSLog(@"性別:%@",[self.realSubject getSex]);
    return [self.realSubject getSex];
}

-(NSString*)getName{
    NSLog(@"名字:%@", [self.realSubject getName]);
    return [self.realSubject getName];

}

-(NSInteger)getAge{
    if (!self.isReload)
    {
        [self reloadDB];
    }
    NSLog(@"年齡:%zd", [self.realSubject getAge]);
    return [self.realSubject getAge];
}


-(NSString *)getAddress{
    if (!self.isReload)
    {
        [self reloadDB];
    }

   NSLog(@"地址:%@",[self.realSubject getAddress]);
    return [self.realSubject getAddress];

}


-(NSString *)getCountry{
    if (!self.isReload)
    {
        [self reloadDB];
    }

    NSLog(@"國家:%@",[self.realSubject getCountry]);
    return  [self.realSubject getCountry];

}

-(void)reloadDB{
    self.isReload = YES;
    //假設下面的資料是從資料庫重新查詢到的資料
    self.realSubject.age = 19;
    self.realSubject.address = @"泰坦星球";
    self.realSubject.country =  @"賽亞王國";

}

-(void)getSimpleInfo{
    NSLog(@"查詢資料庫獲取簡單資料....");
    self.realSubject.name = @"張三";
    self.realSubject.sex = @"男";
    [self getName];
    [self getSex];
}


-(void)getCompleteInfo{
    NSLog(@"重新查詢資料庫獲取全部資料....");
    [self getName];
    [self getSex];
    [self getCountry];
    [self getAddress];
    [self getAge];
}

@end複製程式碼

建立真實類


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

@interface realSubject : NSObject<subject>
//真實環境有幾十條屬性,這裡為了方便只展示幾條屬性
@property(nonatomic,strong)NSString *name;
@property(nonatomic,assign)NSInteger age;
@property(nonatomic,strong)NSString *sex;
@property(nonatomic,strong)NSString *address;
@property(nonatomic,strong)NSString *country;


@end

================
#import "realSubject.h"

@implementation realSubject

-(NSString *)getSex{
    return self.sex;
}

-(NSString *)getName{
    return self.name;

}

-(NSInteger)getAge{
    return self.age;
}


-(NSString *)getAddress{
    return self.address;
}


-(NSString *)getCountry{
    return self.country;
}


@end複製程式碼

客戶端呼叫:

    proxy *pro = [[proxy alloc]initWithRealSubject:[realSubject new]];
    //先獲取簡單的資料,此時只有name和age欄位
    [pro getSimpleInfo];
    //獲取完整的資料,包括所有資訊
    [pro getCompleteInfo];複製程式碼

輸出:


2016-11-29 16:48:45.901 代理模式[11123:753185] 查詢資料庫獲取簡單資料....
2016-11-29 16:48:45.901 代理模式[11123:753185] 名字:張三
2016-11-29 16:48:45.901 代理模式[11123:753185] 性別:男
2016-11-29 16:48:45.901 代理模式[11123:753185] 重新查詢資料庫獲取全部資料....
2016-11-29 16:48:45.902 代理模式[11123:753185] 名字:張三
2016-11-29 16:48:45.902 代理模式[11123:753185] 性別:男
2016-11-29 16:48:45.902 代理模式[11123:753185] 國家:賽亞王國
2016-11-29 16:48:45.902 代理模式[11123:753185] 地址:泰坦星球
2016-11-29 16:48:45.902 代理模式[11123:753185] 年齡:19複製程式碼

這裡為了簡單,只展示了一個使用者的資訊,首次只獲取了name和sex,當需要獲取全部資訊的時候,會再次查詢資料庫去獲取完整資料。


2、保護代理和智慧引用

2.1、需求分析

保護代理主要是對原有物件加上一層許可權控制,根據訪問者許可權來決定訪問者可以進行哪些操作

假設我們現在有一個訂單系統,需要實現如下兩個需求:

1、每個使用者登入進來,可以新增和修改自己的訂單,但是對於他人的訂單隻能看不能改。

2、對於每次訪問我們都要記錄下次數。

需求1可以使用保護代理實現,需求2使用智慧引用代理實現

具體實現過程就是:在使用者需求對某個訂單進行修改的時候,先用代理來判斷該使用者是否是訂單所有人,是就可以修改然後把修改請求轉發到真實資料庫操作物件,不是就禁止修改,不轉發請求。每次代理查詢請求傳送到真是物件之前,先進行計數操作。

直接看程式碼實現

2.2、程式碼實現

建立代理和真實物件的介面

@protocol orderInterface <NSObject>

-(void)changeProductName:(NSString *)productName operator:(NSString *)opreator;
-(void)queryOrder;
@end複製程式碼

建立代理物件

#import <Foundation/Foundation.h>
#import "orderInterface.h"
#import "order.h"

@interface orderProxy : NSObject<orderInterface>
- (instancetype)initWithOrder:(order* )order;
@end

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

#import "orderProxy.h"


@interface orderProxy()
@property(strong,nonatomic)order * ord;
@end

static NSInteger orderQueryCount;

@implementation orderProxy
- (instancetype)initWithOrder:(order* )order
{
    self = [super init];
    if (self) {
        self.ord = order;
    }
    return self;
}

-(void)changeProductName:(NSString *)productName operator:(NSString *)opreator{
    if([opreator isEqualToString:self.ord.orderOperator]){
        NSLog(@"修改訂單成功");
        [self.ord changeProductName:productName operator:opreator];
    }else{
        NSLog(@"你無權操作該訂單");
    }
}



-(void)queryOrder{
    orderQueryCount ++;
    NSLog(@"訂單被查詢%zd次", orderQueryCount);
    [self.ord queryOrder];

}


@end複製程式碼

建立真實物件

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

@interface order : NSObject<orderInterface>
@property(strong,nonatomic)NSString *orderOperator;
@property(strong,nonatomic)NSString *productName;
@property(assign,nonatomic)NSInteger productAmount;
@property(strong,nonatomic)NSString *orderSignTime;


- (instancetype)initWithName:(NSString *)operator name:(NSString *)name amount:(NSInteger)amount time:(NSString *)time;
@end

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


#import "order.h"
@implementation order
- (instancetype)initWithName:(NSString *)operator name:(NSString *)name amount:(NSInteger)amount time:(NSString *)time
{
    self = [super init];
    if (self) {
        self.orderOperator = operator;
        self.productName = name;
        self.productAmount = amount;
        self.orderSignTime = time;
    }
    return self;
}


-(void)changeProductName:(NSString *)productName operator:(NSString *)opreator{
    self.productName = productName;
}

-(void)queryOrder{
    NSLog(@"\n訂單名字:%@\n 訂單操作員:%@\n 訂單數量:%zd\n 訂單簽訂時間:%@",self.productName,self.orderOperator,self.productAmount,self.orderSignTime);
}


@end複製程式碼

客戶端呼叫:

        order *ord = [[order alloc]initWithName:@"張三" name:@"電腦訂單" amount:1000 time:@"2016-10-11"];
        orderProxy *proxy = [[orderProxy alloc]initWithOrder:ord];
        [proxy changeProductName:@"辦公椅訂單" operator:@"李四"];
        [proxy queryOrder];

        [proxy changeProductName:@"辦公椅訂單" operator:@"張三"];
        [proxy queryOrder];

        [proxy changeProductName:@"檯燈訂單" operator:@"張三"];
        [proxy queryOrder];複製程式碼

輸出顯示:

2016-11-29 16:48:45.902 代理模式[11123:753185] 你無權操作該訂單
2016-11-29 16:48:45.902 代理模式[11123:753185] 訂單被查詢12016-11-29 16:48:45.902 代理模式[11123:753185] 
訂單名字:電腦訂單
 訂單操作員:張三
 訂單數量:1000
 訂單簽訂時間:2016-10-11
2016-11-29 16:48:45.902 代理模式[11123:753185] 修改訂單成功
2016-11-29 16:48:45.902 代理模式[11123:753185] 訂單被查詢22016-11-29 16:48:45.902 代理模式[11123:753185] 
訂單名字:辦公椅訂單
 訂單操作員:張三
 訂單數量:1000
 訂單簽訂時間:2016-10-11
2016-11-29 16:48:45.902 代理模式[11123:753185] 修改訂單成功
2016-11-29 16:48:45.902 代理模式[11123:753185] 訂單被查詢32016-11-29 16:48:45.903 代理模式[11123:753185] 
訂單名字:檯燈訂單
 訂單操作員:張三
 訂單數量:1000
 訂單簽訂時間:2016-10-11複製程式碼

3、iOS實現代理模式

其實iOS已經內建了代理的實現,我們只需要使用NSProxy類的兩個方法就可以實現代理模式的功能。

-(void)forwardInvocation:(NSInvocation *)anInvocation

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;複製程式碼

其實就是runtime的訊息轉發,因為oc裡面的方法訪問本質是訊息的轉發,所以可以使用上面兩個方法變相實現代理模式。

NSObject類也有這兩個方法用來做訊息轉發,那麼他們有什麼區別呢?

具體看這篇文章:

使用NSProxy和NSObject設計代理類的差異

一般要實現實現代理功能都是繼承下NSProxy,然後實現上面兩個方法就可以了。

我們來把需求2的功能改成使用NSProxy來實現。只需要把orderProxy類換成如下所示即可:


#import <Foundation/Foundation.h>
#import "orderInterface.h"
#import "order.h"

@interface orderProxy : NSProxy<orderInterface>
- (instancetype)initWithOrder:(order* )order;
@end


========

#import "orderProxy.h"


@interface orderProxy()
@property(strong,nonatomic)order * ord;
@end

static NSInteger orderQueryCount;

@implementation orderProxy
- (instancetype)initWithOrder:(order* )order
{
    self.ord = order;
    return self;
}


-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if([self.ord respondsToSelector:aSelector] ){
        return [self.ord methodSignatureForSelector:aSelector];
    }else{
        return [super methodSignatureForSelector:aSelector];
    }
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    NSString *selName = NSStringFromSelector(anInvocation.selector);

    if([self.ord respondsToSelector:anInvocation.selector] &&  [selName isEqualToString:@"changeProductName:operator:"]){
        NSString *opreator ;
        [anInvocation getArgument:&opreator atIndex:3];//self和_cmd分別是引數0和1,所有後面的引數index從2開始,這裡取的是operator引數,index=3
        if([opreator isEqualToString:self.ord.orderOperator]){
            NSLog(@"修改訂單成功");
            [anInvocation invokeWithTarget:self.ord];
        }else{
            NSLog(@"你無權操作該訂單");
        }
    }else if ([self.ord respondsToSelector:anInvocation.selector] &&  [selName isEqualToString:@"queryOrder"]){
        orderQueryCount ++;
        NSLog(@"訂單被查詢%zd次", orderQueryCount);
        [anInvocation invokeWithTarget:self.ord];
    }
    else{
        [super forwardInvocation:anInvocation];
    }

}

@end複製程式碼

關於NSProxy更高階的用法可以看看這篇文章:

利用NSProxy實現訊息轉發-模組化的網路介面層設計


4、對比其他模式

設計模式系列5--代理模式
image


5、Demo下載

代理模式Demo下載

相關文章