独孤九剑--设计模式(iOS结构型篇)

独孤九剑--设计模式(iOS创建型篇)
独孤九剑--设计模式(iOS行为型篇)

适配器模式 Adapter Pattern

情景

Apple公司维护了其员工管理系统,员工数据如下:

struct AppleEmployee {
    var name: String
    var seniority: Int
    var salary: Double
    
    func getEmployeeInfo() -> String {
        return "员工:\(name), 工龄:\(seniority), 薪酬:\(salary)"
    }
}

现在Apple公司收购了Spaces公司,Spaces公司也有一套自己的员工管理系统,员工数据如下;显而易见的是,不管从数据结构,还是提供的功能来说,两者完全不兼容;

struct SpacesEmployee {
    var s_name: String
    var s_seniority: Int
    var s_salary: Int
}

现在Apple需要将Spaces员工接入到自己的系统;

简单粗暴的做法有2种:

  1. 直接修改Spaces员工系统所有源码,重构成Apple系统要求的数据结构、方法调用;
    对于庞大的系统而言,这种改法工作量巨大而且就容易出错;

  2. 在使用Apple员工功能的地方,都加一个对应的Spaces功能;
    以系统中的一个获取员工信息的功能为例

// 原有系统的功能
func dealEmployee(employee: AppleEmployee) {
    // ...
    print(employee.getEmployeeInfo())
}

Spaces员工也需要支持该功能,重写一样的功能:

func dealEmployee(employee: SpacesEmployee) {
    // ...
    let spacesEeInfo = "员工:\(employee.s_name), 工龄:\(employee.s_seniority), 薪酬:\(Double(employee.s_salary))"
    print(spacesEeInfo)
}

这种方式相比第一种方案工作量小一点点,但代码同样很难维护;
(这还是基于Swift语言的方法重载而言,如果是OC那将会比这个更复杂,不能使用重载那OC就需要判断到底是哪个类型,增加一系列if else)

破解招式

适配器模式

将一个接口转换成客户希望的另一个接口,从而使原本接口不匹配而无法一起工作的两个类能够在一起工作。

适配器模式又分为2种:类适配器模式,对象的适配器模式

类适配器

类适配器模式是声明一个公共接口,通过继承的方式把适配的类的API转换成为目标类的API。

UML类图
class adapter

Target:目标抽象类;Adapter:适配器类;Adaptee:适配者类

代码实现
  1. 提取adapter、adaptee需要用到的公共属性、方法,封装成接口/protocol
protocol Employee {
    var salary: Double { get }
    func getEmployeeInfo() -> String
}
  1. 扩展target (AppleEmployee),实现Employee协议
// target
extension AppleEmployee : Employee {
        // Employee协议接口都是根据AppleEmployee 提取的; 因此原本就已经实现协议方法了
}
  1. 新建adapter类,继承需要适配的adaptee并实现Employee协议;(如果是C++这种支持多继承的语言,adapter可以继承adaptee和target)
class SpacesEmployeeClassAdapter : SpacesEmployee, Employee {
    var salary: Double {
        return Double(self.salary)
    }
    
    func getEmployeeInfo() -> String {
        return "员工:\(self.s_name), 工龄:\(self.s_seniority), 薪酬:\(Double(self.s_salary))"
    }
}
  1. 重构代码,AppleEmployee都替换成Employee;需要接入SpacesEmployee的使用其子类SpacesEmployeeClassAdapter
let appleEmployee = AppleEmployee(name: "jobs", seniority: 20, salary: 100000)
dealEmployee(employee: appleEmployee)
let employeeAdapter = SpacesEmployeeClassAdapter(s_name: "jack", s_seniority: 1, s_salary: 5000)
dealEmployee(employee: employeeAdapter)
func dealEmployee(employee: Employee) {
    // ...
    print(employee.getEmployeeInfo())
}

对象适配器

与类适配器不同,对象适配器不继承被适配者,而是组合了一个对其引用。

UML类图

类适配器中,由于语言不支持多继承,创建了公有协议,变相的也更改了使用Target的具体类(AppleEmployee)的地方;对象适配器,适配器不需要继承被适配者,现在就可以将适配器对象直接继承自Target的具体类,公有协议也无需创建;

