每周技巧 #42:优先使用工厂函数,而不是初始化方法

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #42: Prefer Factory Functions to Initializer Methods

原文最初作为 totw/42 发布于 2013 年 5 月 10 日。

作者:Geoffrey Romer

修订于 2017-12-21。

“建造工厂的人建造了一座神殿;在那里工作的人在那里礼拜。二者应得的不是蔑视和责备,而是敬意和赞美。”– Calvin Coolidge

在禁用异常的环境中(例如 Google 内部),C++ 构造函数实际上必须成功,因为它没有办法向调用方报告失败。当然,你可以使用 abort(),但这会让整个程序崩溃,而这在生产代码中通常不可接受。

如果你的类初始化逻辑无法避免失败的可能性,一种常见做法是给类提供一个初始化方法(也叫 “init method”)。这个方法执行所有可能失败的初始化工作,并通过返回值表示失败。通常假设用户会在构造后立即调用这个方法,如果它失败,用户会立即销毁对象。然而,这些假设并不总是写在文档里,也不总是被遵守。用户很容易在初始化前、或者初始化失败后开始调用其他方法。有时类本身甚至鼓励这种行为,例如提供一些方法,让用户在初始化前配置对象,或者在初始化失败后从对象中读取错误。

这种设计会让你承诺维护一个至少有两个用户可见状态的类,而且通常是三个:已初始化、未初始化、初始化失败。让这种设计正常工作需要大量纪律:类的每个方法都必须说明可以在哪些状态下调用,用户也必须遵守这些规则。如果这种纪律松动,客户端开发者往往会写任何“刚好能工作”的代码,而不管你原本打算支持什么。事情发展到这一步时,可维护性会急剧下降,因为你的实现必须支持客户端已经开始依赖的各种初始化前方法调用组合。实际上,你的实现已经变成了接口。(见 Hyrum 定律。)

幸运的是,有一个简单替代方案没有这些缺点:提供一个工厂函数,由它创建并初始化类实例,然后通过指针或 absl::optional 返回(见 TotW #123),用 null 表示失败。下面是一个使用 unique_ptr<> 的玩具示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// foo.h
class Foo {
 public:
  // 工厂方法:创建并返回一个 Foo。
  // 失败时可能返回 null。
  static std::unique_ptr<Foo> Create();

  // Foo 不可拷贝。
  Foo(const Foo&) = delete;
  Foo& operator=(const Foo&) = delete;

 private:
  // 客户端不能直接调用构造函数。
  Foo();
};

// foo.c
std::unique_ptr<Foo> Foo::Create() {
  // 注意,因为 Foo 的构造函数是 private,所以这里必须使用 new。
  return absl::WrapUnique(new Foo());
}

在许多情况下,这种模式两全其美:工厂函数 Foo::Create() 像构造函数一样只暴露完全初始化的对象,但又像初始化方法一样可以表示失败。工厂函数的另一个优点是,它可以返回返回类型的任意子类实例(不过如果返回类型是 absl::optional,就做不到这一点)。这让你可以在不更新用户代码的情况下替换成另一种实现,甚至可以根据用户输入动态选择实现类。

这种方法的主要缺点是,它返回一个指向堆分配对象的指针,因此不太适合设计成在栈上工作的“类似值”的类。不过,这类类通常一开始就不需要复杂初始化。派生类构造函数需要初始化基类时,也不能使用工厂函数,所以在基类的 protected API 中有时仍然需要初始化方法。不过,public API 仍然可以使用工厂函数。

每周技巧 #36:新的 Join API

上一节

每周技巧 #45:避免标志,尤其是在库代码中

下一节


本节目录