24种设计模式代码实例学习(三)结构型模式

2,359 阅读35分钟

项目Demo

上一篇文章中,我们介绍了创建型模式,在这一篇文章中,我们将来介绍结构型模式。

结构型模式和创建型模式是设计模式中的两个主要分类。

创建型模式关注对象的创建机制,主要解决对象的实例化过程。创建型模式涉及到对象的创建、组合和表示,以及隐藏对象的创建逻辑。

结构型模式关注的是对象之间的组合和关联方式,以构建更大的结构和功能。它们着重于类和对象之间的静态关系,以及如何将它们组织在一起形成更大的结构,以满足系统的需求。结构型模式可以帮助我们在保持系统灵活性和可扩展性的同时,提供清晰的对象组织结构

总的来说,创建型模式更关注的是对象创建的灵活性和可维护性,结构型模式更关注的是系统结构的设计和组织。

0 常见的结构型模式

  1. 适配器模式(Adapter Pattern)
  2. 桥接模式(Bridge Pattern)
  3. 组合模式(Composite Pattern)
  4. 装饰器模式(Decorator Pattern)
  5. 外观模式(Facade Pattern)
  6. 享元模式(Flyweight Pattern)
  7. 代理模式(Proxy Pattern)

这些结构型模式都有不同的应用场景和优势,可以根据具体的需求来选择合适的模式来改善软件设计的灵活性、可维护性和可扩展性。

1 适配器模式(Adapter Pattern)

image.png

用于将一个类的接口转换成另一个类的接口,以便两个不兼容的类能够一起工作

适配器模式可以解决在系统中使用已有的类,但其接口与需要的接口不匹配的情况。

使用方法:适配器继承依赖已有的对象,实现想要的目标接口。用下面的两个例子说明这两种情况:

例子

继承

适配器可以通过继承已有的对象(旧接口),并实现目标接口来进行适配。通过继承,适配器获得了旧接口的功能,并且可以添加或重写方法来实现目标接口的要求

// 旧接口
@interface OldAPI : NSObject

- (void)legacyRequest;

@end

@implementation OldAPI

- (void)legacyRequest {
    NSLog(@"执行旧接口的请求");
}

@end

// 目标接口
@protocol NewAPI <NSObject>

- (void)newRequest;

@end

// 适配器子类,继承自旧接口类并实现目标接口
@interface Adapter : OldAPI <NewAPI>

- (void)newRequest;

@end

@implementation Adapter

- (void)newRequest {
    [self legacyRequest]; // 调用旧接口的方法
    NSLog(@"执行适配后的新请求");
}

@end

客户端代码:

id<NewAPI> api = [[Adapter alloc] init]; 
[api newRequest];

在这个例子中,OldAPI是旧的接口类,它有一个legacyRequest方法。NewAPI是目标接口,它定义了一个newRequest方法。适配器类Adapter继承自OldAPI,并实现了NewAPI接口。

通过继承,Adapter类继承了legacyRequest方法,然后在newRequest方法中调用了legacyRequest方法来执行旧接口的请求,并添加了适配后的新请求。

依赖

适配器也可以通过依赖已有的对象(旧接口),并将其作为成员变量来实现适配。适配器可以通过委派调用已有对象的方法,并根据目标接口的要求对返回结果进行适配处理。

// 旧接口
@interface OldAPI : NSObject

- (void)legacyRequest;

@end

@implementation OldAPI

- (void)legacyRequest {
    NSLog(@"执行旧接口的请求");
}

@end

// 目标接口
@protocol NewAPI <NSObject>

- (void)newRequest;

@end

// 适配器类,通过依赖旧接口类来实现适配
@interface Adapter : NSObject <NewAPI>

@property (nonatomic, strong) OldAPI *oldAPI;

- (void)newRequest;

@end

@implementation Adapter

- (void)newRequest {
    [self.oldAPI legacyRequest]; // 调用旧接口的方法
    NSLog(@"执行适配后的新请求");
}

@end

客户端代码:

DependencyAdapter *adapter = [[DependencyAdapter alloc] init];
adapter.oldAPI = [[OldAPI alloc] init];
[adapter newRequest];

Adapter适配器类通过依赖旧接口类OldAPI来实现适配。适配器类持有一个OldAPI对象的实例,并实现了NewAPI接口。

在适配器的newRequest方法中,适配器通过调用持有的OldAPI对象的legacyRequest方法来执行旧接口的请求,并在之后添加了适配后的新请求的逻辑。

在客户端代码中,我们创建了适配器类Adapter的实例,并将旧接口类OldAPI的实例设置为适配器类的oldAPI属性。然后调用适配器类的newRequest方法来发起新的请求。

无论是继承还是依赖,适配器都充当了一个中间层,通过转换和包装旧接口的功能,使其符合目标接口的要求。适配器将客户端的调用转换为对旧接口的调用,并对旧接口的返回结果进行适配,以满足客户端的期望。

适配器模式的缺点:

  1. 增加代码复杂性:适配器模式引入了额外的类和逻辑,增加了代码的复杂性。适配器的存在可能会增加代码的理解和维护成本。
  2. 运行时性能损耗:数据或方法的转换需要额外的处理步骤。
  3. 可能会导致过多的适配器类:在某些情况下,如果系统中存在大量不兼容的类,可能需要创建大量的适配器类来进行适配,这可能会导致类的数量过多,增加了系统的复杂性。

总体而言,适配器模式在解决接口不兼容问题提高代码复用性方面具有明显的优点。然而,在设计和使用适配器时需要权衡好代码复杂性、运行时性能以及适配器的数量,确保适配器模式的使用是合理的。

2 桥接模式(Bridge Pattern)

image.png

桥接模式用于把抽象化与实现化解耦,使得二者可以独立变化。他们分离后,可以独立地变化。桥接模式通过组合而不是继承来实现这种分离。

