「设计模式」iOS 中的适配器模式 Adapter

适配器模式.png

1. 生活中的适配器

提到适配器,最先想到什么?莫过于 电源适配器 了,日常使用的电脑、手机等电子设备都会有个电源适配器,作用是将插座里输出的高压交流电转换为电子设备所需的低压直流电。另外,世界各地区除了标准电压不同以外,大部分电源插头形状也不同,所以还有一类适配器,用于连接插头,例如香港的标准插座是三角方头的,就需要一个适配器来连接转换。

概括起来,适配器的功能是让原本不能一起工作的多个设备在不改变自身行为的前提下能一起工作。发散一下的话,笔记本电脑上各种接口使用的扩展坞、在国外旅游可能用到的语言翻译器、各种显卡 / 声卡 / 硬盘的驱动程序等等都可以理解为适配器。

2. 适配器模式

2.1 适配器模式定义

在《Head First 设计模式》中的定义如下:

适配器模式:将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。

适配器模式中主要有三个角色:

  • Target 目标接口 / 对象
  • Adaptee 被适配的对象
  • Adapter 适配器

即:通过适配器 Adapter 将被适配对象 Adaptee 包装成支持目标接口 Target 的对象,使原来 Target 能够完成的任务现在通过适配器包装后的对象也能支持,所以适配器也称作包装器 Wrapper

例如国标三角插座一般是三角扁头的,而港版电源适配器是三角方头的,在内地就不好使,需要弄一个转换器,把港版电源适配器插在转换器上再把转换器插在国标插座上,就可以正常工作了。上面的国标插座就对应为 Target 目标接口角色,港版三角方头插头是被适配的对象,额外的专用适配器将国标三角扁头转换港版三角方头。

2.2 适配器的类型

按照实现适配器的方式可以分为两种类型:类适配器(继承)  和 对象(组合)适配器。类图如下:

适配器模式类图pure.png

类适配器通过继承,也就是子类化,然后在子类中实现目标接口。在支持多重继承的语言中(C++、Python),类适配器同时继承父类以及 Target,由于在 Objective-C 以及 Java 这类语言不支持多重继承,所以目标接口一般为 协议 Protocol / 接口 Interface

对象适配器通过组合 - 将被适配对象作为适配器的属性,在实现目标接口相关方法中根据需要访问被适配对象。

类适配器 vs 对象适配器

类适配器 对象适配器
实现方式 继承 组合
作用范围 仅被适配者类 被适配者类及其子类
其他 + 易于重载,必要时可以覆盖被适配者的行为。+ 结构上更简单,不需要额外属性指向被适配者。 + 可以选择将部分工作委托给被适配者,更具弹性。 - 需要额外属性指向被适配者。

2.3 适配器的优缺点

  • 使用者(客户)与接口绑定,而不是与实现绑定,实现解耦。
  • 让没有关联的类能一起工作,不侵入原有代码,隔离原系统的影响。
  • 过多使用适配器会导致代码结构混乱(任何模式过度使用都会有问题吧:)

2.4 适配器应用场景

  • 面对遗留代码,期望项目统一使用新特性同时兼容已有类。→ eg.《Head First 设计模式》ch 7. 关于迭代器与枚举的示例。
  • 扩展新功能,方便接入新的第三方库。→ eg. 《人人都懂设计模式》电子阅读器中通过适配第三方 PDF 解析库来扩展支持 PDF 阅读。

3. iOS 中的适配器模式

在 iOS 系统上,苹果一般通过协议(可以理解为 Target 为接口)来实现适配器。例如常用的 UITableViewDataSourceUITableViewDelegate,将一个原本不能为 UITableView 提供数据 / 响应相关事件的类包装成数据源 / 代理,显然这是类适配器。详细实践介绍参考 Raywenderlich

3.1 属性包装器 @propertyWrapper

在 Swift 中当需要为属性添加相同的逻辑代码时使用属性包装器会大大减少工作量。属性包装器可以应用于结构体、枚举或者类。

如 Swift 官方文档中的示例,期望整型属性值始终小于 12,可以定义如下 TwelveOrLess 属性包装器:

// 定义 *TwelveOrLess* 属性包装器
@propertyWrapper
struct TwelveOrLess {
    // 私有存储属性 number
    private var number = 0
    // 包装值
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

// 使用 *TwelveOrLess* 来定义一个小矩形,长宽都小于等于一定值。
struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height) // 打印 "0"

rectangle.height = 10
print(rectangle.height) // 打印 "10"

rectangle.height = 24
print(rectangle.height) // 打印 "12"
复制代码

*以下为个人理解,不一定正确。

可以将 @propertWrapper 也理解为一个协议,这个协议要求对象实现包装属性的 Set/Get 方法:

protocol PropertyWrapperProtocol {
    var wrappedValue : Int { set get }
}
复制代码

即通过 TwelveOrLess 实现 PropertyWrapperProtocol 协议,来实现一个适配器,这个适配器的作用是返回一个限定范围内的数,如果尝试设置超过预设最大值的数,也只会保存为最大值。类比于电源适配器将输入的高电压适配器低电压。当然 Swift 中的属性适配器更强大也更灵活,参考 Swift GG 翻译文档 - 属性包装器

3.2 应用代理适配器 UIApplicationDelegateAdaptor

iOS 14 中新增了 UIApplicationDelegateAdaptor 用于包装原来 UIKit 中的应用代理UIApplicationDelegateNSApplicationDelegateAdaptor for AppKit、WKExtensionDelegateAdaptor for WatchKit),以便在 SwiftUI 中访问应用代理。

@propertyWrapper struct UIApplicationDelegateAdaptor<DelegateType> where DelegateType : NSObject, DelegateType : UIApplicationDelegate
复制代码

从 @propertyWrapper 可以看出实际上是属性包装器的一个具体应用场景。通过泛型 DelegateType 传入一个 NSObject 类型且遵循 UIApplicationDelegate 协议的对象,猜测内部一些操作是通过转交给这个代理对象来执行的,显然是一个 对象适配器。这样使用(参考 HackingWithSwiftstackoverflow: swiftui-app-life-cycle-ios14-where-to-put-appdelegate-code):

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        print("do something")
        return true
    }
}

@main
struct testApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
复制代码

有其他 iOS 相关的适配器应用实例欢迎分享讨论。

以上就是目前学习总结的 适配器模式 相关知识了。(有些地方配合图示理解更直观,似乎还差点什么,过些日子一起补上示例代码:)

参考

  1. Raywenderlich - How To Use the Adapter Pattern. 包含一个完整的例子演示如何利用协议(数据源&委托代理)实现通用水平滚动视图。
  2. Bloodline - iOS中的设计模式 - 适配器(Adapter) 介绍挺全面的。
  3. 《Head First 设计模式》ch 7. 适配器模式与外观模式。①用插座作为示例解析适配器;②面向对象适配器小节中的 ‘现有系统 → 适配器 -) 厂商类’ 例子比较形象;③示例:Java 中通过 EnumerationIterator 枚举迭代(适配)器遵循新的 迭代器 Iterator 接口来替代早期的 枚举 Enumeration(除了判断是否还有元素及访问下一个元素,迭代器还支持移除元素) 。
  4. 《人人都懂设计模式 - 从生活中领悟设计模式》第 13 章。①中国古建筑中的榫卯结构例子,不同榫头与榫槽配合工作;②示例:一个支持 .txt 及 .epub 格式的电子阅读器项目,通过适配器适配第三方 PDF 解析库支持 PDF 阅读。

扩展

  • 一般一个适配器只包装一个被适配对象,有没有一个适配器‘包装’多个被适配对象的场景?有!那就是 外观 / 门面模式 Facade Pattern
  • 有的电源适配器除了改变插头形状/电流电压外,还会提供一些额外功能,例如状态指示灯、扩展 USB 接口等,这类特性通过 装饰者模式 Decorator Pattern 实现。(装饰者主要 添加特性,适配器主要 转换接口

Guess you like

Origin juejin.im/post/7031011469189709838