[关闭]
@qidiandasheng 2021-01-11T13:59:21.000000Z 字数 11793 阅读 600

设计模式(二):创建型模式之单例、建造者、原型(😁)

架构


介绍

侧重于对象的创建。

在软件工程中,引自维基百科创建型模式是处理对象创建的设计模式,试图根据实际情况使用合适的方式创建对象。基本的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。
创建型模式由两个主导思想构成。一是将系统使用的具体类封装起来,二是隐藏这些具体类的实例创建和结合的方式。

单例模式

定义

单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,并提供一个访问它的全局访问点。

适用场景

系统只需要一个实例对象,客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。比较典型的例子是音乐播放器,日志系统类等等。

成员与类图

成员

单例模式只有一个成员,就是单例类。因为只有一个成员,所以该设计模式的类图比较简单:

模式类图

导出图片Fri Apr 10 2020 15_43_42 GMT+0800 (中国标准时间).png-144.8kB

一般来说单例类会给外部提供一个获取单例对象的方法,内部会用静态对象的方式保存这个对象。

代码示例

场景概述

在这里我们创建一个简单的打印日至或上报日至的日至管理单例。

场景分析

在创建单例时,除了要保证提供唯一实例对象以外,还需注意多线程的问题。下面用代码来看一下。

代码实现

创建单例类 LogManager

  1. //================== LogManager.h ==================
  2. @interface LogManager : NSObject
  3. +(instancetype)sharedInstance;
  4. - (void)printLog:(NSString *)logMessage;
  5. - (void)uploadLog:(NSString *)logMessage;
  6. @end
  7. //================== LogManager.m ==================
  8. @implementation LogManager
  9. static LogManager* _sharedInstance = nil;
  10. +(instancetype)sharedInstance
  11. {
  12. static dispatch_once_t onceToken ;
  13. dispatch_once(&onceToken, ^{
  14. _sharedInstance = [[super allocWithZone:NULL] init] ;
  15. }) ;
  16. return _sharedInstance ;
  17. }
  18. +(id)allocWithZone:(struct _NSZone *)zone
  19. {
  20. return [LogManager sharedInstance] ;
  21. }
  22. -(id)copyWithZone:(struct _NSZone *)zone
  23. {
  24. return [LogManager sharedInstance];
  25. }
  26. -(id)mutableCopyWithZone:(NSZone *)zone
  27. {
  28. return [LogManager sharedInstance];
  29. }
  30. - (void)printLog:(NSString *)logMessage{
  31. //print logMessage
  32. }
  33. - (void)uploadLog:(NSString *)logMessage{
  34. //upload logMessage
  35. }
  36. @end

从上面的代码中可以看到:

下面分别用这些接口来验证一下实例的唯一性:

  1. //================== Using by client ==================
  2. //alloc&init
  3. LogManager *manager0 = [[LogManager alloc] init];
  4. //sharedInstance
  5. LogManager *manager1 = [LogManager sharedInstance];
  6. //copy
  7. LogManager *manager2 = [manager0 copy];
  8. //mutableCopy
  9. LogManager *manager3 = [manager1 mutableCopy];
  10. NSLog(@"\nalloc&init: %p\nsharedInstance: %p\ncopy: %p\nmutableCopy: %p",manager0,manager1,manager2,manager3);

我们看一下打印出来的四个指针所指向对象的地址:

  1. alloc&init: 0x60000000f7e0
  2. sharedInstance: 0x60000000f7e0
  3. copy: 0x60000000f7e0
  4. mutableCopy: 0x60000000f7e0

可以看出打印出来的地址都相同,说明都是同一对象,证明了实现方法的正确性。

代码对应的类图

导出图片Fri Apr 10 2020 15_47_28 GMT+0800 (中国标准时间).png-152.5kB

优点

缺点

iOS SDK中的应用

生成器(建造者)模式

定义

生成器模式(Builder Pattern):也叫建造者模式,它将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

具体点说就是:有些对象的创建流程是一样的,但是因为自身特性的不同,所以在创建他们的时候需要将创建过程和特性的定制分离开来。

适用场景

解决构造函数过长问题

假设你的构造函数中有十个可选参数,那么调用该函数会非常不方便; 因此你需要重载这个构造函数, 新建几个只有较少参数的简化版。但这些构造函数仍需调用主构造函数,传递一些默认数值来替代省略掉的参数。

  1. @interface Phone : NSObject
  2. - (void)initWithCPU:(NSString *)cpu;
  3. - (void)initWithCPU:(NSString *)cpu Capacity:(NSString *)capacity;
  4. - (void)initWithCPU:(NSString *)cpu Capacity:(NSString *)capacity Display:(NSString *)display;
  5. - (void)initWithCPU:(NSString *)cpu Capacity:(NSString *)capacity Display:(NSString *)display Camera:(NSString *)camera;
  6. @end