instance adapter
代码实现
  1. 新建adapter对象SpacesEmployeeInstanceAdapter, 继承自AppleEmployee;adapter内引用adaptee对象,重写AppleEmployee方法;
class SpacesEmployeeInstanceAdapter : AppleEmployee {
    var spacesEmployee: SpacesEmployee
    
    init(spacesEmployee: SpacesEmployee) {
        self.spacesEmployee = spacesEmployee
        super.init(name: spacesEmployee.s_name, seniority: spacesEmployee.s_seniority, salary: Double(spacesEmployee.s_salary))
    }
    
    override func getEmployeeInfo() -> String {
        return "员工:\(spacesEmployee.s_name), 工龄:\(spacesEmployee.s_seniority), 薪酬:\(self.salary)"
    }
}
  1. 需要接入SpacesEmployee的使用SpacesEmployeeInstanceAdapter
let appleEmployee = AppleEmployee(name: "jobs", seniority: 20, salary: 100000)
dealEmployee(employee: appleEmployee)
let spacesEmployee = SpacesEmployee(s_name: "jack", s_seniority: 1, s_salary: 5000)
let employeeAdapter = SpacesEmployeeInstanceAdapter(spacesEmployee: spacesEmployee)
dealEmployee(employee: employeeAdapter)
func dealEmployee(employee: AppleEmployee) {
    // ...
    print(employee.getEmployeeInfo())
}

对象适配器和类适配器对比

适配器模式使用场景

适配器模式属于是被动设计的,使用适配器模式一般都是发现系统现有类的接口不符合系统的需要,或者多套相同功能系统间不兼容,需要更优雅实现而引入;如果在最开始就已经知道后续的需求,那完全可以在当时就设计好接口、数据结构等;

另一个场景是,使用不同三方库的相同功能时,当无法修改他们的源码,适配器模式也会是一个很好的处理方式

适配器模式在iOS系统中的使用

iOS中的委托(delegate、dataSource)按其用途来说其实是使用了对象适配器模式的;实现代理协议的具体类就是个适配器;
以UITableView为例,tableView需要多少行的代码:

tableView. dataSource=vc;

// vc
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.datas.count;
}

vc就是adapter,它实现了tableView的numberOfRowsInSection接口要求,适配了tableView;datas就相当于adaptee,vcnumberOfRowsInSection的具体实现中,调用了datas而得到真正需要的数据;

桥接模式 Bridge

情景

App需要新增一个分享功能;

  1. 第一个版本需求是分享文字到v信;
    简单的面向对象代码:
// v信分享平台
struct WXSharePlatform {
    func shareText(text: String) {
        // WXApi ....
        // ...
        print("\(text) v信分享成功")
    }
}

// 文字分享类
struct WXTextShare {
    var text: String
    private let wxPlatform = WXSharePlatform()
    
    func share() {
        wxPlatform.shareText(text: text)
    }
}

客户端分享调用:

let share = WXTextShare(text: "心灵毒鸡汤...")
share.share()

实现的还行。。。

  1. 迭代需求:需要支持对图片内容的分享,同时分享平台还要支持QQ;
    还是一样的套路,新建对应的类即可:
// QQ平台
struct QQSharePlatform {
    func shareText(text: String) {
        // QQApi ....
        print("\(text) QQ分享成功")
    }
    
    func shareImage(imageData: Data) {
        // QQApi ....
        print("图片 QQ分享成功")
    }
}

// 新增wx图片分享、新增qq文字分享、新增qq图片分享 处理类
struct WXImageShare {
....
}

struct QQTextShare {
...
}

struct QQImageShare {
...
}

忍忍还是能勉强接受。。。

  1. 迭代需求,需要支持对视频内容的分享
    。。。
  2. 迭代需求,需要支持微博平台
    。。。

我想你已经疯了,也意识到问题所在:
随着功能不断增加,类层级爆炸式增长,而且这些类大部分代码都是重复的;

破解招式

桥接模式

将抽象部分与它的实现部分分离,使它们都可以独立地变化

以上示例,存在两个维度:1. 分享内容,2.分享平台;
使用桥接模式需要做的,就是将分享内容分享平台分离,使他们独立变化;

UML类图

