在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")
}
}
答案 0 :(得分:2)
我认为,有关此问题的讨论必须从对依赖注入,它是什么以及它解决什么问题的理解开始。
编程就是将小组件组装成抽象的组件,这些组件可以完成很多事情。很好,但是大型程序集很难测试,因为它们非常复杂。理想情况下,我们要测试小型零件及其组装方式,而不是测试整个装配。
为此,单元测试和集成测试非常有用。但是,每个全局函数调用(包括对静态函数的直接调用,它们实际上只是一个不错的小名称空间中的全局函数)都是责任。这是一个没有接缝的固定结,可以通过单元测试将其断开。例如,如果您有一个直接调用排序方法的视图控制器,则无法隔离该排序方法来测试视图控制器。有一些后果:
sort
方法会由使用该方法的每段代码进行测试)。这不利于定期运行它们,这很重要。动态调度会引入接缝。接缝是代码中可配置性的要点。可以更改一个实现的位置,然后放入另一个实现的位置。例如,您可能想要一个MockDataStore
,一个BetaDataStore
和一个ProdDataStore
,具体取决于在环境上。如果所有这三种类型都遵循一个通用协议,则可以编写依赖代码以依赖该协议,该协议允许根据需要交换这些不同的实现。
为此,对于您希望能够隔离的代码,您永远不要使用全局函数(例如foo()
),也不要直接调用静态函数(实际上只是一个函数中的全局函数)命名空间),例如FooUtils.foo()
。如果要用foo()
替换foo2()
或用FooUtils.foo()
替换BarUtils.foo()
,则不能。
依赖注入是“注入”依赖关系的一种做法(取决于配置,而不是对其进行硬编码。您无需创建FooUtils.foo()
接口,而是对Fooable
进行依赖关系的硬编码,而不是对其进行硬编码。需要一个函数foo
。在从属代码(将调用foo
的类型)中,您将存储类型为Fooable
的实例成员。当您需要调用foo
时,调用self.myFoo.foo()
。这样,您将调用Fooable
实例在构建时已经提供(“注入”)了self
的任何实现。 MockFoo
,NoOpFoo
,ProdFoo
无关紧要,它所知道的是它的myFoo
成员具有一个foo
函数,并且可以调用它来满足所有的foo需求。
上面相同的事情也可以实现基类/子类关系,出于这些目的和目的,其行为就像协议/符合类型的关系一样。
您已经注意到,Swift在Java中提供了更多的灵活性。编写函数时,可以选择使用:
在每个地方都有适当的时间和地点。 Java将选项2和3推倒了(主要是选项2),而Swift让您更依赖自己的判断。我将讨论每种情况,何时使用或不使用。
这些功能对于其中一种实用程序功能很有用,在这种情况下,以特定方式将它们“分组”并没有太大好处。
优点:
foo
,而不是FooUtils.foo
)而使用短名称缺点:
优点:
self
的成员)的权限,几乎总是比全局状态更可取。缺点:
MathUtils
对象,只需使用其pow
实例方法,该方法实际上并不使用任何实例数据(例如MathUtils().pow(2, 2)
)优点:
缺点:
对于类,static func
类似于final class func
。 Java支持这些功能,但是在Swift中,您也可以具有非最终类功能。唯一的区别是它们支持覆盖(通过子类)。所有其他优点/缺点都与静态功能共享。
要视情况而定。
如果您要编程的部分是想隔离进行测试的部分,则全局函数不是候选对象。您必须使用基于协议或继承的依赖注入。如果代码不具有某种实例状态(并且永远不会需要它),则静态函数可能是适当的,而当需要实例状态时,则应该使用实例函数。如果不确定,则应该选择一个实例函数,因为如前所述,将一个函数从静态转换为实例是一项API重大更改,如果可能的话,您应该避免这样做。
如果新功能真的很简单,则可能是全局功能。例如。 print
,min
,abs
,isKnownUniquelyReferenced
等。但前提是没有有意义的分组。有一些例外情况可供注意:
如果您的代码重复公共前缀,命名模式等,则表明存在逻辑分组,这可以更好地表示为公共命名空间下的统一性。例如:
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点开始的重构相比,它将呼叫站点从
更改为LEDUtils.enableLED(pin: 1)`
LEDUtils.disableLED(pin: 1)`
到
guard let redLED = LED(pin: 1) else { fatalError("Invalid LED pin!") }
redLED.enable();
redLED.disable();
不仅更好,而且现在我们有了一种方法,可以使用Int
与LED
来清楚地区分那些期望任何旧整数的函数以及那些期望LED引脚号的函数。我们还为所有与LED相关的操作提供了一个中心位置,并为我们可以验证引脚号确实有效的位置提供了一个中心点。您知道如果您提供了LED
的实例,则pin
是有效的。您不需要自己检查它,因为您可以依靠已经检查过的它(否则该LED
实例将不存在)。