生成器模式让你可以分步骤生成对象,而且允许你仅使用必须的步骤。应用该模式后,你再也不需要将几十个参数塞进构造函数里了。

解决参数使用属性set的问题

解决上面构造函数过长问题我们可以使用属性set的方式。那这里会存在几个问题:

创建不同形式的产品

如果你需要创建的各种形式的产品,它们的制造过程相似且仅有细节上的差异,此时可使用生成器模式(如手机CPU不同)。

基本生成器接口中定义了所有可能的制造步骤,具体生成器将实现这些步骤来制造特定形式的产品。同时主管类将负责管理制造步骤的顺序。

跟工厂模式不同的点:

工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。建造者模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象。

比如:顾客走进一家餐馆点餐,我们利用工厂模式,根据用户不同的选择,来制作不同的食物,比如披萨、汉堡、沙拉。对于披萨来说,用户又有各种配料可以定制,比如奶酪、西红柿、起司,我们通过建造者模式根据用户选择的不同配料来制作披萨。

成员与类图

成员

建造者模式包含4个成员:

  1. 抽象建造者(Builder):定义构造产品的几个公共方法。
  2. 具体建造者(ConcreteBuilder):根据不同的需求来实现抽象建造者定义的公共方法;每一个具体建造者都包含一个产品对象作为它的成员变量。
  3. 指挥者(Director):根据传入的具体建造者来返回其所对应的产品对象。(主管只负责按照特定顺序执行生成步骤。其在根据特定步骤或配置来生成产品时会很有帮助。由于客户端可以直接控制生成器,所以严格意义上来说,主管类并不是必需的)
  4. 产品角色(Product):创建的产品。

模式类图

导出图片Fri Apr 10 2020 15_51_38 GMT+0800 (中国标准时间).png-218.8kB

代码示例

场景概述

模拟一个制造手机的场景:手机的组装需要几个固定的零件:CPU,RAM,屏幕,摄像头,而且需要CPU -> RAM -> 屏幕 -> 摄像头的顺序来制造。

代码实现

Builder.h:

  1. @interface Builder : NSObject
  2. @property (readonly, nonatomic, copy) NSString * cpu;
  3. @property (readonly, nonatomic, copy) NSString * capacity;
  4. @property (readonly, nonatomic, copy) NSString * display;
  5. @property (readonly, nonatomic, copy) NSString * camera;
  6. //采用链式编程的方式来创建
  7. @property (readonly, nonatomic, copy) Builder * (^buildCpu)(NSString *cpu);
  8. @property (readonly, nonatomic, copy) Builder * (^buildCapacity)(NSString *capacity);
  9. @property (readonly, nonatomic, copy) Builder * (^buildDisplay)(NSString *display);
  10. @property (readonly, nonatomic, copy) Builder * (^buildCamera)(NSString *camera);
  11. @property (readonly, nonatomic, copy) Phone * (^build)(void);
  12. @end

Builder.m:

  1. @interface Builder()
  2. @property (nonatomic, copy) NSString * cpu;
  3. @property (nonatomic, copy) NSString * capacity;
  4. @property (nonatomic, copy) NSString * display;
  5. @property (nonatomic, copy) NSString * camera;
  6. @end
  7. @implementation Builder
  8. - (Builder * (^)(NSString * _Nonnull))buildCpu{
  9. return ^(NSString *cpu){
  10. self.cpu = cpu;
  11. return self;
  12. };
  13. }
  14. - (Builder * (^)(NSString * _Nonnull))buildCapacity{
  15. return ^(NSString *capacity){
  16. self.capacity = capacity;
  17. return self;
  18. };
  19. }
  20. - (Builder * (^)(NSString * _Nonnull))buildDisplay{
  21. return ^(NSString *display){
  22. self.display = display;
  23. return self;
  24. };
  25. }
  26. - (Builder * (^)(NSString * _Nonnull))buildCamera{
  27. return ^(NSString *camera){
  28. self.camera = camera;
  29. return self;
  30. };
  31. }
  32. - (Phone * (^)(void))build{
  33. return ^(void){
  34. Phone *phone = [[Phone alloc] initWithBuild:self];
  35. return phone;
  36. };
  37. }
  38. @end

Phone.h:

  1. @interface Phone : NSObject
  2. - (instancetype)initWithBuild:(Builder *)builder;
  3. @end

