Objective-C設計模式解析-訪問者

看圖識模式

好比說有一個農場(這是一個結構體),裏面包括了木頭、牛羊、空閒的土地(結構體裏的元素)算法

需求

  • 需求一: 我要在這裏生活,因此要建房子、生火作飯
  • 需求二: 我要在這裏開工廠,因此要建廠房、生成火腿腸

這些需求均可以經過農場裏的材料進行不一樣的加工來完成。segmentfault

分析

咱們如何設計這個類? 想一下~設計模式

其實很簡單,給這個類添加2個方法。一個方法建房子上火作飯,另外一個方法建廠房生產火腿。
若是這時候有軍隊提出需求,須要建堡壘、生產肉罐頭。咱們能夠再加一個新方法來知足它。可是這樣有一些問題:
首先,它不符合開放封閉原則,且這個類過於複雜,不利於維護。隨着功能的增長,這個類會愈來愈臃腫。數組

猜測

經過上面的例子,咱們知道這個農場是穩定的(裏面就是木材、牛羊和土地),可是你們的需求不同,致使對這個農場進行的操做也不同。
那咱們可不能夠把農場和對農場的操做分離出來。不一樣人的來訪問這個農場就會進行不一樣的操做。數據結構

clipboard.png

農民來訪問它,建房子生火作飯
商人來訪問它,建廠房生產火腿
軍人訪問它,建堡壘生產肉罐頭架構

好處: 操做集合從數據結構中分離出來了,能夠相對獨立自由的演化。框架

模式定義

表示一個做用於某對象結構中的各元素的操做。它讓咱們能夠在不改變各元素的類的前提下定義做用於這些元素的新操做。

先來看第一句話,說是一個做用於某對象結構中的各元素的操做,這裏提到了三個事物,一個是對象結構,一個是各元素,一個是操做。那麼咱們能夠這麼理解,有這麼一個操做,它是做用於一些元素之上的,而這些元素屬於某一個對象結構。atom

好了,最關鍵的第二句來了,它說使用了訪問者模式以後,可讓咱們在不改變各元素類的前提下定義做用於這些元素的新操做。這裏面的關鍵點在於前半句,即不改變各元素類的前提下,在這個前提下定義新操做是訪問者模式精髓中的精髓。spa

結構圖

clipboard.png

訪問者模式涉及到6類角色: 抽象訪問者角色、具體訪問者角色、抽象節點角色、具體節點角色、結構對象角色以及客戶端角色。設計

  • Visitor接口:它定義了對每個元素(Element)訪問的行爲,它的參數就是能夠訪問的元素,它的方法個數理論上來說與元素個數(Element的實現類個數)是同樣的,從這點不難看出,訪問者模式要求元素類的個數不能改變(不能改變的意思是說,若是元素類的個數常常改變,則說明不適合使用訪問者模式)。
  • ConcreteVisitor:具體的訪問者,它須要給出對每個元素類訪問時所產生的具體行爲。
  • Element接口:元素接口,它定義了一個接受訪問者(accept)的方法,其意義是指,每個元素都要能夠被訪問者訪問。
  • ConcreteElement:具體的元素類,它提供接受訪問方法的具體實現,而這個具體的實現,一般狀況下是使用訪問者提供的訪問該元素類的方法。
  • ObjectStructure:這個即是定義當中所提到的對象結構,對象結構是一個抽象表述,具體點能夠理解爲一個具備容器性質或者複合對象特性的類,它會含有一組元素(Element),而且能夠迭代這些元素,供訪問者訪問。

使用場景

  • 須要對一個組合結構中的對象進行不少不相關的操做,可是不想讓這些操做「污染」這這些對象的類。能夠將相關的操做集中起來,定義在一個訪問者類中,並在訪問者定義的操做中使用它。
  • 數據結構穩定,做用於數據結構的操做常常變化的時候。
  • 當一個數據結構中,一些元素類須要負責與其不相關的操做的時候,爲了將這些操做分離出去,以減小這些元素類的職責時,可使用訪問者模式。
  • 有時在對數據結構上的元素進行操做的時候,須要區分具體的類型,這時使用訪問者模式能夠針對不一樣的類型,在訪問者類中定義不一樣的操做,從而去除掉類型判斷。