在桥接模式中,抽象部分(Abstraction)和实现部分(Implementation)分别定义了两个独立的类层次结构。抽象部分包含一个指向实现部分的引用,并且通过这个引用进行交互。这样,抽象部分就可以与不同的实现部分进行桥接,而不会与特定的实现部分耦合。

桥接模式中的“桥”就是协议接口,符合依赖倒转原则和面向接口的编程。

例子

简单例子

// 实现部分的接口
@protocol Implementor <NSObject>
- (void)doSomething;
@end

// 实现部分的具体实现类A
@interface ConcreteImplementorA : NSObject <Implementor>
@end

@implementation ConcreteImplementorA

- (void)doSomething {
    NSLog(@"Concrete Implementor A is doing something.");
}

@end

// 实现部分的具体实现类B
@interface ConcreteImplementorB : NSObject <Implementor>
@end

@implementation ConcreteImplementorB

- (void)doSomething {
    NSLog(@"Concrete Implementor B is doing something.");
}

@end

// 抽象部分
@interface Abstraction : NSObject

@property (nonatomic, strong) id<Implementor> implementor;

- (void)doAction;

@end

@implementation Abstraction

- (void)doAction {
    [self.implementor doSomething];
}

@end

客户端代码:

// 创建具体的实现对象A
ConcreteImplementorA *implementorA = [[ConcreteImplementorA alloc] init];
// 创建抽象部分对象,并将具体的实现对象A桥接进去
Abstraction *abstraction = [[Abstraction alloc] init];
abstraction.implementor = implementorA;
// 调用抽象部分的操作
[abstraction doAction];

// 创建具体的实现对象B
ConcreteImplementorB *implementorB = [[ConcreteImplementorB alloc] init];
// 将具体的实现对象B桥接进抽象部分对象
abstraction.implementor = implementorB;
// 调用抽象部分的操作
[abstraction doAction];

在上述代码中,我们有一个实现部分的接口 Implementor,以及两个具体实现类 ConcreteImplementorAConcreteImplementorB。抽象部分 Abstraction 持有一个 Implementor 引用,并通过它调用实现部分的操作。

在客户端代码中,我们首先创建了一个具体的实现对象 ConcreteImplementorA,然后创建了抽象部分对象 Abstraction,并将实现对象A桥接进去。当调用抽象部分的操作[abstraction doAction] 时,实际上是通过抽象部分与具体实现对象A的桥接,执行了实现对象A的具体操作,输出了 "Concrete Implementor A is doing something."。

接下来,我们创建了另一个具体的实现对象 ConcreteImplementorB,并将其桥接进抽象部分对象。再次调用抽象部分的操作 [abstraction doAction] 时,由于实现对象B替换了实现对象A的桥接,输出变为了 "Concrete Implementor B is doing something."。

通过这个例子,我们可以看到桥接模式的优点:抽象部分和实现部分可以独立地扩展和变化,它们之间的关系是通过桥接而不是继承建立的。 这种分离允许我们在不影响其他部分的情况下修改抽象部分或实现部分,从而提高了系统的灵活性和可扩展性。 在iOS开发中,桥接模式可以应用于以下方面:

  1. 网络请求库: 当你需要进行网络请求时,可以使用桥接模式将网络请求库与具体的网络协议(如HTTP、WebSocket等)进行解耦。你可以定义一个抽象的网络请求接口,并针对每种具体的网络协议实现一个具体的网络请求类。通过桥接模式,可以灵活地切换或添加新的网络协议实现。
  2. 视图控制器和视图之间的桥接: 在iOS应用中,视图控制器负责管理视图的显示和交互逻辑,而视图负责界面的展示。你可以使用桥接模式来将视图控制器与具体的视图实现进行解耦。通过定义一个抽象的视图接口,并针对不同的界面元素(如按钮、标签等)实现具体的视图类,可以实现视图控制器和视图的独立变化和组合。

网络请求库

// 抽象的网络请求接口
@protocol NetworkRequestProtocol <NSObject>
- (void)sendRequestWithURL:(NSURL *)url;
@end

// 具体的网络请求类 - 使用HTTP协议
@interface HTTPRequest : NSObject <NetworkRequestProtocol>
@end

@implementation HTTPRequest
- (void)sendRequestWithURL:(NSURL *)url {
    NSLog(@"Sending HTTP request to URL: %@", url);
    // 发送HTTP请求的具体实现
}
@end

// 具体的网络请求类 - 使用WebSocket协议
@interface WebSocketRequest : NSObject <NetworkRequestProtocol>
@end

@implementation WebSocketRequest
- (void)sendRequestWithURL:(NSURL *)url {
    NSLog(@"Sending WebSocket request to URL: %@", url);
    // 发送WebSocket请求的具体实现
}
@end

// 使用网络请求的客户端类
@interface NetworkClient : NSObject
@property (nonatomic, strong) id<NetworkRequestProtocol> request;
- (void)makeRequestWithURL:(NSURL *)url;
@end

@implementation NetworkClient
- (void)makeRequestWithURL:(NSURL *)url {
    [self.request sendRequestWithURL:url];
}
@end

客户端代码:

NSURL *url = [NSURL URLWithString:@"https://example.com"];
NetworkClient *client = [[NetworkClient alloc] init];
// 使用HTTP协议发送请求
client.request = [[HTTPRequest alloc] init];
[client makeRequestWithURL:url];

// 使用WebSocket协议发送请求
client.request = [[WebSocketRequest alloc] init];
[client makeRequestWithURL:url];

在上述代码中,抽象的网络请求接口 NetworkRequestProtocol 定义了发送请求的方法 sendRequestWithURL:。具体的网络请求类 HTTPRequestWebSocketRequest 分别实现了这个接口,每个类负责不同的网络协议的具体实现。