Phone.m:

  1. @interface Phone()
  2. @property (nonatomic, copy) NSString * cpu;
  3. @property (nonatomic, copy) NSString * capacity;
  4. @property (nonatomic, copy) NSString * display;
  5. @property (nonatomic, copy) NSString * camera;
  6. @end
  7. @implementation Phone
  8. - (instancetype)initWithBuild:(Builder *)builder{
  9. self = [super init];
  10. if (self) {
  11. _cpu = builder.cpu;
  12. _capacity = builder.capacity;
  13. _display = builder.display;
  14. _camera = builder.camera;
  15. }
  16. return self;
  17. }

使用:

  1. //这里使用方可以是指挥者(Director),也可以直接是业务方
  2. Phone *iphoneXR = [Builder new]
  3. .buildCpu(@"A12")
  4. .buildCapacity(@"256")
  5. .buildDisplay(@"6.1")
  6. .buildCamera(@"12MP")
  7. .build();
  8. Phone *miPhone = [Builder new]
  9. .buildCpu(@"Snapdragon 845")
  10. .buildCapacity(@"128")
  11. .buildDisplay(@"6.21")
  12. .buildCamera(@"12MP")
  13. .build();

继承Builder生成具体的Builder:

  1. @interface IphoneXRBuilder : Builder
  2. @end
  3. @implementation IphoneXRBuilder
  4. - (instancetype)init{
  5. self = [super init];
  6. if (self) {
  7. self.buildCpu(@"A12")
  8. .buildCapacity(@"256")
  9. .buildDisplay(@"6.1")
  10. .buildCamera(@"12MP");
  11. }
  12. return self;
  13. }
  14. @end
  15. //使用
  16. Phone *iphoneXR = [[IphoneXRBuilder alloc] init].build();

优点

缺点

原型模式

定义

原型模式(Prototype Pattern): 使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。

适用场景

成员与类图

成员

原型模式主要包含如下两个角色:

  1. 抽象原型类(Prototype):抽象原型类声明克隆自身的接口。
  2. 具体原型类(ConcretePrototype):具体原型类实现克隆的具体操作(克隆数据,状态等)。

模式类图

导出图片Fri Apr 10 2020 16_04_30 GMT+0800 (中国标准时间).png-152.1kB

需要注意的是,这里面的clone()方法返回的是被复制出来的实例对象。

代码示例

场景概述

模拟一份校招的简历,简历里面有人名,性别,年龄以及学历相关的信息。这里面学历相关的信息又包含学校名称,专业,开始和截止年限的信息。

场景分析

这里的学历相关信息可以使用单独一个对象来做,因此整体的简历对象的结构可以是:

简历对象:

而且因为对于同一学校同一届的同一专业的毕业生来说,学历对象中的信息是相同的,这时候如果需要大量生成这些毕业生的简历的话比较适合使用原型模式。

代码实现

首先定义学历对象:

  1. //================== UniversityInfo.h ==================
  2. @interface UniversityInfo : NSObject<NSCopying>
  3. @property (nonatomic, copy) NSString *universityName;
  4. @property (nonatomic, copy) NSString *startYear;
  5. @property (nonatomic, copy) NSString *endYear;
  6. @property (nonatomic, copy) NSString *major;
  7. - (id)copyWithZone:(NSZone *)zone;
  8. @end
  9. //================== UniversityInfo.m ==================
  10. @implementation UniversityInfo
  11. - (id)copyWithZone:(NSZone *)zone
  12. {
  13. UniversityInfo *infoCopy = [[[self class] allocWithZone:zone] init];
  14. [infoCopy setUniversityName:[_universityName mutableCopy]];
  15. [infoCopy setStartYear:[_startYear mutableCopy]];
  16. [infoCopy setEndYear:[_endYear mutableCopy]];
  17. [infoCopy setMajor:[_major mutableCopy]];
  18. return infoCopy;
  19. }
  20. @end

因为学历对象是支持复制的,因此需要遵从协议并实现copyWithZone:方法。而且支持的是深复制,所以在复制NSString的过程中需要使用mutableCopy来实现。

接着我们看一下简历对象:

  1. //================== Resume.h ==================
  2. #import "UniversityInfo.h"
  3. @interface Resume : NSObject<NSCopying>
  4. @property (nonatomic, copy) NSString *name;
  5. @property (nonatomic, copy) NSString *gender;
  6. @property (nonatomic, copy) NSString *age;
  7. @property (nonatomic, strong) UniversityInfo *universityInfo;
  8. @end
  9. //================== Resume.m ==================
  10. @implementation Resume
  11. - (id)copyWithZone:(NSZone *)zone
  12. {
  13. Resume *resumeCopy = [[[self class] allocWithZone:zone] init];
  14. [resumeCopy setName:[_name mutableCopy]];
  15. [resumeCopy setGender:[_gender mutableCopy]];
  16. [resumeCopy setAge:[_age mutableCopy]];
  17. [resumeCopy setUniversityInfo:[_universityInfo copy]];
  18. return resumeCopy;
  19. }
  20. @end