bridge
  • Abstraction:定义抽象接口,拥有一个Implementor类型的对象引用
  • RefinedAbstraction:扩展Abstraction中的接口定义
  • Implementor:具体实现的接口
  • ConcreteImplementor:实现Implementor接口,给出具体实现

代码实现

  1. 抽象出分享平台实现类接口
// implementor
protocol SharePlatform {
    func shareText(text: String)
    
    func shareImage(imageData: Data)
}
  1. 重构分享平台实现类
// ConcreteImplementor
// 内部实现不变
struct WXSharePlatform : SharePlatform {
    func shareText(text: String) {
...
    }
    
    func shareImage(imageData: Data) {
...
    }
}

struct QQSharePlatform : SharePlatform {
    func shareText(text: String) {
...
    }
    
    func shareImage(imageData: Data) {
...
    }
}
  1. 定义分享内容的抽象接口
// Abstraction
protocol ShareContent {
    var implementor: SharePlatform { get set }
    
    func share()
}
  1. 扩展分享内容接口定义
// RefinedAbstraction
struct TextShare : ShareContent {
    var implementor: SharePlatform
    var text: String
    
    func share() {
        implementor.shareText(text: text)
    }
}

struct ImageShare : ShareContent {
    var implementor: SharePlatform
    var imageData: Data
    
    func share() {
        implementor.shareImage(imageData: imageData)
    }
}

客户端调用分享:

let wx = WXSharePlatform()
let share1 = TextShare(implementor: wx, text: "心灵毒鸡汤...")
share1.share()
let imgData = ...
let qq = QQSharePlatform()
let share2 = ImageShare(implementor: qq, imageData: imgData)
share2.share()

当需要增加一类分享内容,或新增一类分享平台,都只需要增加一个类即可;具体的操作都通过Abstraction与Implementor组合实现

适用场景

  • 扩展相同功能时,不断的新增类导致的类层级爆炸问题;可以采用桥接模式
  • 如果出现抽象部分和实现部分都应该可以扩展的情况,可以采用桥接模式
  • 如果你不希望在抽象和实现部分采用固定的绑定关系,可以采用桥接模式

装饰模式 Decorator Pattern

情景

开发一套网上商城功能:售卖指定品类的物品,每类物品价格固定;选择商品后给出商品信息及其价格;

简单实现:
抽象商品公有接口

protocol Goods {
    var name: String { get }
    var price: Double { get }
}

extension Goods {
    func sold() {
        print("\(name), price:\(price)")
    }
}

为每种商品新建对应对象

class Flower: Goods {
    var name: String = "鲜花"
    var price: Double = 99.9
}

class Chocolate: Goods {
    var name: String = "巧克力"
    var price: Double = 66
}

使用

let flower = Flower()
flower.sold()
// 鲜花, price:99.9

需求迭代:适逢节日,商城为每种商品提供有偿的礼物包装和贺卡销售,需要编码支持;
简单。。。在原有商品基础上继承一个新类即可:

// 商品= 鲜花+礼物包装
class FlowerWithGift: Flower {
    private let giftPrice = 5.0
    
    override init() {
        super.init()
        name = super.name + "+" + "礼物包装"
        price = super.price + giftPrice
    }
}

class FlowerWithCard: Flower {...}
class ChocolateWithGift: Chocolate {...}
class ChocolateWithCard: Chocolate {...}

选择了鲜花并且需要包装的:

  let flowerGift = FlowerWithGift()
  flowerGift.sold()
// 鲜花+礼物包装, price:104.9

如果消费者选择鲜花并且需要包装的,同时还要求提供贺卡呢?
好像写的类还不满足,那就再加一个:

class FlowerWithGiftCard: FlowerWithGift {...}

如果再增加一个商品附属品呢,按照组合计算,那每个商品类就都需要2^3-1=7个子类了。。。

破解招式

装饰模式

动态地给一个对象添加一些额外的职责。就扩展功能来说,装饰模式相比生成子类更为灵活

UML类图

decorator.png
  • Component:给出一个抽象接口,以规范准备接受附加责任的对象。
  • ConcreteComponent:定义一个将要接收附加责任的类。
  • Decorator:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
  • ConcreteDecorator:负责给构件对象添加附加职责。

代码实现

  1. 抽象商品附属品协议,继承自原有商品协议
// Decorator
// 商品附属品
protocol GoodsAttached: Goods {
    var goods: Goods { get }
}
  1. 礼物包装、贺卡单独封装 具体实现