有意思的是,在不少狀況下不使用設計模式反而會獲得一個較好的設計。換言之,每個設計模式都有其不該當使用的狀況。訪問者模式也有其不該當使用的狀況,讓咱們
先看一看訪問者模式不該當在什麼狀況下使用。

傾斜的可擴展性

訪問者模式僅應當在被訪問的類結構很是穩定的狀況下使用。換言之,系統不多出現須要加入新節點的狀況。若是出現須要加入新節點的狀況,那麼就必須在每個訪問對象里加入一個對應於這個新節點的訪問操做,而這是對一個系統的大規模修改,於是是違背"開一閉"原則的。

訪問者模式容許在節點中加入新的方法,相應的僅僅須要在一個新的訪問者類中加入此方法,而不須要在每個訪問者類中都加入此方法。

顯然,訪問者模式提供了傾斜的可擴展性設計:方法集合的可擴展性和類集合的不可擴展性。換言之,若是系統的數據結構是頻繁變化的,則不適合使用訪問者模式。

"開一閉"原則和對變化的封裝

面向對象的設計原則中最重要的即是所謂的"開一閉"原則。一個軟件系統的設計應當儘可能作到對擴展開放,對修改關閉。達到這個原則的途徑就是遵循"對變化的封裝"的原則。這個原則講的是在進行軟件系統的設計時,應當設法找出一個軟件系統中會變化的部分,將之封裝起來。

不少系統能夠按照算法和數據結構分開,也就是說一些對象含有算法,而另外一些對象含有數據,接受算法的操做。若是這樣的系統有比較穩定的數據結構,又有易於變化的算法的話,使用訪問者模式就是比較合適的,由於訪問者模式使得算法操做的增長變得容易。

反過來,若是這樣一個系統的數據結構對象易於變化,常常要有新的數據對象增長進來的話,就不適合使用訪問者模式。由於在訪問者模式中增長新的節點很困難,要涉及到在抽象訪問者和全部的具體訪問者中增長新的方法。

應用示例

這裏爲了更接近實際項目開發而不是單純的紙上談兵,咱們以塗鴉板爲例來演示。

項目介紹

核心功能就是在屏幕上塗鴉,把手指滑動的軌跡繪製出來。至於顏色、粗細之類的咱們之後再添加,這裏只實現核心功能,主要目的是演示訪問者模式在實踐中的使用。

注意: 這裏以iOS項目爲例,基於CocoaTouch框架構建; 若是不熟悉能夠先查閱相關API再看一下內容。

設計

繪製前須要把手指在屏幕上劃過的點記錄下來。理論上咱們可使用所知的任何數據結構來存儲線條和點等。可是若是所有都用多維數組(好比說)來保存,使用和解析時就須要進行不少類型檢查。並且,數據結構並不一致和可靠,須要大量的調試。

若是一種數據結構能夠保存獨立的點,又能夠把點保存爲子節點的線條,可使用
把每個點和線條都組合到樹中,而咱們又但願可以統一的對待(處理)樹上的任意節點,這就能夠經過組合模式來實現了。

定義父類型Mark協議。Vertex、Dot和Stroke都是Mark的具體類。
Mark: 不論線條仍是點,其實都是在介質上留下的標誌(Mark),它爲全部具體類定義了屬性和方法。
Dot: 點,組件只有一個點,那麼它會表現爲一個實心圓,在屏幕上表明一個點。
Vertex: 頂點,鏈接起來的一串頂點,被繪製成鏈接起來的線條。
Stroke: 線條,一個線條實體,包含了若干的Vertex子節點
這樣當客戶端基於接口來操做具體類的時候,能夠統一對待每一個具體類,而沒必要在客戶端做類型檢查。Mark對象又有add方法,能夠把其它Mark對象加爲本身的子節點,造成組合體。

