行为型模式之访问者模式

本文介绍了访问者模式,它能将数据结构和操作分离,解决稳定数据结构与易变操作的耦合问题。通过农场场景说明其应用,阐述了模式结构。还以屏幕涂鸦项目为例给出示例代码,讲解了模式功能、调用通路和两次分发技术,可在不改变对象结构时增加功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

定义

表示一个作用于某对象结构中的各元素的操作。它让我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

意图: 主要将数据结构和数据操作分离
主要解决:稳定的数据结构和易变的操作耦合问题。
何时使用

  1. 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
  2. 数据结构稳定,作用于数据结构的操作经常变化的时候
  3. 当一个数据结构中,一些元素类需要负责与其不相关的操作的时候,为了将这些操作分离出去,以减少这些元素类的职责时,可以使用访问者模式。
  4. 有时在对数据结构上的元素进行操作的时候,需要区分具体的类型,这时使用访问者模式可以针对不同的类型,在访问者类中定义不同的操作,从而去除掉类型判断。
    如何解决:在被访问的类里面加一个对外提供接待访问者的接口。

上面都是一堆通用的定义或者说是古板的说法,大家肯定还是蒙圈,下面来使用场景说明

场景

比如有一个农场(结构体),里面包括木头,牛羊,空闲的土地(结构体力的元素),现在有两个需求:
需求一: 我要在这里生活,所以要建房子、生火做饭
需求二: 我要在这里开工厂,所以要建厂房、生产火腿肠
这些需求都是不同人对农场里的材料有着不同的需求

现在咱们来实现上面的需求,通常是在这个类里添加2个方法,一个方法建房子生火做饭,另一个方法建厂房生产火腿.但这样大家想想会有上面问题呢?如果这时候有军队提出需求,需要建堡垒、生产肉罐头。我们就需要再加一个新方法来满足它。
这样就会出现一些问题:

  1. 不符合开放封闭原则,类过于复杂,不利于维护
  2. 功能越多,类就越臃肿

那么怎么解决上面的问题呢?
通过上面的例子,我们知道这个农场是稳定的(里面就是木材、牛羊和土地),但是大家的需求不一样,导致对这个农场进行的操作也不一样。
那我们可不可以把农场和对农场的操作分离出来,不同的人来访问这个农场就会进行不同的操作

在这里插入图片描述
农民来访问它,建房子生火做饭
商人来访问它,建厂房生产火腿
军人访问它,建堡垒生产肉罐头

好处: 操作集合从数据结构中分离出来了,可以相对独立自由的演化。

上面的解决方案就是访问者模式,其关键点在于不改变各元素,在这个前提下定义新操作是访问者模式精髓中的精髓

模式结构和说明

在这里插入图片描述

Visitor: 访问者,为所有的访问者对象声明一个visit方法,用来代表为对象结构添加的功能,理论上可以代表任意的功能
ConcreteVisitor:具体的访问者实现对象,实现要真正被添加到对象结构中的功能
Element:抽象的元素对象,对象结构的顶层接口,定义接受访问的操作
ConcreteElement:具体元素对象,对象结构中具体对象,也是被访问的对象,通常会回调访问者的真实功能,同事开放自身的数据供访问者使用
ObjectStructure:对象结构,通常包含多个被访问的对象,它可以遍历这多个被访问的对象,也可以让访问者访问它的元素.可以是一个符合或者是一个集合,如一个列表或者无序集合

项目介绍

在屏幕上涂鸦,即把手指滑动的轨迹绘制出来

设计

大家想想,你要绘制轨迹,就需要把手指在屏幕上划过的点记录下来,理论上我们可以使用所知的任何数据结构来存储线条和点等.但是如果全部都要用多维数组来保存,使用和解析时就需要进行很多类型检查,而且数据结构并不一致和可靠,需要大量的调试
如果一种数据结构可以保存独立的点,又可以把点保存为子节点的线条,可以使用树。把每一个点和线条都组合到树中,而我们又希望能够统一的对待(处理)树上的任意节点,这就可以通过组合模式来实现了。

定义父类型Mark协议。Vertex、Dot和Stroke都是Mark的具体类。
Mark: 不论线条还是点,其实都是在介质上留下的标志(Mark),它为所有具体类定义了属性和方法。
Dot: 点,组件只有一个点,那么它会表现为一个实心圆,在屏幕上代表一个点。
Vertex: 顶点,连接起来的一串顶点,被绘制成连接起来的线条。
Stroke: 线条,一个线条实体,包含了若干的Vertex子节点
这样当客户端基于接口来操作具体类的时候,可以统一对待每个具体类,而不必在客户端作类型检查。Mark对象又有add方法,可以把其它Mark对象加为自己的子节点,形成组合体。
在这里插入图片描述

示例代码

  1. 定义Mark协议,这儿的Mark也是我们要进行访问的元素Element