// concreteDecorator
// 礼物包装
struct Gift: GoodsAttached {
    private let giftPrice = 5.0
    
    var goods: Goods
    
    var name: String {
        return goods.name + "+" + "礼物包装"
    }
    
    var price: Double {
        return goods.price + giftPrice
    }
}

// 贺卡
struct Card: GoodsAttached {
    private let cardPrice = 1.1
    
    var goods: Goods
    
    var name: String {
        return goods.name + "+" + "贺卡"
    }
    
    var price: Double {
        return goods.price + cardPrice
    }
}
  1. 原有商品类Component、ConcreteComponent不做更改;
  2. 商品+礼物包装+贺卡 调用
let flower = Flower()
let giftWrap = Gift(goods: flower)
let cardWrap = Card(goods: giftWrap)
cardWrap.sold()
// 鲜花+礼物包装+贺卡, price:106.0

后续再增加附属品时,只要增加一个对应的concreteDecorator类即可

适用场景

  • 用继承扩展功能不太现实的情况下,应该考虑用组合的方式
  • 透明并且动态地给对象增加新的职责

外观模式 Facade Pattern

为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

相对比较简单

facade.png
// facade
struct System {
    let sa = SystemA()
    let sb = SystemB()
    let sc = SystemC()
    
    func start() {
        // 封装调用子系统
        sa.open()
        sb.turnOn()
        sc.run()
    }
}

代理模式 Proxy Pattern

为其他对象提供一种代理,以控制对这个对象的访问

UML类图

proxy.png
  • Subject:抽象主题角色,声明了目标对象和代理对象的共同接口,这样一来在任何可以使用目标对象的地方都可以使用代理对象。
  • RealSubject:具体主题角色,被代理对象,定义了代理对象所代表的目标对象。
  • Proxy:代理类,代理对象内部含有目标对象的引用,从而可以在任何时候操作目标对象;代理对象提供一个与目标对象相同的接口,以便可以在任何时候替代目标对象。代理对象通常在客户端调用传递给目标对象之前或之后,执行某个操作,而不是单纯地将调用传递给目标对象。

示例

新版mac只有美丽国才能买到,在中国需要购买就需要通过代购(代理Proxy);

  1. 创建购买者抽象对象接口
protocol Buyer {
    func buyMac()
}
  1. 创建真实的购买者对象
struct ChinaBuyer: Buyer {
    func buyMac() {
        print("china buyer buy Mac")
    }
}
  1. 创建代理对象
struct BuyerProxy: Buyer {
    let buyer = ChinaBuyer()
    func buyMac() {
        buyer.buyMac()
        print("from usa")
    }
}

调用:

let proxy = BuyerProxy()
proxy.buyMac()

应用

代理模式的应用形式:

  • 远程代理:即为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实;
  • 虚拟代理:即根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象;
  • 安全代理:用来控制真实对象访问时的权限;
  • 智能指引:即当调用真实对象时,代理处理另外一些事。

iOS系统中的代理模式

说到代理,iOS开发立马就想到delegate,dataSource;
但实际上,他们并不是代理模式,只能称为委托,实质上还是归为适配器模式(参考上面适配器模式);

代理模式和对象适配器模式,从UML图也可以看出,他们是及其类似的;有区别的是代理模式中RealSubject也需要实现同一个接口,而适配器中Adaptee无限制;根据实际应用场景也很容易分辨出来;

而iOS中真正使用到代理模式的NSProxy,NSProxy是个抽象基类,使用时需子类化;
具体可以根据YYKit的YYWeakProxy分析:

@interface YYWeakProxy : NSProxy

/**
 The proxy target.
 */
@property (nullable, nonatomic, weak, readonly) id target;

 // return A new proxy object.
- (instancetype)initWithTarget:(id)target;
...
@end

YYWeakProxy对应proxy,target就是RealSubject;
proxy重写了forwardingTargetForSelector等动态转发方法,当给proxy发送消息,基于oc runtime机制会将消息转发给target;实现了代理作用;

(YYWeakProxy这种代理模式,主要是为了解决内存循环引用问题)


完整代码


参考:
《Objective-C编程之道》
《精通Swift设计模式》
《大话设计模式》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容