數據最後的組合結構圖是這樣的:

clipboard.png

代碼

Element

這裏Mark也是咱們要進行訪問的元素Element,它的定義以下

@protocol Mark <NSObject>

@property (nonatomic, assign) CGPoint location;
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, readonly) id <Mark> lastChild;

- (void)addMark:(id <Mark>)mark;
- (void)removeMark:(id <Mark>) mark;

- (void)acceptMarkVisitor:(id <MarkVisitor>)visitor;

@end

具體類型Dot的實現
因爲它就是一個圓點,不會真的有添加和移除子節點功能

@interface Dot : NSObject <Mark>

@property (nonatomic, assign) CGPoint location;
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, readonly) id <Mark> lastChild;

@end

@implementation Dot

- (void)addMark:(id <Mark>)mark { }

- (void)removeMark:(id <Mark>) mark { }

- (id <Mark>)lastChild { return nil; }

- (void)acceptMarkVisitor:(id <MarkVisitor>)visitor
{
    [visitor visitDot:self];
}

@end

具體類型Vertex的實現
它只是線條中的一個頂點,也不會有添加和移除子節點功能

@interface Vertex : NSObject <Mark>

@property (nonatomic, assign) CGPoint location;
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, readonly) id <Mark> lastChild;

@end

@implementation Vertex

- (void)addMark:(id <Mark>)mark { }

- (void)removeMark:(id <Mark>) mark { }

- (id <Mark>)lastChild { return nil; }

- (void)acceptMarkVisitor:(id <MarkVisitor>)visitor
{
    [visitor visitVertex:self];
}

@end

具體類型Stroke的實現

@interface Stroke : NSObject <Mark>

@property (nonatomic, assign) CGPoint location;
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, readonly) id <Mark> lastChild;

@end

@implementation Stroke
@dynamic location;

- (instancetype)init
{
    self = [super init];
    if (self) {
        _children = [NSMutableArray array];
    }
    return self;
}

- (void)addMark:(id <Mark>)mark
{
    [self.children addObject:mark];
}

- (void)removeMark:(id <Mark>) mark
{
    [self.children removeObject:mark];
}

- (void)setLocation:(CGPoint)aPoint
{
    // it doesn't set any arbitrary location
}

- (CGPoint)location
{
    // return the location of the first child
    if ([self.children count] > 0)
    {
        id <Mark> child = [self.children objectAtIndex:0];
        return [child location];
    }

    // otherwise returns the origin
    return CGPointZero;
}

- (id <Mark>)lastChild
{
    return [self.children lastObject];
}

- (void)acceptMarkVisitor:(id <MarkVisitor>)visitor
{
    for (id <Mark> dot in self.children) {
        [dot acceptMarkVisitor:visitor];
    }
    
    [visitor visitStroke:self];
}

@end

這裏有3中類型的元素Element,它們分別都在本身的acceptMarkVisitor方法裏調用visitor的visit方法,並把本身self做爲參數傳遞出去

訪問者

先定義抽象的Visitor接口
它定義了對每個元素(Element)訪問的行爲

@protocol MarkVisitor <NSObject>

- (void)visitMark:(id <Mark>)mark;
- (void)visitVertex:(Vertex *)vertex;
- (void)visitDot:(Dot *)dot;
- (void)visitStroke:(Stroke *)stroke;

@end

具體的訪問者,MarkRenderer繪製訪問者,它是對這些點和先進行繪製操做的

@interface MarkRenderer : NSObject <MarkVisitor>

- (instancetype)initWithCGContext:(CGContextRef)context;

@end

@interface MarkRenderer ()

@property (nonatomic, assign) CGContextRef context;
@property (nonatomic, assign) BOOL shouldMoveContextToDot;

@end

