您通常使用类或结构来定义实用函数列表吗?

Che*_*eng 1 ios swift

在Java中,有一个实用函数列表是很常见的

public class Utils {
    private Utils() {
    }

    public static void doSomething() {
        System.out.println("Utils")
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我在 Swift 中,我应该使用classstruct实现类似的事情吗?或者说,这真的不重要吗?

班级

class Utils {
    private init() {
    }

    static func doSomething() {
        print("class Utils")
    }
}
Run Code Online (Sandbox Code Playgroud)

结构体

struct Utils {
    private init() {
    }

    static func doSomething() {
        print("struct Utils")
    }
}
Run Code Online (Sandbox Code Playgroud)

Ale*_*ica 5

我认为有关此问题的讨论必须从了解依赖注入、它是什么以及它解决什么问题开始。

依赖注入

编程就是将小组件组装成更抽象的组件,从而完成很酷的事情。这很好,但是大型装配体很难测试,因为它们非常复杂。理想情况下,我们希望测试小组件以及它们组装在一起的方式,而不是测试整个组件。

为此,单元测试和集成测试非常有用。然而,每个全局函数调用(包括对静态函数的直接调用,它们实际上只是一个漂亮的小命名空间中的全局函数)都是一种责任。它是一个没有接缝的固定连接,可以通过单元测试将其分开。例如,如果您有一个直接调用排序方法的视图控制器,则无法独立于排序方法来测试视图控制器。这会带来一些后果:

  1. 您的单元测试需要更长的时间,因为它们会多次测试依赖性(例如,该sort方法由使用该方法的每一段代码进行测试)。这会抑制定期运行它们的积极性,而这是一件大事。
  2. 您的单元测试在隔离问题方面变得更差。破坏了排序方法?现在你的测试有一半失败了(一切都依赖于排序方法)。查找问题比只有一个测试用例失败更困难。

动态调度引入了接缝。接缝是代码中的可配置点。可以更改删除一个实现,然后放入另一个实现。例如,您可能想要一个 an MockDataStore、 aBetaDataStore和 a ProdDataStore,这根据环境进行选择。如果所有这 3 种类型都符合通用协议,则可以编写依赖于协议的相关代码,该协议允许根据需要交换这些不同的实现。

为此,对于您希望能够隔离的代码,您永远不想使用全局函数(例如foo()),或直接调用静态函数(实际上只是命名空间中的全局函数),例如FooUtils.foo()。如果您想替换foo()foo2()FooUtils.foo()with BarUtils.foo(),则不能。

依赖注入是“注入”依赖项的实践(取决于配置,而不是对它们进行硬编码。FooUtils.foo()您创建一个需要函数的Fooable接口,而不是硬编码依赖项foo。在依赖代码中(将要执行的类型) call foo),您将存储类型 的实例成员Fooable。当您需要 call 时foo,调用self.myFoo.foo()。这样,您将调用在构造时Fooable向实例提供(“注入”)的任何实现。它可以self是 a MockFoo、 a NoOpFoo、 a ProdFoo,它并不关心。它只知道它的myFoo成员有一个foo函数,并且可以调用它来满足它的所有 foo 需求。

上面同样的事情也可以实现基类/子类关系,对于这些意图和目的,其行为就像协议/一致类型关系一样。

贸易工具

正如您所注意到的,Swift 在 Java 中提供了更多的灵活性。编写函数时,您可以选择使用:

  1. 全局函数
  2. 实例函数(结构体、类或枚举的)
  3. 静态函数(结构体、类或枚举的)
  4. (类的)类函数

每个人都有合适的时间和地点。Java 将选项 2 和 3 强加给你(主要是选项 2),而 Swift 让你更多地依靠自己的判断。我将讨论每种情况,何时您可能想要使用它,何时可能不想使用它。

1) 全局函数

这些对于其中一个实用函数很有用,在这种情况下以特定方式“分组”它们并没有多大好处。

优点:

  1. 由于不合格的访问而导致的短名称(可以访问foo,而不是FooUtils.foo
  2. 写得短

缺点:

  1. 污染全局名称空间,并使自动完成功能不再那么有用。
  2. 未以有助于发现的方式分组
  3. 无法进行依赖注入
  4. 任何访问的状态都必须是全局状态,这几乎总是引发灾难

2)实例函数

优点:

  1. 将相关操作分组到公共命名空间下
  2. 可以访问本地状态( 的成员self),这几乎总是优于全局状态。
  3. 可以进行依赖注入
  4. 可以被子类覆盖

缺点:

