ios - 您通常使用类或结构来定义实用程序功能列表吗?

标签 ios swift

在Java中,拥有实用程序功能列表非常普遍

public class Utils {
    private Utils() {
    }

    public static void doSomething() {
        System.out.println("Utils")
    }
}

如果我在Swift中,应该使用class还是struct实现类似的目的?还是没关系?


class Utils {
    private init() {
    }

    static func doSomething() {
        print("class Utils")
    }
}

结构
struct Utils {
    private init() {
    }

    static func doSomething() {
        print("struct Utils")
    }
}

最佳答案

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

依赖注入

编程就是将小部件组装成抽象的,能完成很多事情的程序集。很好,但是大型程序集很难测试,因为它们非常复杂。理想情况下,我们要测试小型零件及其组装方式,而不是测试整个组件。

为此,单元测试和集成测试非常有用。但是,每个全局函数调用(包括对静态函数的直接调用,它们实际上只是一个不错的小名称空间中的全局函数)都是责任。这是一个没有接缝的固定结,可以通过单元测试将其断开。例如,如果您有一个直接调用排序方法的视图控制器,则无法隔离该排序方法来测试视图控制器。有一些后果:

  • 您的单元测试会花费更长的时间,因为它们会多次测试依赖关系(例如sort方法由使用它的每段代码进行测试)。这不利于定期运行它们,这很重要。
  • 您的单元测试在隔离问题上变得更糟。打破了排序方法?现在,您的测试有一半都失败了(所有传递性都取决于sort方法)。与仅单个测试用例失败相比,寻找问题更难。

  • 动态调度会引入接缝。接缝是代码中可配置性的要点。可以更改一种实现的位置,然后放入另一种实现的位置。例如,您可能需要一个MockDataStoreBetaDataStoreProdDataStore,具体取决于环境。如果所有这三种类型都符合一个通用协议,则可以编写依赖代码以依赖该协议,该协议允许根据需要交换这些不同的实现。

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

    依赖注入是“注入”依赖关系(取决于配置,而不是对其进行硬编码的一种做法。)而不是对依赖关系硬编码FooUtils.foo(),而是创建一个Fooable接口,该接口需要函数foo。在依赖代码中(会调用foo的类型,您将存储一个Fooable类型的实例成员。当您需要调用foo时,请调用self.myFoo.foo()这样,您将调用已提供(“注入”)到Fooable实例的任何self实现。在构造时,它可以是MockFooNoOpFooProdFoo,它不在乎,它知道它的myFoo成员具有foo函数,可以调用它来照顾所有这是foo的需求。

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

    交易工具

    如您所见,Swift在Java中提供了更多的灵活性。编写函数时,可以选择使用:
  • 全局函数
  • (结构,类或枚举的)实例函数
  • (结构,类或枚举的静态函数)
  • (一个类的)类函数

  • 在每个地方都有一个合适的时间和地点。 Java将选项2和3推倒了(主要是选项2),而Swift使您可以更经常地依靠自己的判断。我将讨论每种情况,何时使用或不使用。

    1)全局功能

    这些对于其中一种实用程序功能可能很有用,在这种情况下,以特定方式将它们“分组”并没有太大好处。

    优点:
  • 由于访问不合格(可以访问foo,而不是FooUtils.foo)而导致的短名称
  • 简写

  • 缺点:
  • 污染全局名称空间,并使得自动完成功能不那么有用。
  • 不以有助于发现的方式分组
  • 不能依赖注入
  • 任何访问状态都必须是全局状态,这几乎总是灾难
  • 的秘诀

    2)实例功能

    优点:
  • 公用名称空间
  • 下的组相关操作
  • 可以访问本地化状态(self的成员),几乎总是比全局状态更可取。
  • 可以依赖注入
  • 可以被子类
  • 覆盖

    缺点:
  • 比全局函数
  • 更长的编写时间
  • 有时实例没有意义。例如。如果您必须创建一个空的MathUtils对象,则只需使用其pow实例方法,该方法实际上并不使用任何实例数据(例如MathUtils().pow(2, 2))

  • 3)静态功能

    优点:
  • 公用名称空间
  • 下的组相关操作
  • 在Swift中可以依赖(协议可以支持对静态函数,下标和属性的要求)

  • 缺点:
  • 比全局函数
  • 更长的编写时间
  • 将来很难将它们扩展为有状态的。一旦将函数编写为静态函数,则需要更改API才能将其转换为实例函数,如果需要实例状态时,则必须进行更改。

  • 4)类功能

    对于类,static func类似于final class func。 Java支持这些功能,但是在Swift中,您也可以具有非最终类功能。唯一的区别是它们支持覆盖(由子类覆盖)。所有其他优点/缺点与静态功能共享。

    我应该使用哪一个?

    这取决于。

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

    如果新功能真的很简单,则可能是全局功能。例如。 printminabsisKnownUniquelyReferenced等。但前提是没有有意义的分组。有一些例外情况需要注意:
  • 如果您的代码重复了 public 前缀,命名模式等,则强烈表明存在逻辑分组,可以更好地表示为 public 命名空间下的统一。例如:
    func formatDecimal(_: Decimal) -> String { ... }
    func formatCurrency(_: Price) -> String { ... }
    func formatDate(_: Date) -> String { ... }
    func formatDistance(_: Measurement<Distance>) -> String { ... }
    

    如果将这些功能归为一类,可以更好地表达。在这种情况下,我们不需要实例状态,因此我们不需要使用实例方法。另外,有一个FormattingUtils实例是有意义的(因为它没有状态,没有任何东西可以使用该状态),因此,禁止创建实例是一个好主意。一个空的enum就是这样做的。
    enum FormatUtils {
        func formatDecimal(_: Decimal) -> String { ... }
        func formatCurrency(_: Price) -> String { ... }
        func formatDate(_: Date) -> String { ... }
        func formatDistance(_: Measurement<Distance>) -> String { ... }
    }
    

    这种逻辑分组不仅“有意义”,而且还具有使您更进一步支持这种类型的依赖注入的额外好处。您需要做的就是将接口提取到新的FormatterUtils协议中,将此类型重命名为ProdFormatterUtils,并更改依赖代码以依赖协议而不是具体类型。
  • 如果您发现自己有和情况1一样的代码,而且还发现自己在每个函数中都重复了相同的参数,则非常有力地表明您刚刚等待发现类型抽象。考虑以下示例:
    func enableLED(pin: Int) { ... }
    func disableLED(pin: Int) { ... }
    func checkLEDStatus(pin: Int) -> Bool { ... }
    

    我们不仅可以从上面的第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 { ... }
    }
    

    与从第1点进行重构相比,这将 call 站点从
    LEDUtils.enableLED(pin: 1)`
    LEDUtils.disableLED(pin: 1)`
    


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

    这不仅更好,而且现在我们有了一种方法,可以使用IntLED来清楚地区分期望任何旧整数的函数和期望LED引脚号的函数。我们还为所有与LED有关的操作提供了一个中心位置,并为我们可以验证引脚号确实有效的位置提供了一个中心点。您知道如果您提供了LED实例,则pin是有效的。您不需要自己检查它,因为您可以依靠已经检查过的它(否则这个LED实例将不存在)。
  • 关于ios - 您通常使用类或结构来定义实用程序功能列表吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56963022/

    相关文章:

    ios - 如何按日期作为部分从核心数据中获取数据?

    ios - iOS 的 Facebook SDK 共享对话框错误

    iPhone 5 [[UIScreen mainScreen] bounds].size.height

    ios - 用户位置检索不起作用

    ios - 如何使 3D Touch Quick Action 与 UITabBar 一起使用?

    ios - 将 CIFilter 保存为 RAW 图像

    ios - 让应用程序在终止时保存数据

    ios - 向下滚动时出现 fatal error

    objective-c - cellForRowAtIndexPath - [indexPath row] 滚动时返回奇怪的数字

    设置 setVisibleXRangeMaximum 时,iOS-Charts X 轴值无限重复