25、设计模式:享元与代理模式解析

设计模式:享元与代理模式解析

1. 享元模式介绍

享元模式(Flyweight Pattern)利用共享技术高效支持大量细粒度对象。当满足以下条件时,可考虑使用该模式:
- 应用程序使用大量对象。
- 内存中保存对象会影响性能。
- 对象的大部分唯一状态(外部状态)可外部化且轻量化。
- 去除对象的外部状态后,相对较少的共享对象可替代原有的大量对象。
- 应用程序不依赖对象标识,因为共享对象没有唯一标识。

为了说明该模式的概念,我们将开发一个应用程序,从可共享的花池中绘制数百个或更多的花图案。

2. 创建百花池应用

我们要创建一个小应用,在屏幕上显示大量随机的花图像。有六种花需要显示,分别是银莲花、大波斯菊、非洲菊、蜀葵、茉莉和百日草。目标是仅用这六种花的唯一实例,绘制大量(数百个或更多)随机大小和位置的花。如果为屏幕上绘制的每个实例都创建一个花实例,应用程序可能会消耗大量内存。因此,我们使用享元模式将花实例数量限制为可选花的类型总数。

3. 享元模式设计与实现
  • 享元工厂与产品 :需要一个享元工厂和一些享元产品。 FlowerView UIImageView 的子类,可自定义绘制花图像。应用的享元工厂是 FlowerFactory ,它管理 FlowerView 实例池。虽然池中对象的类是 FlowerView ,但客户端期望从 FlowerFactory 返回的是 UIView 实例,这样设计更灵活。
  • 可共享花的设计与实现 :有六种花图像,每种图像由 FlowerView 的唯一实例维护。无论需要返回多少花, FlowerFactory 只返回 FlowerView 的唯一实例。
  • FlowerView 类的实现
// FlowerView.h
@interface FlowerView : UIImageView  
{
}
- (void) drawRect:(CGRect)rect;
@end

// FlowerView.m
#import "FlowerView.h"
@implementation FlowerView
- (void) drawRect:(CGRect)rect
{
  [self.image drawInRect:rect];
}
@end
  • FlowerFactory 类的实现
// FlowerFactory.h
@interface FlowerFactory : NSObject  
{
  @private
  NSMutableDictionary *flowerPool_;
}
- (UIView *) flowerViewWithType:(FlowerType)type;
@end

// 定义 FlowerType
typedef enum  
{
  kAnemone,
  kCosmos,
  kGerberas,
  kHollyhock,
  kJasmine,
  kZinnia,
  kTotalNumberOfFlowerTypes
} FlowerType;

// FlowerFactory.m
#import "FlowerFactory.h"
#import "FlowerView.h"
@implementation FlowerFactory
- (UIView *) flowerViewWithType:(FlowerType)type
{
  // 懒加载花池
  if (flowerPool_ == nil)
  {
    flowerPool_ = [[NSMutableDictionary alloc]  
                   initWithCapacity:kTotalNumberOfFlowerTypes];
  }
  // 尝试从池中检索花
  UIView *flowerView = [flowerPool_ objectForKey:[NSNumber  
                                                  numberWithInt:type]];
  // 如果请求的类型不可用,则创建一个新的并添加到池中
  if (flowerView == nil)
  {
    UIImage *flowerImage;
    switch (type)
    {
      case kAnemone:
        flowerImage = [UIImage imageNamed:@"anemone.png"];
        break;
      case kCosmos:
        flowerImage = [UIImage imageNamed:@"cosmos.png"];
        break;
      case kGerberas:
        flowerImage = [UIImage imageNamed:@"gerberas.png"];
        break;
      case kHollyhock:
        flowerImage = [UIImage imageNamed:@"hollyhock.png"];
        break;
      case kJasmine:
        flowerImage = [UIImage imageNamed:@"jasmine.png"];
        break;
      case kZinnia:
        flowerImage = [UIImage imageNamed:@"zinnia.png"];
        break;
      default:
        break;
    }  
    flowerView = [[[FlowerView alloc]  
                   initWithImage:flowerImage] autorelease];
    [flowerPool_ setObject:flowerView  
                    forKey:[NSNumber numberWithInt:type]];
  }
  return flowerView;
}
- (void) dealloc
{
  [flowerPool_ release];
  [super dealloc];
}
@end

花池( flowerPool_ )不在 FlowerFactory init 方法中初始化,而是在 flowerViewWithType: 工厂方法中懒加载。如果请求的实例不在池中,工厂将创建一个新的 FlowerView 实例,并将其添加到池中。

4. FlowerView 对象的共享

享元对象通常与某种可共享的内部状态相关联, FlowerView 享元对象共享其内部花图像作为内部状态。无论享元对象是否有可共享的内部状态,都需要定义某种外部数据结构来跟踪享元对象的外部状态(唯一信息)。我们定义了一个 ExtrinsicFlowerState 的 C 结构体:

typedef struct 
{
  UIView *flowerView;
  CGRect area;
} ExtrinsicFlowerState;

每个花的显示区域是唯一的,因此需要将其视为外部状态。我们需要一个数组来存储客户端创建的花的外部状态。

5. 享元模式的节省效果

使用享元对象能节省多少资源取决于以下因素:
| 因素 | 说明 |
| ---- | ---- |
| 对象总数的减少 | 来自共享的对象数量减少 |
| 每个对象的内部状态量 | 即可共享状态的量 |
| 外部状态的处理方式 | 是计算还是存储 |

然而,传输、查找和/或计算享元对象的外部状态可能会带来运行时成本。随着更多享元对象被共享,这种成本可以被节省的空间所抵消。共享的享元对象越多,存储节省就越大。当对象同时使用大量的内部和外部状态,且外部状态可以计算而不是存储时,节省效果最佳。

6. 客户端代码实现
- (void)viewDidLoad  
{
  [super viewDidLoad];
  // 构建花列表
  FlowerFactory *factory = [[[FlowerFactory alloc] init] autorelease];
  NSMutableArray *flowerList = [[[NSMutableArray alloc]  
                                 initWithCapacity:500] autorelease];
  for (int i = 0; i < 500; ++i)
  {
    // 从花工厂获取随机花类型的共享花实例
    FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
    UIView *flowerView = [factory flowerViewWithType:flowerType];
    // 设置花在屏幕上的位置和区域
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
    CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
    NSInteger minSize = 10;
    NSInteger maxSize = 50;
    CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;
    // 为花分配属性到外部状态对象
    ExtrinsicFlowerState extrinsicState;
    extrinsicState.flowerView = flowerView;
    extrinsicState.area = CGRectMake(x, y, size, size);
    // 将外部花状态添加到花列表
    [flowerList addObject:[NSValue value:&extrinsicState  
                            withObjCType:@encode(ExtrinsicFlowerState)]];
  }
  // 将花列表添加到 FlyweightView 实例
  [(FlyweightView *)self.view setFlowerList:flowerList];
}

// 自定义视图重写 drawRect: 方法绘制花
- (void)drawRect:(CGRect)rect  
{
  // 绘图代码
  for (NSValue *stateValue in flowerList_)
  {
    ExtrinsicFlowerState state;
    [stateValue getValue:&state];
    UIView *flowerView = state.flowerView;
    CGRect area = state.area;
    [flowerView drawRect:area];
  }
}

在主循环的每次迭代中, FlowerFactory 根据随机选择的花类型返回一个 UIView 实例(实际上是 FlowerView )。将唯一的位置和大小放入 ExtrinsicFlowerState 结构中,并将其添加到 flowerList 中。当 500 朵花的完整列表分配到数组后,将列表设置到 FlyweightView 实例。

7. 代理模式介绍

代理模式是一种常见的设计模式,在很多场景中都有应用。例如,在线约会网站提供免费搜索、调情等有限功能,但不提供与其他成员的实际交流。直到用户觉得值得支付费用时,才可以访问更多仅付费成员可用的功能。代理模式就是基于这种思想,代理作为一个替身或占位符,控制对另一个对象的访问。

8. 代理模式的类型
  • 远程代理 :为不同地址空间或网络中的对象提供本地代表。
  • 虚拟代理 :按需创建重量级对象。
  • 保护代理 :根据不同的访问权限控制对原始对象的访问。
  • 智能引用代理 :计算对真实对象的引用数量以进行内存管理,也可用于锁定真实对象,防止其他对象更改。
9. 虚拟代理实现图片懒加载

在某些应用中,如 TouchPainter 应用的缩略图视图,用户要浏览文件系统中所有以前保存的涂鸦。但加载所有缩略图不实际,尤其是设备内存有限时。在原始设计中,涂鸦以小缩略图的形式逐行排列在屏幕上。每个缩略图占位符在实际缩略图图像加载之前,显示一个占位符图像。当真实缩略图图像完全加载后,原始占位符图像将被替换。

这种用户界面在一些 iOS 应用中用于在用户滚动视图时加载缩略图,在后台加载单个小缩略图时,为用户提供流畅的操作和响应。

以下是享元模式中客户端代码绘制花的流程 mermaid 图:

graph TD;
    A[开始] --> B[初始化 FlowerFactory 和 flowerList];
    B --> C{循环 500 次};
    C -- 是 --> D[随机选择花类型];
    D --> E[从 FlowerFactory 获取花实例];
    E --> F[设置花的位置和大小];
    F --> G[创建 ExtrinsicFlowerState 结构];
    G --> H[将 ExtrinsicFlowerState 编码并添加到 flowerList];
    H --> C;
    C -- 否 --> I[将 flowerList 设置到 FlyweightView];
    I --> J[结束];

以下是虚拟代理加载图片的流程 mermaid 图:

graph TD;
    A[用户进入缩略图视图] --> B[显示占位符图像];
    B --> C{是否有缩略图需要加载};
    C -- 是 --> D[后台加载缩略图];
    D --> E{缩略图是否加载完成};
    E -- 是 --> F[替换占位符图像为真实缩略图];
    F --> C;
    C -- 否 --> G[结束加载];
    E -- 否 --> D;

设计模式:享元与代理模式解析

10. 代理模式的概念与工作原理

代理模式的核心概念是代理作为一个替身或占位符,控制对另一个对象的访问。当客户端向代理对象发送“请求”消息时,代理对象会将相同的消息转发给真实主题对象(RealSubject),由真实主题对象执行实际操作来间接满足客户端的请求。

在运行时,客户端拥有一个抽象类型的主题引用,实际上这个引用指向的是代理对象,而代理对象内部又持有一个真实主题实例的引用,当需要执行重型任务时,由真实主题实例来完成。

11. 何时使用代理模式

以下情况可以考虑使用代理模式:
- 需要远程代理为不同地址空间或网络中的对象提供本地代表。
- 需要虚拟代理按需创建重量级对象,如上述提到的图片懒加载场景。
- 需要保护代理根据不同的访问权限控制对原始对象的访问。
- 需要智能引用代理计算对真实对象的引用数量以进行内存管理,或锁定真实对象防止其他对象更改。

12. 虚拟代理在图片加载中的详细实现

TouchPainter 应用的缩略图视图中,为了实现图片的懒加载,我们可以使用虚拟代理。以下是一个简化的实现思路:

首先,定义一个抽象的图片协议,让真实图片对象和代理图片对象都遵循这个协议:

@protocol ImageProtocol <NSObject>
- (void)displayImage;
@end

然后,实现真实图片对象:

@interface RealImage : NSObject <ImageProtocol>
- (instancetype)initWithImageName:(NSString *)imageName;
@end

@implementation RealImage
{
    NSString *_imageName;
    UIImage *_image;
}
- (instancetype)initWithImageName:(NSString *)imageName
{
    self = [super init];
    if (self) {
        _imageName = imageName;
        _image = [UIImage imageNamed:imageName];
    }
    return self;
}
- (void)displayImage
{
    // 显示真实图片
    NSLog(@"Displaying real image: %@", _imageName);
}
@end

接着,实现虚拟代理图片对象:

@interface ProxyImage : NSObject <ImageProtocol>
- (instancetype)initWithImageName:(NSString *)imageName;
@end

@implementation ProxyImage
{
    NSString *_imageName;
    RealImage *_realImage;
}
- (instancetype)initWithImageName:(NSString *)imageName
{
    self = [super init];
    if (self) {
        _imageName = imageName;
    }
    return self;
}
- (void)displayImage
{
    if (!_realImage) {
        _realImage = [[RealImage alloc] initWithImageName:_imageName];
    }
    [_realImage displayImage];
}
@end

在客户端代码中使用代理对象:

- (void)viewDidLoad
{
    [super viewDidLoad];
    ProxyImage *proxyImage = [[ProxyImage alloc] initWithImageName:@"thumbnail.png"];
    // 当需要显示图片时调用
    [proxyImage displayImage];
}
13. 代理模式的优势
  • 性能优化 :如虚拟代理按需创建重量级对象,避免一次性加载大量资源,提高应用性能。
  • 访问控制 :保护代理可以根据不同的访问权限控制对原始对象的访问,增强系统的安全性。
  • 延迟加载 :通过代理模式可以延迟对真实对象的操作,直到真正需要时才进行,节省系统资源。
14. 享元与代理模式的对比
模式 特点 应用场景
享元模式 利用共享技术支持大量细粒度对象,节省内存 当应用使用大量对象,且对象有可共享的内部状态时
代理模式 作为替身或占位符控制对另一个对象的访问 如远程访问、按需创建对象、访问控制等场景
15. 总结

享元模式和代理模式都是非常实用的设计模式,它们在不同的场景下发挥着重要作用。享元模式通过共享对象节省内存,提高资源利用率;代理模式通过控制对对象的访问,实现性能优化、访问控制和延迟加载等功能。在实际开发中,我们可以根据具体的需求选择合适的设计模式,以提高应用的性能和可维护性。

以下是代理模式中虚拟代理加载图片的详细流程 mermaid 图:

graph TD;
    A[客户端请求显示图片] --> B[创建 ProxyImage 对象];
    B --> C{RealImage 是否已创建};
    C -- 否 --> D[创建 RealImage 对象];
    D --> E[加载真实图片];
    E --> F[显示真实图片];
    C -- 是 --> F;
    F --> G[结束];

通过对享元模式和代理模式的学习和应用,我们可以更好地设计和实现高效、灵活的软件系统。在后续的开发中,我们可以根据具体的业务需求,合理运用这些设计模式,提升软件的质量和性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值