同样地,简历对象也需要遵从协议并实现copyWithZone:方法。

最后我们看一下复制的效果有没有达到我们的预期(被复制对象和复制对象的地址和它们所有的属性对象的地址都不相同)

  1. //================== Using by client ==================
  2. //resume for LiLei
  3. Resume *resume = [[Resume alloc] init];
  4. resume.name = @"LiLei";
  5. resume.gender = @"male";
  6. resume.age = @"24";
  7. UniversityInfo *info = [[UniversityInfo alloc] init];
  8. info.universityName = @"X";
  9. info.startYear = @"2014";
  10. info.endYear = @"2018";
  11. info.major = @"CS";
  12. resume.universityInfo = info;
  13. //resume_copy for HanMeiMei
  14. Resume *resume_copy = [resume copy];
  15. NSLog(@"\n\n\n======== original resume ======== %@\n\n\n======== copy resume ======== %@",resume,resume_copy);
  16. resume_copy.name = @"HanMeiMei";
  17. resume_copy.gender = @"female";
  18. resume_copy.universityInfo.major = @"TeleCommunication";
  19. NSLog(@"\n\n\n======== original resume ======== %@\n\n\n======== revised copy resume ======== %@",resume,resume_copy);

上面的代码模拟了这样一个场景:李雷同学写了一份自己的简历,然后韩梅梅复制了一份并修改了姓名,性别和专业这三个和李雷不同的信息。

这里我们重写了Resumedescription方法来看一下所有属性的值及其内存地址。最后来看一下resume对象和resume_copy对象打印的结果:

  1. //================== Output log ==================
  2. ======== original resume ========
  3. resume object address:0x604000247d10
  4. name:LiLei | 0x10bc0c0b0
  5. gender:male | 0x10bc0c0d0
  6. age:24 | 0x10bc0c0f0
  7. university name:X| 0x10bc0c110
  8. university start year:2014 | 0x10bc0c130
  9. university end year:2018 | 0x10bc0c150
  10. university major:CS | 0x10bc0c170
  11. ======== copy resume ========
  12. resume object address:0x604000247da0
  13. name:LiLei | 0xa000069654c694c5
  14. gender:male | 0xa000000656c616d4
  15. age:24 | 0xa000000000034322
  16. university name:X| 0xa000000000000581
  17. university start year:2014 | 0xa000000343130324
  18. university end year:2018 | 0xa000000383130324
  19. university major:CS | 0xa000000000053432
  20. ======== original resume ========
  21. resume object address:0x604000247d10
  22. name:LiLei | 0x10bc0c0b0
  23. gender:male | 0x10bc0c0d0
  24. age:24 | 0x10bc0c0f0
  25. university name:X| 0x10bc0c110
  26. university start year:2014 | 0x10bc0c130
  27. university end year:2018 | 0x10bc0c150
  28. university major:CS | 0x10bc0c170
  29. ======== revised copy resume ========
  30. resume object address:0x604000247da0
  31. name:HanMeiMei | 0x10bc0c1b0
  32. gender:female | 0x10bc0c1d0
  33. age:24 | 0xa000000000034322
  34. university name:X| 0xa000000000000581
  35. university start year:2014 | 0xa000000343130324
  36. university end year:2018 | 0xa000000383130324
  37. university major:TeleCommunication | 0x10bc0c1f0
  • 上面两个是原resume和刚被复制后的 copy resume的信息,可以看出来无论是这两个对象的地址还是它们的值对应的地址都是不同的,说明成功地实现了深复制。
  • 下面两个是原resume和被修改后的 copy_resume的信息,可以看出来新的copy_resume的值发生了变化,而且值所对应的地址还是和原resume的不同。

注:还可以用序列化和反序列化的办法来实现深复制

代码对应的类图

导出图片Fri Apr 10 2020 16_10_55 GMT+0800 (中国标准时间).png-362.2kB

在这里需要注意的是:

  • copy方法是NSObject类提供的复制本对象的接口。NSObject类似于Java中的Object类,在Objective-C中几乎所有的对象都继承与它。而且这个copy方法也类似于Object类的clone()方法。
  • copyWithZone(NSZone zone)方法是接口NSCopying提供的接口。而因为这个接口存在于实现文件而不是头文件,所以它不是对外公开的;即是说外部无法直接调用copyWithZone(NSZone zone)方法。copyWithZone(NSZone zone)方法是在上面所说的copy方法调用后再调用的,作用是将对象的所有数据都进行复制。因此使用者需要在copyWithZone(NSZone zone)方法里做工作,而不是copy方法,这一点和Java的clone方法不同。

优点

缺点

iOS SDK中的应用

参考

面向对象设计的设计模式(一):创建型模式(附 Demo & UML类图)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注