客户端类 NetworkClient 持有一个 NetworkRequestProtocol 的引用,并通过该引用进行网络请求。通过在客户端中切换具体的请求类,例如使用 HTTPRequestWebSocketRequest,可以在运行时切换不同的网络协议的实现。

视图控制器和视图之间的桥接

// 抽象的视图样式接口
@protocol ViewStyleProtocol <NSObject>
- (UIColor *)backgroundColor;
- (UIColor *)textColor;
@end

// 具体的浅色主题样式
@interface LightThemeStyle : NSObject <ViewStyleProtocol>
@end

@implementation LightThemeStyle
- (UIColor *)backgroundColor {
    return [UIColor whiteColor];
}

- (UIColor *)textColor {
    return [UIColor blackColor];
}
@end

// 具体的深色主题样式
@interface DarkThemeStyle : NSObject <ViewStyleProtocol>
@end

@implementation DarkThemeStyle
- (UIColor *)backgroundColor {
    return [UIColor blackColor];
}

- (UIColor *)textColor {
    return [UIColor whiteColor];
}
@end

// 视图控制器类
@interface ViewController : UIViewController
@property (nonatomic, strong) id<ViewStyleProtocol> viewStyle;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建并配置不同的视图样式
    self.viewStyle = [[LightThemeStyle alloc] init];
    
    // 设置视图背景颜色
    self.view.backgroundColor = [self.viewStyle backgroundColor];
    
    // 创建并配置视图控件,使用视图样式的文本颜色
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 40)];
    label.textColor = [self.viewStyle textColor];
    label.text = @"Hello, Bridge Pattern!";
    [self.view addSubview:label];
}

@end

客户端代码:

ViewStyleViewController *viewController = [[ViewStyleViewController alloc] init]; 

// 创建并配置不同的视图样式
viewController.viewStyle = [[DarkThemeStyle alloc] init];

// 设置视图背景颜色

viewController.view.backgroundColor = [viewController.viewStyle backgroundColor];

// 创建并配置视图控件,使用视图样式的文本颜色

UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 40)];

label.textColor = [viewController.viewStyle textColor];

label.text = @"Hello, Bridge Pattern!";

[viewController.view addSubview:label];

在上述代码中,抽象的视图样式接口 ViewStyleProtocol 定义了获取背景颜色和文本颜色的方法。具体的浅色主题样式 LightThemeStyle 和深色主题样式 DarkThemeStyle 分别实现了这个接口,提供了不同主题的样式。

ViewController持有一个ViewStyleProtocol的引用viewStyle ,通过该引用可以获取视图样式的背景颜色和文本颜色。在 viewDidLoad 方法中,根据当前的视图样式设置视图的背景颜色,并创建并配置相应的视图控件。

在客户端代码中,我们创建一个 ViewController 对象,并通过配置 viewStyle 属性和设置视图的背景颜色来使用不同的视图样式。

通过桥接模式,我们可以在运行时选择不同的视图样式,而不需要修改视图控制器的其他部分。这种设计使得图控制器与视图样式之间解耦,提供了更好的可扩展性和灵活性。

3 组合模式(Composite Pattern)

image.png

组合模式可以将对象组合成树形结构,以表示"整体-部分"的层次结构。组合模式使得客户端能够以统一的方式处理单个对象和组合对象,从而使得代码更加简洁和可扩展。

在组合模式中,有两种基本类型的对象:叶节点(Leaf)和组合节点(Composite)。叶节点表示树结构中的最小单位,它们没有子节点。而组合节点则包含叶节点和其他组合节点,形成了树结构的层次关系。

例子

下面这个例子中将会演示如何使用组合模式来表示一个文件系统的树形结构。

首先,定义一个抽象基类 FileSystemComponent,它包含了共同的行为和属性,包括名称、添加子节点、删除子节点、获取子节点等:

// FileSystemComponent.h

@interface FileSystemComponent : NSObject

@property (nonatomic, strong) NSString *name;

- (instancetype)initWithName:(NSString *)name;
- (void)addComponent:(FileSystemComponent *)component;
- (void)removeComponent:(FileSystemComponent *)component;
- (NSArray<FileSystemComponent *> *)getChildren;

@end

// FileSystemComponent.m

@implementation FileSystemComponent

- (instancetype)initWithName:(NSString *)name {
    self = [super init];
    if (self) {
        _name = name;
    }
    return self;
}

- (void)addComponent:(FileSystemComponent *)component {
    // 默认实现为空,只有组合节点需要实现该方法
}

- (void)removeComponent:(FileSystemComponent *)component {
    // 默认实现为空,只有组合节点需要实现该方法
}

- (NSArray<FileSystemComponent *> *)getChildren {
    // 默认实现为空,只有组合节点需要实现该方法
    return nil;
}

@end

然后,定义叶节点 File 类,表示文件:

// File.h

@interface File : FileSystemComponent

@end

// File.m

@implementation File

@end

接下来,定义组合节点 Directory 类,表示文件夹:

// Directory.h

@interface Directory : FileSystemComponent

@end

// Directory.m

@implementation Directory {
    NSMutableArray<FileSystemComponent *> *_children;
}

- (instancetype)initWithName:(NSString *)name {
    self = [super initWithName:name];
    if (self) {
        _children = [NSMutableArray array];
    }
    return self;
}

- (void)addComponent:(FileSystemComponent *)component {
    [_children addObject:component];
}

- (void)removeComponent:(FileSystemComponent *)component {
    [_children removeObject:component];
}

- (NSArray<FileSystemComponent *> *)getChildren {
    return [_children copy];
}

@end

现在,我们可以使用组合模式来构建一个文件系统的树形结构。例如:

Directory *root = [[Directory alloc] initWithName:@"root"];
Directory *documents = [[Directory alloc] initWithName:@"Documents"];
File *readme = [[File alloc] initWithName:@"readme.txt"];
[documents addComponent:readme];
[root addComponent:documents];
Directory *pictures = [[Directory alloc] initWithName:@"Pictures"];
File *photo1 = [[File alloc] initWithName:@"photo1.jpg"];
File *photo2 = [[File alloc] initWithName:@"photo2.jpg"];
[pictures addComponent:photo1];
[pictures addComponent:photo2];
[root addComponent:pictures];
[root addComponent:[[File alloc] initWithName:@"notes.txt"]];

在上面的代码中,我们创建了一个文件系统的树形结构。root 是根目录,包含了 Documents 文件夹和 Pictures 文件夹,以及一个名为 notes.txt 的文件。Documents 文件夹中包含一个名为 readme.txt 的文件,而 Pictures 文件夹中包含两个图片文件 photo1.jpgphoto2.jpg

通过组合模式,我们可以以统一的方式处理文件和文件夹,无论它们是叶节点还是组合节点。例如,我们可以通过递归遍历整个文件系统来打印出所有的文件和文件夹:

- (void)printFileSystem:(FileSystemComponent *)component  {
    NSLog(@"%@", component.name);
    if ([component isKindOfClass:[Directory class]]) {
        Directory *directory = (Directory *)component;
        NSArray<FileSystemComponent *> *children = [directory getChildren];
        for (FileSystemComponent *child in children) {
            [self printFileSystem:child];
        }
    }
}

调用:

[self printFileSystem:root];

通过上述代码,我们可以遍历整个文件系统,并打印出文件和文件夹的结构。

组合模式的优点是,它简化了对整体和部分之间的处理。客户端可以一致地对待单个对象和组合对象,而不需要关心它们的具体类型。这种一致性使得代码更加灵活和可扩展,可以轻松地添加、删除和修改组合节点,而无需修改客户端代码

在iOS开发中,组合模式可以在多种场景中使用。下面是一些常见的应用场景:

  1. 视图层次结构:在iOS中,视图层次结构是一个典型的树形结构。UIView类可以看作是组合模式中的组合节点,它可以包含其他UIView实例作为子视图(叶节点)。通过使用组合模式,可以以统一的方式处理视图层次结构中的单个视图和组合视图。
  2. 文件系统操作:在处理文件系统相关操作时,组合模式也非常有用。例如,你可以使用组合模式来构建一个文件管理器应用程序,以便处理文件和文件夹的创建、删除、复制等操作。组合模式使得你可以一致地对待文件和文件夹,无论是单个文件还是包含其他文件和文件夹的文件夹。
  3. 数据结构处理:在处理具有层次结构的数据时,组合模式可以派上用场。例如,你可能需要处理一个包含多级分类的数据集合,每个分类又可以包含子分类。通过使用组合模式,可以轻松地表示和操作这种层次结构的数据。

也即:UIView中常用的addSubview方法其实就是组合模式。

通过这个方法,我们可以将一个 UIView 对象作为子视图添加到另一个 UIView 对象中,形成视图层次结构。

在组合模式中,UIView 对象可以看作是组合模式中的组合节点,它可以包含其他 UIView 对象作为子视图(叶节点)。通过使用 addSubview: 方法,我们可以将叶节点视图添加到组合节点视图中,构建复杂的视图层次结构。

组合模式具有以下优点:

  1. 统一接口:组合模式提供了一个统一的接口,使得客户端可以以相同的方式处理单个对象和组合对象。这简化了客户端代码,并提高了代码的可读性和可维护性
  2. 简化客户端代码:使用组合模式,客户端不需要关心处理单个对象还是组合对象,可以通过统一的接口直接操作整个对象层次结构。这样可以简化客户端代码,减少条件判断和类型检查的逻辑。
  3. 可扩展性:组合模式支持动态的添加、删除和修改对象,使得对象层次结构可以很容易地进行扩展和变化。这种灵活性使得我们可以构建复杂的对象结构,并在不影响现有代码的情况下进行修改和调整。
  4. 递归处理:组合模式使用递归的方式处理整个对象层次结构,可以方便地对整个层次结构进行遍历和操作。

组合模式也存在一些缺点:

  1. 限制类型的组合:在组合模式中,叶节点和组合节点具有相同的接口,但是并不意味着所有操作对于叶节点和组合节点都是有意义的。有些操作可能只适用于叶节点,而有些操作可能只适用于组合节点。
  2. 对象层次结构的复杂性:随着对象层次结构的增长,特别是在处理大型和复杂的层次结构时,组合模式可能会导致对象层次结构变得复杂和难以理解。需要合理地划分对象的职责和功能,以避免层次结构的过度复杂化。
  3. 可能带来性能损失:由于组合模式涉及递归操作和遍历整个对象层次结构,可能会在一些情况下带来一定的性能损失。特别是在处理大型层次结构时,需要仔细考虑性能问题,并根据实际情况进行优化。

总的来说,组合模式在处理对象层次结构时提供了一种简洁和灵活的方式,但在使用时需要注意合理划分对象的职责和功能,以及在性能方面的考虑。根据具体的应用场景和需求,权衡利弊后选择是否 使用组合模式。

4 装饰器模式(Decorator Pattern)

装饰器模式最大的特点就是在不改变现有对象结构的情况下,动态地向对象添加新的行为或功能

装饰器模式通过将对象包装在一个装饰器对象中,来扩展对象的功能。

在iOS开发中,装饰器模式常常用于以下情况:

  1. 动态扩展对象的功能:当我们需要在不修改现有代码的情况下,为一个对象添加额外的功能时,可以使用装饰器模式。这样可以避免修改原有代码,同时实现功能的灵活组合。
  2. 对象功能的组合:当一个对象可能有多个功能组合的情况下,装饰器模式可以让我们通过不同的装饰器进行功能的组合。