@implementation MarkRenderer

- (instancetype)initWithCGContext:(CGContextRef)context
{
    self = [super init];
    if (self) {
        _context = context;
        _shouldMoveContextToDot = YES;
    }
    return self;
}

- (void)visitMark:(id <Mark>)mark
{
    // default behavior
}

- (void)visitVertex:(Vertex *)vertex
{
    CGFloat x = vertex.location.x;
    CGFloat y = vertex.location.y;
    if (self.shouldMoveContextToDot) {
        CGContextMoveToPoint(self.context, x, y);
        self.shouldMoveContextToDot = NO;
    } else {
        CGContextAddLineToPoint(self.context, x, y);
    }
}
- (void)visitDot:(Dot *)dot
{
    CGFloat x = dot.location.x;
    CGFloat y = dot.location.y;
    CGRect frame = CGRectMake(x, y, 2, 2);
    
    CGContextSetFillColorWithColor(self.context, [UIColor blackColor].CGColor);
    CGContextFillEllipseInRect(self.context, frame);
}

- (void)visitStroke:(Stroke *)stroke
{
    CGContextSetStrokeColorWithColor(self.context, [UIColor blueColor].CGColor);
    CGContextSetLineWidth(self.context, 1);
    CGContextSetLineCap(self.context, kCGLineCapRound);
    CGContextStrokePath(self.context);
    self.shouldMoveContextToDot = YES;
}

@end

Client調用

前提是咱們已經把數據都蒐集好了,放在具體的Mark結構體中,而後在一個具體的view類的drawRect方法中調用就能夠了

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    MarkRenderer *markRender = [[MarkRenderer alloc] initWithCGContext:context];
    [self.mark acceptMarkVisitor:markRender];
}

運行效果圖,具體能夠看源碼源碼下載

clipboard.png

訪問者拓展

這個例子,用繪製節點對象的訪問者(MarkRenderer)拓展了Mark家族類,這樣就能夠把它們顯示到屏幕上了。還能夠在增長一個訪問者,好比,訪問Mark組合體每一個節點,對它實施仿射變換(旋轉、縮放、平移等)。在不改變組合結構的前提下,咱們拓展了它的功能。

雙分派技術

這個模式有一個繞的地方是採用了雙分派技術,其實它自己本不難,就是過程有點曲折而已。
第一次分派: 把具體的訪問者對象傳遞給結構對象,好比上面例子中的MarkRenderer傳遞給了Mark對象,MarkRenderer只是某一個具體的visitor,客戶端也能夠傳遞其它的visitor過來,作不同的操做。
第二次分派: 當Mark接到具體的visitor對象過來後,具體的Mark實例會根據本身的類型調用visitor對應的方法,並把本身(self)做爲參賽傳遞過去,這就完成了第二次分派。

經過上面的分析發現它並不難,單這樣的目的是什麼呢? 其實這就意味着最後獲得的操做是由兩個具體對象的類型(具體元素和具體訪問者)來決定的。

總結

  • 訪問者模式適用於數據結構相對穩定的系統

    它把數據結構和做用於數據結構上的操做之間的耦合解脫開,使得操做集合能夠相對自由的演化
  • 訪問者模式的目的是要把處理從數據結構分離出來。

    不少系統能夠安裝算法和數據結構分開,若是這樣的系統有比較穩定的數據結構,又有易於變化算法的話,使用訪問者模式就是比較合適的,由於訪問者模式使得算法操做的增長變更容易。
  • 訪問者模式的有點就是增長新的操做很容易,由於增長新的操做就意味着增長一個新的訪問者。訪問者模式將有關的行爲集中到一個訪問者對象中。
  • 反之,訪問者的缺點也就是增長新的數據結構變得困難了。

《設計模式》做者GoF四人中的一個說過:大多數狀況下,你並須要使用訪問者模式,可是當你一旦須要使用它時,那你就是真的須要它了。

相關文章
相關標籤/搜索