Swift:静态工厂方法

2020-02-18 15:40:15 浏览数 (1)

大多数对象在我们的APP中使用之前,都需要某种形式的设置。无论是我们要根据APP的品牌设置样式的视图(View),还是要配置的视图控制器(View Controller),亦或是在测试中创建存根的值时,我们经常发现需要将设置代码放在某个地方。

放置此类设置代码的一个非常常见的地方是子类。只需将您需要设置的对象子类化,覆盖其初始化程序并在那里进行设置——完成!尽管这肯定是一种可行的方法,但是本周,让我们看一下编写不需要任何子类形式的设置代码的另一种方法——使用静态工厂方法(static factory methods

swift: 静态工厂方法

视图 Views

视图是我们在编写UI代码时必须设置的最常见对象之一。iOS上的UIKit和Mac上的AppKit都为我们提供了创建具有原生外观的UI所需的所有基本核心构建块,但是我们经常需要自定义这些外观以适合我们的设计并为其定义布局。

同样,这是许多开发人员选择子类化并创建内置视图类的自定义变体的地方,就像这里的UILabel一样,我们将使用它来渲染标题:

代码语言:javascript复制
class TitleLabel: UILabel {
    override init(frame: CGRect) {
        super.init(frame: frame)

        font = .boldSystemFont(ofSize: 24)
        textColor = .darkGray
        adjustsFontSizeToFitWidth = true
        minimumScaleFactor = 0.75
    }
}
  • 上面的方法并没有什么真正的问题,但是它确实创建了更多类型来跟踪,而且最终我们将拥有多个子类,因为我们经常为相同视图类型配置其他变体(例如TitleLabelSubtitleLabelFeaturedTitleLabel等)。
  • 尽管子类化是一项重要的语言功能,即使在面向协议的编程时代,也很容易将自定义设置与自定义行为混淆。我们并没有在上面的UILabel中真正添加任何新行为,我们只是在设置一个实例。 因此,问题是子类是否真的适合此处的工作?
相反,让我们尝试使用静态工厂方法来实现相同的目的。我们要做的是在 UILabel 上添加一个扩展,使我们能够从上面创建与 TitleLabel完全相同设置的新实例,如下所示:
代码语言:javascript复制
extension UILabel {
    static func makeForTitle() -> UILabel {
        let label = UILabel()
        label.font = .boldSystemFont(ofSize: 24)
        label.textColor = .darkGray
        label.adjustsFontSizeToFitWidth = true
        label.minimumScaleFactor = 0.75
        return label
    }
}
  • 上述方法的优点(除了它不依赖于子类或添加任何新类型之外)是我们显然将设置代码与实际逻辑分开。
  • 此外,由于扩展名可以限制为单个文件(通过添加private关键字),因此我们可以轻松地为需要创建特定视图的应用程序部分设置扩展名,只有一个功能即可:
代码语言:javascript复制
//我们只会在单个视图控制器中使用它,因此我们将范围设为私有(暂时),
//以免将此功能添加到我们的应用程序全局使用UIButton中。
private extension UIButton {
    static func makeForBuying() -> UIButton {
        let button = UIButton()
        ...
        return button
    }
}
  • 使用上面的静态工厂方法方法,我们现在可以使我们的UI代码看起来很漂亮,因为我们要做的就是调用我们的方法来创建所需的完全配置的实例:
代码语言:javascript复制
class ProductViewController {
    private lazy var titleLabel = UILabel.makeForTitle()
    private lazy var buyButton = UIButton.makeForBuying()
}
  • 如果我们想使API更加简约(Swift在很多方面都鼓励使用点语法以及它如何缩短导入的Objective-C API的功能),我们甚至可以将我们的方法变成一个计算属性,如下所示:
代码语言:javascript复制
extension UILabel {
    static var title: UILabel {
        let label = UILabel()
        ...
        return label
    }
}
  • 这将使调用更加简单和干净:
代码语言:javascript复制
class ProductViewController {
    private lazy var titleLabel = UILabel.title
    private lazy var buyButton = UIButton.buy
}
  • 当然,如果最终将参数添加到设置API中,则需要将其转换为方法——但是对于更简单的用例,这种方式使用静态计算属性可能是不错的选择。

视图控制器 View controllers

让我们继续查看控制器,这是使用子类非常常见的另一种对象。虽然我们可能无法完全摆脱视图控制器(或与此相关的视图)的子类化,但是某些类型的视图控制器可以从工厂方法中受益。

尤其是在使用子视图控制器时,我们通常最终会得到一组视图控制器,它们只能在其中呈现特定状态,而不是在其中包含大量逻辑。对于那些视图控制器,将其设置移动到静态工厂API可能是一个很好的解决方案。

在这里,我们使用这种方法来实现一个计算属性,该属性返回一个加载视图控制器,用于显示加载旋转框:

代码语言:javascript复制
extension UIViewController {
    static var loading: UIViewController {
        let viewController = UIViewController()

        let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
        indicator.translatesAutoresizingMaskIntoConstraints = false
        indicator.startAnimating()
        viewController.view.addSubview(indicator)

        NSLayoutConstraint.activate([
            indicator.centerXAnchor.constraint(
                equalTo: viewController.view.centerXAnchor
            ),
            indicator.centerYAnchor.constraint(
                equalTo: viewController.view.centerYAnchor
            )
        ])

        return viewController
    }
}
  • 如您在上面看到的,我们甚至可以在静态属性或函数中设置内部“自动布局”约束。在这种情况下,“自动版式”的声明性确实很方便——我们可以预先指定所有约束,而不必重写任何方法或响应任何调用。
  • 就像用于视图一样,工厂方法为我们提供了非常干净的调用方式。特别是如果与"Swift:将子视图控制器用作插件" 中的便捷API的稍加修改版本结合使用,我们现在可以在执行异步操作时轻松添加预先配置的加载视图控制器:
代码语言:javascript复制
class ProductListViewController: UIViewController {
    func loadProducts() {
        let loadingVC = add(.loading)

        productLoader.loadProducts { [weak self] result in
            loadingVC.remove()
            self?.handle(result)
        }
    }
}

对添加便捷API的唯一修改是使其返回添加的子视图控制器,从而可以在使用点语法的同时获取对其的引用。当不使用该新功能时,也可以添加@discardableResult来删除所有警告。

测试存根 Test stubs

不仅需要在主应用程序代码中执行很多设置,而且在编写测试时还经常需要这样做。尤其是在测试依赖于特定模型配置的代码时,很容易以充满样板的测试结束,这使它们更难以阅读和调试。

假设我们的应用程序中有一个User模型,其中包含给定用户具有什么样的权限,并且我们的许多测试都是基于当前用户的权限来验证我们的逻辑。不必在所有测试中都使用样板数据手动创建用户,而是创建一个静态工厂方法,该方法基于一组权限返回一个用户存根,如下所示:

代码语言:javascript复制
extension User {
    static func makeStub(permissions: Set<User.Permission>) -> User {
        return User(
            name: "TestUser",
            age: 30,
            signUpDate: Date(),
            permissions: permissions
        )
    }
}
  • 现在,我们可以摆脱任何用户设置代码,从而使我们可以专注于实际测试中的内容——例如在此处,我们将验证具有deleteFolders权限的用户是否可以删除文件夹:
代码语言:javascript复制
class FolderManagerTests: XCTestCase {
    func testDeletingFolder() throws {
        // 现在,我们可以快速创建具有所需权限的用户
        let user = User.makeStub(permissions: [.deleteFolders])
        let manager = FolderManager(user: user)
        let folderName = "Test"

        try manager.addFolder(named: folderName)
        XCTAssertNotNil(manager.folder(named: folderName))

        try manager.deleteFolder(named: folderName)
        XCTAssertNil(manager.folder(named: folderName))
    }
}
  • 随着测试套件的增长以及我们开始验证涉及User模型的更多内容,在创建存根时可能还需要设置其他属性。使用默认参数是一种简单的方式,这不需要我们添加新的方法:
代码语言:javascript复制
extension User {
    static func makeStub(age: Int = 30,
                         permissions: Set<User.Permission> = []) -> User {
        return User(
            name: "TestUser",
            age: age,
            signUpDate: Date(),
            permissions: permissions
        )
    }
}
  • 现在,我们可以自由地提供年龄,一组权限或同时提供这两种权限,并且即使我们要测试的内容不依赖于任何特定的用户状态,我们甚至可以轻松地使用User.makeStub()创建空白用户。
  • 通过命名上述工厂方法makeStub,我们还可以清楚地知道此代码仅用于测试,因此将来不会意外将其添加到我们的主要应用程序目标中。

结论 Conclusion

  • 使用静态工厂方法和属性来执行对象的设置可能是一种将设置代码与实际逻辑清晰分开的好方法,可以启用漂亮的语法功能并简化编写干净的测试代码的过程。
  • 尽管子类仍然是我们工具箱中拥有的重要工具——尤其是当我们想向类型中实际添加逻辑时——摆脱仅仅执行配置的子类可以使我们的代码库更易于浏览并减少我们拥有的类型数量。 -使用静态工厂方法和属性的替代方法是使用实​​际工厂对象。如果您想了解有关此类对象以及我通常使用工厂模式的其他方式的更多信息,请查看"Swift:使用工厂模式以避免共享状态""Swift:使用工厂进行依赖注入""Swift: 使用懒加载属性"

文章来自 John Sundell的Static factory methods in Swift简单翻译了一下,希望对大家有用

附:
  • 文中的静态工厂方法swift5.0才支持
  • 我们也可以使用类方法实现类似功能 Swift:
代码语言:javascript复制
extension UILabel {
    class func makeForTitle() -> UILabel {
        let label = UILabel()
        label.font = .boldSystemFont(ofSize: 24)
        label.textColor = .darkGray
        label.adjustsFontSizeToFitWidth = true
        label.minimumScaleFactor = 0.75
        return label
    }
}

OC: 创建一个UILabelCategory

代码语言:javascript复制
@interface UILabel (Factory)
  (UILabel *)makeForTitle;
@end

@implementation UILabel (Factory)
  (UILabel *)makeForTitle {
    UILabel *label = [[UILabel alloc] init];
    label.font = [UIFont boldSystemFontOfSize:24];
    label.textColor = [UIColor darkGrayColor];
    label.adjustsFontSizeToFitWidth = YES;
    label.minimumScaleFactor = 0.75;
    return label;
}
@end
  • 类方法和静态方法的区别是:类方法可以被重写,静态方法不可以

0 人点赞