装饰器模式也可以与其他设计模式结合使用,例如结合工厂模式来创建装饰器对象。

例子

我们就来举例子展现装饰器模式和工厂模式结合使用可以实现更灵活的对象创建和装饰过程:

首先,我们定义一个抽象的形状接口 Shape

@protocol Shape <NSObject>
- (void)draw;
@end

然后,我们创建具体的形状类,如矩形和圆形:

@interface Rectangle : NSObject <Shape>
@end

@implementation Rectangle

- (void)draw {
    NSLog(@"绘制矩形");
}

@end

@interface Circle : NSObject <Shape>
@end

@implementation Circle

- (void)draw {
    NSLog(@"绘制圆形");
}

@end

接下来,我们创建具体的装饰器类 ColorDecoratorBorderDecorator,它们实现了 Shape 接口,并在 draw 方法中添加了颜色和边框的绘制:

@interface ColorDecorator : NSObject <Shape>
@property (nonatomic, strong) id<Shape> shape;
- (instancetype)initWithShape:(id<Shape>)shape;
@end

@implementation ColorDecorator

- (instancetype)initWithShape:(id<Shape>)shape {
    self = [super init];
    if (self) {
        self.shape = shape;
    }
    return self;
}

- (void)draw {
    [self.shape draw];
    NSLog(@"添加颜色");
}

@end

@interface BorderDecorator : NSObject <Shape>
@property (nonatomic, strong) id<Shape> shape;
- (instancetype)initWithShape:(id<Shape>)shape;
@end

@implementation BorderDecorator

- (instancetype)initWithShape:(id<Shape>)shape {
    self = [super init];
    if (self) {
        self.shape = shape;
    }
    return self;
}

- (void)draw {
    [self.shape draw];
    NSLog(@"添加边框");
}

@end

接下来,我们定义一个装饰器工厂类 ShapeDecoratorFactory,用于创建装饰器对象:

@interface ShapeDecoratorFactory : NSObject
+ (id<Shape>)decoratedShapeWithType:(NSString *)type;
@end

@implementation ShapeDecoratorFactory

+ (id<Shape>)decoratedShapeWithType:(NSString *)type {
    id<Shape> shape;
    
    if ([type isEqualToString:@"rectangle"]) {
        shape = [[Rectangle alloc] init];
    } else if ([type isEqualToString:@"circle"]) {
        shape = [[Circle alloc] init];
    }
    
    // 创建装饰器对象,并返回装饰后的形状
    shape = [[ColorDecorator alloc] initWithShape:shape];
    shape = [[BorderDecorator alloc] initWithShape:shape];
    
    return shape;
}

@end

客户端代码:

id<Shape> rectangle = [ShapeDecoratorFactory decoratedShapeWithType:@"rectangle"];
[rectangle draw];

id<Shape> circle = [ShapeDecoratorFactory decoratedShapeWithType:@"circle"];
[circle draw];

接下来,我们可以使用装饰器工厂类 ShapeDecoratorFactory 来创建装饰后的形状对象。以下是一个示例的使用代码:

objectiveCopy code
id<Shape> rectangle = [ShapeDecoratorFactory decoratedShapeWithType:@"rectangle"];
[rectangle draw];

id<Shape> circle = [ShapeDecoratorFactory decoratedShapeWithType:@"circle"];
[circle draw];

输出结果将会是:

绘制矩形
添加颜色
添加边框

绘制圆形
添加颜色
添加边框

装饰器模式具有以下优点:

  1. 动态扩展功能:装饰器模式允许在不修改现有代码的情况下,动态地为对象添加额外的功能。通过装饰器模式,可以在运行时对对象进行灵活的功能组合,实现功能的动态扩展。
  2. 遵循开放封闭原则:装饰器模式可以遵循开放封闭原则,即对扩展开放,对修改封闭。通过装饰器模式,可以通过添加装饰器来扩展对象的功能,而无需修改原有对象的代码。
  3. 避免继承的复杂性:装饰器模式通过组合和委托的方式,避免了使用大量的子类来实现功能的扩展。相比于继承,装饰器模式更加灵活,并且可以在运行时动态地组合功能。
  4. 单一职责原则:装饰器模式可以将功能的细粒度分离,每个装饰器类只关注特定的功能扩展,使得每个类都具有单一职责。

然而,装饰器模式也存在一些缺点:

  1. 增加复杂性:使用装饰器模式会增加额外的类和对象,导致代码结构变得更加复杂,理解和维护成本增加。
  2. 装饰器顺序依赖:如果装饰器的顺序不正确,可能会影响功能的正确执行。特别是当多个装饰器共同修改同一个方法时,装饰器的顺序非常重要。
  3. 不适合大量装饰器的情况:如果需要使用大量的装饰器来装饰对象,会导致装饰器的层级嵌套过深,代码变得复杂且难以维护。

因此,如果需要动态扩展对象的功能且保持代码的灵活性,装饰器模式是一种有效的设计模式选择。

5 外观模式(Facade Pattern)

image.png

外观模式提供了一个简单而统一的接口,用于访问复杂系统中的一组接口。

外观模式隐藏了系统的复杂性,并为客户端提供了一个更简单和更直接的访问方式。它可以将一个复杂的子系统封装起来,客户端只需要通过与外观对象进行交互,而无需直接与子系统中的各个对象进行交互

例子

在iOS开发中,外观模式常用于简化复杂的系统或框架,并提供一个更简单的接口供客户端使用。

也就是说,在使用第三方库的应用中,对第三方库进行封装,便于客户端的使用,就是外观模式的一种体现。

