Swift:静态工厂方法

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

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

swift: 静态工厂方法

视图 Views

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

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

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完全相同设置的新实例,如下所示:
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关键字),因此我们可以轻松地为需要创建特定视图的应用程序部分设置扩展名,只有一个功能即可:
//我们只会在单个视图控制器中使用它,因此我们将范围设为私有(暂时),
//以免将此功能添加到我们的应用程序全局使用UIButton中。
private extension UIButton {
    static func makeForBuying() -> UIButton {
        let button = UIButton()
        ...
        return button
    }
}
  • 使用上面的静态工厂方法方法,我们现在可以使我们的UI代码看起来很漂亮,因为我们要做的就是调用我们的方法来创建所需的完全配置的实例:
class ProductViewController {
    private lazy var titleLabel = UILabel.makeForTitle()
    private lazy var buyButton = UIButton.makeForBuying()
}
  • 如果我们想使API更加简约(Swift在很多方面都鼓励使用点语法以及它如何缩短导入的Objective-C API的功能),我们甚至可以将我们的方法变成一个计算属性,如下所示:
extension UILabel {
    static var title: UILabel {
        let label = UILabel()
        ...
        return label
    }
}
  • 这将使调用更加简单和干净:
class ProductViewController {
    private lazy var titleLabel = UILabel.title
    private lazy var buyButton = UIButton.buy
}
  • 当然,如果最终将参数添加到设置API中,则需要将其转换为方法——但是对于更简单的用例,这种方式使用静态计算属性可能是不错的选择。

视图控制器 View controllers

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

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

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

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的稍加修改版本结合使用,我们现在可以在执行异步操作时轻松添加预先配置的加载视图控制器:
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模型,其中包含给定用户具有什么样的权限,并且我们的许多测试都是基于当前用户的权限来验证我们的逻辑。不必在所有测试中都使用样板数据手动创建用户,而是创建一个静态工厂方法,该方法基于一组权限返回一个用户存根,如下所示:

extension User {
    static func makeStub(permissions: Set<User.Permission>) -> User {
        return User(
            name: "TestUser",
            age: 30,
            signUpDate: Date(),
            permissions: permissions
        )
    }
}
  • 现在,我们可以摆脱任何用户设置代码,从而使我们可以专注于实际测试中的内容——例如在此处,我们将验证具有deleteFolders权限的用户是否可以删除文件夹:
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模型的更多内容,在创建存根时可能还需要设置其他属性。使用默认参数是一种简单的方式,这不需要我们添加新的方法:
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 SundellStatic factory methods in Swift简单翻译了一下,希望对大家有用

附:

  • 文中的静态工厂方法swift5.0才支持
  • 我们也可以使用类方法实现类似功能
    Swift:
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

@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
  • OC和Swift混合开发,关于宏NS_STRING_ENUM,NS_STRING_ENUM 这个宏一看非常像是对应Swift中的字符串枚举,但是实际上对应的是静态属性。
    OC: .h文件
typedef NSString * WXOptionKey NS_STRING_ENUM;

FOUNDATION_EXPORT WXOptionKey const WXOptionKeyOne;
FOUNDATION_EXPORT WXOptionKey const WXOptionKeyTwo;

OC: .m 文件

WXOptionKey const WXOptionKeyOne = @"One";
WXOptionKey const WXOptionKeyTwo = @"Two";

Swift,对应的是静态属性

struct WXOptionKey {
  static var one = "One"
  static var two = "Two"
}

使用时都可以使用点语法

  • 类方法和静态方法的区别是:类方法可以被重写,静态方法不可以

赏我一个赞吧~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容