@protocol Mark <NSObject>

/**/
@property (nonatomic,assign) CGPoint location;
/**/
@property (nonatomic,assign) CGSize size;
/**/
@property (nonatomic,strong) id<Mark> lastChild;

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

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

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

@end
  1. 定义Mark的具体类(Vertex、Dot和Stroke)
//由于它就是一个圆点,不会真的有添加和移除子节点功能
@implementation Dot

@synthesize lastChild;

@synthesize size;

@synthesize location;

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

@end
//它只是线条中的一个顶点,也不会有添加和移除子节点功能
@implementation Vertex

@synthesize lastChild;

@synthesize location;

@synthesize size;
- (void)acceptMarkVisitor:(id<MarkVisitor>)visitor
{
    [visitor visitVertex:self];
}
@end
@implementation Stroke
@dynamic location;
- (void)setLocation:(CGPoint)location
{
    
}

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

@synthesize lastChild;

@synthesize size;

- (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];
}

- (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作为参数传递出去

  1. 定义抽象的Visitor接口
@protocol  Mark;
@class Dot,Vertex,Stroke;
@protocol MarkVisitor <NSObject>

- (void)visitMark:(id<Mark>)mark;

- (void)visitDot:(Dot *)dot;

- (void)visitVertex:(Vertex *)vertex;

- (void)visitStroke:(Stroke *)stroke;

@end
  1. 定义具体的访问者,MarkRender绘制访问者,它是对这些点和先进行绘制操作的
@interface MarkRender()

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


@end

@implementation MarkRender

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


- (void)visitMark:(id<Mark>)mark
{
    
}
- (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)visitVertex:(Vertex *)vertex {
    CGFloat x = vertex.location.x;
    CGFloat y = vertex.location.y;
    if (self.shouldMoveContextToDot) {
        CGContextMoveToPoint(self.context, x, y);
        _shouldMoveContextToDot = NO;
    } else {
        CGContextAddLineToPoint(self.context, x, y);
    }
    
}
- (void)visitStroke:(Stroke *)stroke
{
    CGContextSetStrokeColorWithColor(self.context, [UIColor blueColor].CGColor);
    CGContextSetLineWidth(self.context, 1);
    CGContextSetLineCap(self.context, kCGLineCapRound);
    CGContextStrokePath(self.context);
    self.shouldMoveContextToDot = YES;
}
  1. 客户端调用
- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    MarkRender *render = [[MarkRender alloc] initWithCGContext:context];
    [self.mark acceptMarkVisitor:render];
}

在这个例子中,用绘制节点对象的访问者(MarkRender)拓展了Mark家族类,这样就可以把他们显示到屏幕上,如果你还有别的业务,比如访问Mark组合体每个节点,对它实施仿射变换(旋转、缩放、平移等),那就增加一个访问者,在不改变组合结构的前提下,我们扩展了它的功能

模式讲解

1. 模式的功能

访问者模式能给一系列对象,透明的添加新功能。从而避免在维护期间,对这一系列对象进行修改,而且还能变相实现复用访问者所具有的功能。

2. 调用通路

访问者之所以能实现"为一系列对象透明的添加新功能",注意透明的,也就是这一系列对象是不知道被添加功能的.重要的就是依靠通用方法,访问者这边说要去访问,就提供一个访问的方法,如visit方法;而对象那边说,好的,我接受你的访问,提供一个接受访问的方法,如accept方法.这两个方法并不代表任何具体的功能,只是构成一个调用的通路,那么真正的功能实现在哪里呢?又如何调用到呢?

很简单,就在accept方法里面,回调visit的方法,从而回调到访问者的具体实现上,而这个访问者的具体实现的方法才是要添加的新的功能。

3. 两次分发技术

访问者模式能够实现在不改变对象结构的情况下,就能给对象结构中的类增加功能,实现这个效果所使用的核心技术就是两次分发的技术。

  1. 把具体的访问者对象传递给结构对象,比如上面例子中的MarkRenderer传递给了Mark对象,MarkRender只是某一个具体的visitor,客户端也可以传递其它的visitor过来,做不一样的操作。
  2. 当Mark接到具体的visitor对象过来后,具体的Mark实例会根据自己的类型调用visitor对应的方法,并把自己(self)作为参赛传递过去,这就完成了第二次分派。

两次分发技术使得客户端的请求不再被静态的绑定在元素对象上,这个时候真正执行什么样的功能同时取决于访问者类型和元素类型,就算是同一种元素类型,只要访问者类型不一样,最终执行的功能也不会一样,这样一来,就可以在元素对象不变的情况下,通过改变访问者的类型,来改变真正执行的功能。

两次分发技术还有一个优点,就是可以在程序运行期间进行动态的功能组装和切换,只需要在客户端调用时,组合使用不同的访问者对象实例即可。

Demo地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值