这里以封装流行的AFNetworking库为例子,展现外观模式的作用,除了外观模式,这个例子还使用了单例模式,简单工厂模式。

@interface NetworkManager : NSObject

/// 创建单例
+ (instancetype _Nonnull)shareManager;

/// 请求类型
typedef NS_ENUM(NSUInteger, NetworkManagerRequestType) {
    NetworkManagerRequestTypeGet,
    NetworkManagerRequestTypePost,
    NetworkManagerRequestTypePut,
    NetworkManagerRequestTypeDelete
};

/// 统一请求方法 responseSerializer默认都为AFJSONResponseSerializer
- (void)requestURL:(NSString * _Nonnull)url
              type:(NetworkManagerRequestType)requestType
        parameters:(id _Nonnull)parameters
          progress:(void (^_Nullable)(NSProgress * _Nullable progress))progress
           success:(void (^)(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject))success
           failure:(void (^)(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error))failure;

@end

#import <AFNetworking/AFNetworking.h>

@interface NetworkManager ()

/// AFHTTPSessionManager
@property (nonatomic, strong) AFHTTPSessionManager *sessionManager;

@end

@implementation NetworkManager

/// 创建单例
+ (instancetype _Nonnull)shareManager {
    static dispatch_once_t onceToken;
    static NetworkManager *_shareManager = nil;
    dispatch_once(&onceToken, ^{
        _shareManager = [[self alloc] init];
    });
    return _shareManager;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        self.sessionManager = [AFHTTPSessionManager manager];
        self.sessionManager.requestSerializer = [AFJSONRequestSerializer serializer];
        // 默认上传JSON 格式
        self.sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
    }
    return self;
}

/// 统一请求方法 responseSerializer默认都为AFJSONResponseSerializer
- (void)requestURL:(NSString * _Nonnull)url
              type:(NetworkManagerRequestType)requestType
        parameters:(id _Nonnull)parameters
          progress:(void (^_Nullable)(NSProgress * _Nullable progress))progress
           success:(void (^)(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject))success
           failure:(void (^)(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error))failure {
    switch (requestType) {
        case NetworkManagerRequestTypeGet:
            [self GETRequestURL:url
                     parameters:parameters
                       progress:progress
                        success:success
                        failure:failure];
            break;
        case NetworkManagerRequestTypePost:
            [self POSTRequestURL:url
                     parameters:parameters
                       progress:progress
                        success:success
                        failure:failure];
            break;
        case NetworkManagerRequestTypePut:
            [self PUTRequestURL:url
                     parameters:parameters
                       progress:progress
                        success:success
                        failure:failure];
            break;
        case NetworkManagerRequestTypeDelete:
            [self DELETERequestURL:url
                     parameters:parameters
                       progress:progress
                        success:success
                        failure:failure];
            break;
        default:
            break;
    }
}

/// GET 请求
- (void)GETRequestURL:(NSString * _Nonnull)url
           parameters:(id _Nonnull)parameters
             progress:(void (^_Nullable)(NSProgress * _Nullable progress))progress
              success:(void (^)(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject))success
              failure:(void (^)(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error))failure {
    [self.sessionManager
     GET:url
     parameters:parameters
     progress:progress
     success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        if (success) {
            success(task, responseObject);
        }
    }
     failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        if (failure) {
            failure(task, error);
        }
    }];
}

@end

网络请求工厂:

@interface NetworkRequestFactory : NSObject

+ (NetworkManager *)creatNetworkManager;

@end

@implementation NetworkRequestFactory

+ (NetworkManager *)creatNetworkManager {
    return [NetworkManager shareManager];
}

现在,我们可以通过工厂类NetworkManagerFactory来创建NetworkManager对象,并使用外观模式提供的统一接口来进行网络请求。以下是使用示例:

NetworkManager *networkManager = [NetworkRequestFactory creatNetworkManager];
[networkManager requestURL:@"http://example.com/api"
                      type:NetworkManagerRequestTypeGet
                parameters:nil
                  progress:nil
                   success:^(NSURLSessionDataTask *task, id responseObject) {
                       // 请求成功处理
                   }
                   failure:^(NSURLSessionDataTask *task, NSError *error) {
                       // 请求失败处理
                   }];

外观模式有以下优点:

  1. 简化接口:外观模式提供了一个简化的接口,隐藏了底层系统的复杂性,使得客户端更容易使用和理解系统功能。
  2. 解耦合:外观模式将客户端与底层系统解耦,客户端只需要通过外观类与系统进行交互,而不需要直接与系统的各个子组件进行交互。
  3. 提高灵活性:通过外观类作为中间层,可以灵活地修改底层系统的实现细节,而不影响客户端的代码。
  4. 提高可维护性:外观模式将底层系统的复杂性封装在一个类中,使得系统结构更加清晰,易于理解和维护。

外观模式也有一些缺点:

  1. 不符合开闭原则当底层系统发生变化时,可能需要修改外观类的代码,这违反了对修改关闭的原则。
  2. 可能引入性能问题:外观模式的封装会增加一层额外的间接调用,可能会对系统的性能产生一定影响。
  3. 可能导致过度设计:过度使用外观模式可能导致系统结构过于复杂,增加不必要的代码复杂性和维护成本。

6 享元模式(Flyweight Pattern)

image.png

享元模式旨在通过共享对象减少内存使用和提高性能。它适用于存在大量相似对象的情况,通过共享这些对象的公共部分来减少内存消耗

在享元模式中,对象分为两种类型:内部状态(Intrinsic State)和外部状态(Extrinsic State)。内部状态是可以被共享的,它包含对象的固有属性,不会因外部环境的改变而改变。外部状态是不可共享的,它取决于对象被使用的上下文。

