Swift 初学者秘技:如何用模块(Module)进一步细粒度控制代码访问控制

概览

按照软件工程学里面"低耦合,高聚合"这一至理名言,我们有必要只让代码只知道它该应该知道的,不该知道的"打死也不说",越包代俎的事儿坚决不能做!

在 Swift 语言中我们可以合理利用访问控制(Access Control)辅以模块来完成这一神圣使命。

在本篇博文中,您将学到如下内容:

  1. Swift 语言访问控制简介
  2. 用扩展(extension)来隔离代码
  3. "美中不足"的扩展
  4. 利用库让问题冰解的破

那还等什么呢?让我们马上开始控制之旅吧!Let's go!!!;)


1. Swift 语言访问控制简介

和其它有理想、有颜值且懂"情趣"的编程语言一样,Swift 也提供代码访问控制(Access Control)通过声明、文件以及模块等对象来管理代码的可见性。

我们可以将特定的访问级别分配给各种类型(类、结构和枚举),以及属于这些类型的属性、方法、初始值预设项和下标。协议可以被限制在特定上下文中,全局常量、变量和函数自然也不在话下。

具体来说 Swift 对于代码实体(Code Entities)提供 5 种不同的访问级别(Access Levels),它们分别是:

  • open:最高且最自由(highest and least restrictive)的访问级别,一般用在模块中;
  • public:自由度仅次于 open 的访问级别,一般也用在模块中;
  • internal:默认的访问级别。它使实体能够在其定义模块的任何源文件中使用,但不能在该模块之外的任何源文件中使用。一般用在定义应用程序或框架内部结构中;
  • fileprivate:这个访问级别很好理解:它的访问级别仅限制在某一个源代码文件的内部,在此之外一切都是"渺无踪影";
  • private:这是最低也是最不自由(lowest and most restrictive)的访问级别,它将实体的使用限制到封闭声明以及同一文件中该声明的扩展中。一般用在特定功能的实现细节仅局限在单个声明的情况中;

上面的 open 仅适用于类和类成员,其与 public 的不同在于 open 允许模块外的代码进行子类化和重写。

将一个类标记为 open 明确表示我们已经充分考虑了其他模块将该类用作超类的代码影响。这除了为撸码人提供了超高自由度以外,无疑也更加需要秃头码农们的深思熟虑,因为稍不留神它就会成为秒天、秒地、秒空气的一把锋利"双刃剑"。

全面介绍访问控制超出了本篇范畴,有机会将在后续博文中结合具体实例进一步展开说明。

2. 用扩展(extension)来隔离代码

大家都知道,在 Swift 语言里有一种超实用的代码扩展机制:extension。

swift 复制代码
class LocalModel {
    var count = 0
}

extension LocalModel {
    func inc() {
        count += 1
    }
}

如上代码所示:我们创建了一个 LocalModel 类,并将 inc() 方法放到它的扩展中去了。

把玩过 ruby 的秃头码农们都知道:Swift 中的扩展和 ruby 中"打开"一个类再改点什么或新塞点什么进去差不多。

当然 ruby 的动态性要远超 Swift,所以我们可以在类对象上利用 class_eval 方法动态执行代码的方式实时的"扩展"其自身:

ruby 复制代码
Model.class_eval do
    def power
        11
    end  
end  

mod.power
# => 11

好了,下面言归正传。

在 Swift 中发挥扩展真正威力之处在于:我们可以将一个大类(或结构)拆分到不同源代码文件中去"分而治之"。

swift 复制代码
class LocalModel {
    var count = 0
}

// LocalModel+inc_dec.swift
// 加一、减一运算(Increasing and decreasing)
extension LocalModel {
    func inc() {
        count += 1
    }
    
    func dec() {
        count -= 1
    }
}

// LocalModel+add_sub.swift
// 加法与减法运算(Add and sub)
extension LocalModel {
    func add(_ value: Int) {
        count += value
    }
    
    func sub(_ value: Int) {
        count -= sub
    }
}

// LocalModel+verifying.swift
// 验证操作(Verifying)
extension LocalModel {
    var isTooBig: Bool {
        count > 10000
    }
    
    var isTooSmall: Bool {
        count <= -10000
    }
    
    var isNice: Bool {
        count == 11
    }
}

在上面示例代码中,我们通过扩展将 LocalModel 类的相关逻辑划分到 3 个不同的源代码文件中去,这在"巨婴"类中可以极大改善代码的明晰度,帮助秃头码农们少挠掉几根白头发。

3. "美中不足"的扩展

现在大家来想象下面这一种情况:我们需要一个 Model 类来处理定时事件,当事件发生时我们将 Model#value 的值增加 1。

为了充分利用 Swift 的扩展机制,我们将 Model 的实例属性全部放在 Model.swift 文件中:

swift 复制代码
// Model.swift
import Foundation
import Combine

final class Model {
    
    static let shared = Model()
    
    var value = 0
    var timer: AnyPublisher<Date,Never>?
    var cancel: AnyCancellable?
    
    private init() {
        
    }
}

从上面的代码中可以看到我们的 Model 是一个单例类,这意味着它只能使用唯一一个 shared 实例对象。

接下来,我们单开一个 Model+ext.swift 文件来存放与时钟相关的逻辑:

swift 复制代码
// Model+ext.swift
import Foundation
import Combine

extension Model {
    func startTimer() {
        timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect().eraseToAnyPublisher()
        
        cancel = timer?.sink { [unowned self] _ in
            value += 1
        }
    }
    
    func stopTimer() {
        cancel?.cancel()
        cancel = nil
        timer = nil
    }
}

这样划分并没有什么不妥,不过随后我们发现 Model 中的某些实例属性不应该被外界所感知,所以我们将它们设为 private 访问级别:

swift 复制代码
final class Model {
    
    public static let shared = Model()
    
    private(set) var value = 0
    private var timer: AnyPublisher<Date,Never>?
    private var cancel: AnyCancellable?
    
    private init() {}
}

可是这样一来"可捅了马蜂窝鸟",编译器会立即发出惹眼的抱怨:

出现这种情况的原因很简单:因为被 private 访问级别修饰的实例并不能在 class 和它们的扩展中共享。一种处理方法是像 value 属性那样也将它们用 private(set) 来修饰,不过这样只是略好一些,从外部还是可以探查到 Model 的内部实现。

有没有更好的解决方法呢?

答案是肯定的!

4. 利用库让问题冰解的破

之前提到过,在 Swift 中利用模块(Module)可以从宏观层面分割代码逻辑。要知道 App 本身也可以被看做是一个"自给自足"的模块,所以如果我们将上面 Model 定义和扩展通通放到一个单独的模块中去的话就可以进一步做更细粒度"细针密缕"的排兵布阵。

首先,在 Xcode 中新建一个库项目,选择动态或静态库都无所谓,这里为了简单我们就使用静态库(Static Library)吧:

和上面类似,在创建的库中分别创建 Model.swift 和 Model+ext.swift 源代码文件:

swift 复制代码
// Model.swift in Module
public final class Model {
    
    public static let shared = Model()
    
    public var value = 0
    var timer: AnyPublisher<Date,Never>?
    var cancel: AnyCancellable?
    
    private init() {}
}

// Model+ext.swift in Module
import Foundation
import Combine

extension Model {
    public func startTimer() {
        timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect().eraseToAnyPublisher()
        
        cancel = timer?.sink { [unowned self] _ in
            value += 1
        }
    }
    
    public func stopTimer() {
        cancel?.cancel()
        cancel = nil
        timer = nil
    }
}

聪明的小伙伴们想必已经发现了,放进库中的 Model 与之前在 App 中的有以下几点不同:

  • 所有向库外共享的类或类中的属性、方法等实体都必须用 public(或open)修饰;
  • 库中 internal(即默认无修饰)访问级别现在已经无法被库外所感知了;

这样做的好处是,我们 Model 中 internal 类型的实例属性(timer,cancel)已经无法被 App 代码所察觉,但是在 Model 扩展中依然可以一览无遗、运用自如:

使用模块的"精髓"在于:我们又为访问多加了新的一层控制。当然使用 Module 来精细化控制访问级别只是一种解决方案,大家还有什么更好的解决之道呢?欢迎一起讨论吧!

总结

在本篇博文中,我们介绍了 Swift 语言中的访问控制级别(Access Control),并讨论了如何利用外部模块(Module)进一步细粒度控制代码逻辑的可见性。

感谢观赏,再会!8-)

相关推荐
iOS阿玮13 小时前
苹果审核被拒,其实可以靠回复也能过审
uni-app·app·apple
大熊猫侯佩13 小时前
SwiftUI 调整视图内容周围间隙(Content Margins)的“时髦”方法
swiftui·swift·apple
Eden小峰13 小时前
ReactiveSwift 核心操作符
swift
大熊猫侯佩13 小时前
Swift 5.9 中 if 与 switch 语句简洁新语法让撸码更带劲
swift·编程语言·apple
I烟雨云渊T2 天前
iOS swiftUI的实用举例
ios·swiftui·swift
大熊猫侯佩2 天前
SwiftUI 中为何 DisclosureGroup 视图在收缩时没有动画效果?
swiftui·swift·apple
大熊猫侯佩2 天前
Swift 初学者交心:在 Array 和 Set 之间我们该如何抉择?
数据结构·性能优化·swift
大熊猫侯佩2 天前
Swift 中更现代化的调试日志系统趣谈(一)
debug·swift·apple