  1. 比全局函数的编写时间更长
  2. 有时实例没有意义。例如,如果您必须创建一个空MathUtils对象,只是为了使用它的pow实例方法,它实际上并不使用任何实例数据(例如MathUtils().pow(2, 2)

3)静态函数

优点:

  1. 将相关操作分组到公共命名空间下
  2. 可以是 Swift 中的依赖项(其中协议可以支持静态函数、下标和属性的要求)

缺点:

  1. 比全局函数的编写时间更长
  2. 将来很难将它们扩展为有状态的。一旦函数被编写为静态函数,就需要进行 API 破坏性更改才能将其转换为实例函数,如果需要实例状态,则这是必要的。

4)类函数

对于类来说,static func就像final class func. Java 支持这些函数,但在 Swift 中,您也可以拥有非 Finals 类函数。这里唯一的区别是它们支持重写(通过子类)。所有其他优点/缺点都与静态函数共享。

我应该使用哪一个?

这取决于。

如果您正在编程的部分想要隔离以进行测试,那么全局函数不是一个候选者。您必须使用基于协议或继承的依赖注入。如果代码不需要某种实例状态(并且永远不会需要它),则静态函数可能是合适的,而当需要实例状态时,则应该使用实例函数。如果您不确定,您应该选择实例函数,因为如前所述,将函数从静态转换为实例是一项 API 重大更改,并且您希望尽可能避免这种更改。

如果新的部分真的很简单,也许它可能是一个全局函数。例如printminabsisKnownUniquelyReferenced等。但前提是没有有意义的分组。有一些例外情况需要注意:

  1. 如果您的代码重复公共前缀、命名模式等,则强烈表明存在逻辑分组,这可以更好地表示为公共命名空间下的统一。例如:

    func formatDecimal(_: Decimal) -> String { ... }
    func formatCurrency(_: Price) -> String { ... }
    func formatDate(_: Date) -> String { ... }
    func formatDistance(_: Measurement<Distance>) -> String { ... }
    
    Run Code Online (Sandbox Code Playgroud)

    如果将这些功能分组在一个共同的框架下,可以更好地表达。在这种情况下,我们不需要实例状态,因此不需要使用实例方法。此外,拥有一个实例是有意义的FormattingUtils(因为它没有状态,并且没有任何东西可以使用该状态),因此禁止实例的创建可能是一个好主意。空的enum就是这样做的。

    enum FormatUtils {
        func formatDecimal(_: Decimal) -> String { ... }
        func formatCurrency(_: Price) -> String { ... }
        func formatDate(_: Date) -> String { ... }
        func formatDistance(_: Measurement<Distance>) -> String { ... }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    这种逻辑分组不仅“有意义”,而且还具有额外的好处,可以让您更接近支持这种类型的依赖注入。您所需要做的就是将接口提取到新FormatterUtils协议中,将此类型重命名为ProdFormatterUtils,并将依赖代码更改为依赖于协议而不是具体类型。

  2. 如果您发现您的代码与案例 1 类似,但又发现自己在每个函数中重复相同的参数,则非常强烈地表明您有一个类型抽象等待被发现。考虑这个例子:

    func enableLED(pin: Int) { ... }
    func disableLED(pin: Int) { ... }
    func checkLEDStatus(pin: Int) -> Bool { ... }
    
    Run Code Online (Sandbox Code Playgroud)

    我们不仅可以应用上面第 1 点的重构,而且我们还可以注意到这pin: Int是一个重复的参数,可以更好地将其表示为类型的实例。比较:

    class LED { // or struct/enum, depending on the situation.
        let pin: Int
    
        init(pin: Int)? {
            guard pinNumberIsValid(pin) else { return nil }
            self.pin = pin
        }
    
        func enable() { ... }
        func disable() { ... }
        func status() -> Bool { ... }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    与第 1 点的重构相比,这将调用站点从

    LEDUtils.enableLED(pin: 1)`
    LEDUtils.disableLED(pin: 1)`
    
    Run Code Online (Sandbox Code Playgroud)

    guard let redLED = LED(pin: 1) else { fatalError("Invalid LED pin!") }
    redLED.enable(); 
    redLED.disable();
    
    Run Code Online (Sandbox Code Playgroud)

    这不仅更好,而且现在我们有一种方法可以通过使用Intvs来清楚地区分需要任何旧整数的函数和那些需要 LED 引脚编号的函数LED。我们还为所有与 LED 相关的操作提供了一个中心位置,以及一个我们可以验证引脚号确实有效的中心点。您知道,如果您提供了一个实例LED,则该实例pin是有效的。您不需要自己检查它,因为您可以依赖已经检查过的它(否则这个LED实例将不存在)。