在iOS开发中,享元模式可以应用于以下场景:

  1. 视图复用:在使用UITableViewUICollectionView等可重用视图的情况下,可以使用享元模式来共享可重用视图的实例。这样可以避免频繁地创建和销毁视图对象,提高滚动性能和响应速度。
  2. 图像缓存:在需要频繁加载和显示大量图像的应用中,可以使用享元模式来共享已加载的图像实例。这样可以避免重复加载相同的图像,减少内存消耗,并提高性能。
  3. 字符串常量池:在应用中使用的字符串常量(例如错误提示、网络请求URL等)可以被视为享元对象。通过共享这些字符串常量的实例,可以节省内存并提高字符串比较的效率。

总之,享元模式在iOS开发中可以用于任何需要共享大量相似对象降低内存消耗的场景。

例子

视图复用

首先是我们熟知的UITableViewUICollectionView

UITableView会根据需要复用Cell对象来显示不同的数据行,以节省内存和提高性能。

UITableView的实现中,每个可见的Cell都是一个UITableViewCell对象。当用户滚动UITableView时,旧的Cell会被回收并用于展示新的数据行。这种复用机制正是享元模式的一种应用。

下面是一个简单的示例,展示了如何在UITableView中使用享元模式:

@interface CustomTableViewCell : UITableViewCell

@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIImageView *iconImageView;

@end

@implementation CustomTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        // 初始化Cell的子视图,例如titleLabel和iconImageView
    }
    return self;
}

@end

在上述代码中,CustomTableViewCell是自定义的UITableViewCell子类,它包含了Cell的内部状态,例如titleLabeliconImageView。这些视图将在每个复用的Cell上显示不同的数据。

UITableView的数据源方法cellForRowAtIndexPath:中,我们可以使用享元模式来获取可复用的Cell对象并设置相应的数据:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *reuseIdentifier = @"CustomCell";
    
    CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
    if (cell == nil) {
        cell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
    }
    
    // 设置Cell的数据
    [self configureCell:cell atIndexPath:indexPath];
    
    return cell;
}

- (void)configureCell:(CustomTableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    // 根据indexPath设置Cell的数据,例如标题和图像
    cell.titleLabel.text = [self.dataArray[indexPath.row] title];
    cell.iconImageView.image = [UIImage imageNamed:[self.dataArray[indexPath.row] imageName]];
}

标识符:在享元模式中,内部状态用于标识对象并进行共享。对于相同的内部状态,应该返回同一个共享对象。

在上述代码中,我们使用dequeueReusableCellWithIdentifier:方法来获取可复用的Cell对象。如果没有可复用的Cell对象,则创建一个新的Cell对象并将其注册到重用队列中。

通过使用这种方式,UITableView会自动复用之前滚出屏幕的Cell对象,只需更新Cell上的数据,而不需要每次滚动都创建新的Cell对象。这样可以大大减少内存消耗,并提高UITableView的性能。

总结:UITableViewCell复用机制是享元模式的一种应用,它通过复用Cell对象来降低内存消耗和提高性能。通过重用相同类型的Cell,我们可以避免频繁创建和销毁Cell对象,从而优化UITableView的滚动性能和响应速度。

图像缓存

当需要频繁加载和显示大量图像的应用中,使用享元模式可以有效地进行图像缓存,避免重复加载相同的图像,减少内存消耗,并提高性能。

下面是一个简单的示例,展示了如何使用享元模式进行图像缓存:

@interface ImageCache : NSObject

@property (nonatomic, strong) NSCache *cache;

+ (instancetype)sharedCache;
- (UIImage *)getImageWithName:(NSString *)imageName;
- (void)setImage:(UIImage *)image withName:(NSString *)imageName;

@end

@implementation ImageCache

+ (instancetype)sharedCache {
    static ImageCache *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[ImageCache alloc] init];
        sharedInstance.cache = [[NSCache alloc] init];
    });
    return sharedInstance;
}

- (UIImage *)getImageWithName:(NSString *)imageName {
    return [self.cache objectForKey:imageName];
}

- (void)setImage:(UIImage *)image withName:(NSString *)imageName {
    [self.cache setObject:image forKey:imageName];
}

@end

在上述代码中,我们创建了一个ImageCache类,它作为图像的享元工厂和缓存。ImageCache类使用NSCache来存储和管理图像实例。

在应用中需要加载和显示图像时,可以使用ImageCache来获取图像实例:

ImageCache *imageCache = [ImageCache sharedCache];
UIImage *image = [imageCache getImageWithName:@"image1"];
if (image == nil) {
    // 如果缓存中没有图像,则加载图像并存储到缓存中
    image = [UIImage imageNamed:@"image1"];
    [imageCache setImage:image withName:@"image1"];
}

在上面的例子中,cache就是是缓存池.

在上述代码中,我们首先通过sharedCache方法获取单例的ImageCache实例。然后,我们使用getImageWithName:方法来获取指定名称的图像实例。如果缓存中没有该图像实例,则加载图像并将其存储到缓存中。

在后续的代码中,我们可以使用获取到的图像实例进行显示操作,而不需要重复加载相同的图像。通过使用图像缓存,我们避免了重复加载图像,减少了内存消耗,并提高了图像加载的性能和响应速度

享元模式具有以下优点:

  1. 减少内存消耗:通过共享对象实例,可以大大减少系统中相似对象的数量,从而降低内存消耗。共享的对象可以被多个客户端共同使用,避免了创建大量相似对象的开销。
  2. 提高性能:由于减少了对象的数量,享元模式可以提高系统的性能。重复使用共享对象可以避免频繁的对象创建和销毁操作,从而减少了系统的开销。
  3. 状态外部化:享元模式将对象的内部状态和外部状态分离。内部状态是可以共享的,而外部状态是变化的,可以根据需要传递给享元对象。这种状态的外部化可以简化对象的逻辑,使对象更易于使用和维护。
  4. 提高可维护性和可扩展性:通过将对象的状态外部化,享元模式使得对象的逻辑更加清晰和简洁。这样可以提高代码的可维护性和可扩展性,使系统更容易理解和修改。

然而,享元模式也有一些限制和缺点:

  1. 共享对象的状态必须是可共享的:享元模式要求对象的内部状态可以被共享,而外部状态可以在使用时传递。如果对象的状态不可共享,那么无法使用享元模式来实现对象的共享和复用。
  2. 增加了系统的复杂性:享元模式将对象分为内部状态和外部状态,并且需要额外的逻辑来管理共享对象的创建和访问。这样会增加系统的复杂性,需要仔细考虑对象的状态和共享机制的设计。
  3. 可能导致线程安全问题:如果多个线程同时访问和修改共享对象的状态,可能会导致线程安全问题。在使用享元模式时,需要注意对共享对象的状态进行正确的同步和管理,以避免并发访问问题

综上所述,享元模式适用于需要共享大量相似对象,并且可以提供内存和性能优化的场景。

7 代理模式(Proxy Pattern)

image.png

代理模式通过创建一个代理对象来控制对另一个对象的访问。

代理对象充当原始对象的中间人,客户端通过代理对象与原始对象进行交互,代理对象可以在不修改原始对象的情况下增加额外的功能

代理模式的主要目的是实现对对象的间接访问,以提供更灵活的控制和扩展。代理模式常见的应用场景包括:

  1. 远程代理:允许通过网络访问远程对象,隐藏了底层的复杂性
  2. 虚拟代理:用于懒加载对象,只有在需要时才会创建真实对象。
  3. 安全代理:控制对对象的访问权限
  4. 智能代理:在访问对象时执行额外的逻辑,如缓存、日志记录等。

看到这里,我们会自然想到另外两种相似的模式:适配器模式装饰器模式,他们都是尽量不使客户端和原实现层进行直接通信,通过额外的一层进行交流,在这一层中,可能还会增加别的功能,那么他们之间差别在哪里呢?

代理模式的主要目的是提供对对象的间接访问,并控制对原始对象的访问。代理对象充当了原始对象的代表,可以在不修改原始对象的情况下添加额外的功能或控制访问。

装饰器模式的主要目的是动态地为对象添加额外的功能,而不改变其接口。

适配器模式的主要目的是将一个接口转换成另一个接口,以使得原本不兼容的类能够一起工作。

可以看到,代理模式最大的不同是:控制访问。不像适配器模式一样,改变接口;也不像装饰器模式一样,重点在于增加功能,而且一个装饰器还能对多个原始现层代码,随时改变。

例子

假设我们有一个下载器对象(Downloader)负责从互联网上下载文件。现在我们希望在每次下载前后记录下载日志。我们可以使用代理模式来实现:

// Downloader.h
@protocol Downloader <NSObject>
- (void)downloadFile:(NSString *)url;
@end

// RealDownloader.h
#import "Downloader.h"

@interface RealDownloader : NSObject <Downloader>
@end

// RealDownloader.m
#import "RealDownloader.h"

@implementation RealDownloader

- (void)downloadFile:(NSString *)url {
    NSLog(@"RealDownloader: Downloading file from %@", url);
}

@end

// DownloaderProxy.h
#import "Downloader.h"

@interface DownloaderProxy : NSObject <Downloader>
@property (nonatomic, strong) RealDownloader *realDownloader;
@end

// DownloaderProxy.m
#import "DownloaderProxy.h"

@implementation DownloaderProxy

- (void)downloadFile:(NSString *)url {
    NSLog(@"DownloaderProxy: Preparing to download file from %@", url);
    
    if (self.realDownloader == nil) {
        self.realDownloader = [[RealDownloader alloc] init];
    }
    
    [self.realDownloader downloadFile:url];
    
    NSLog(@"DownloaderProxy: File download complete");
}

@end

客户端代码:

id<Downloader> downloader = [[DownloaderProxy alloc] init]; 
[downloader downloadFile:@"http://example.com/file.txt"];

在上述示例中,Downloader是一个协议,定义了下载文件的方法downloadFile:RealDownloader是实际执行下载操作的对象,它实现了Downloader协议。DownloaderProxy是代理对象,也实现了Downloader协议。

当客户端调用代理对象的downloadFile:方法时,代理对象在下载前后分别输出日志信息,并将实际的下载操作委托给RealDownloader对象执行。

代理模式有以下优点:

  1. 隔离客户端和真实对象:代理模式通过代理对象将客户端与真实对象隔离开来,客户端不直接与真实对象交互,而是通过代理对象进行间接访问。这样可以减少客户端与真实对象之间的耦合,提高系统的灵活性和可维护性。
  2. 控制对真实对象的访问:代理对象可以在访问真实对象之前或之后执行额外的操作,例如权限控制、缓存、延迟加载等。这样可以对真实对象的访问进行控制和管理,增加了系统的安全性和可控性
  3. 提供额外的功能:代理对象可以在不修改真实对象的情况下,扩展或增强其功能。例如,代理对象可以添加日志记录、性能统计、异常处理等功能,从而为客户端提供更多的服务。

代理模式也有一些缺点:

  1. 增加了系统复杂性:引入代理对象会增加系统中的类和对象数量,导致系统的复杂性增加。过多的代理对象可能会使代码结构变得复杂,难以理解和维护。
  2. 增加了请求的处理时间:由于代理模式引入了额外的对象,每个请求都需要经过代理对象的处理,可能会增加一定的处理时间。
  3. 可能降低直接访问的效率:由于代理模式是通过间接访问实现的,所以在直接访问真实对象时可能会降低一些效率。

项目Demo

24-Design-Patterns

下一篇

24种设计模式代码实例学习(四)行为型模式