博客 阅读时间 5 分钟

领域驱动设计、不可变性与 C++

分享本文
Domain-Driven Design, Immutability, and C++

编程中有一个简单而强大的概念,却一直被广泛忽视:不可变性(immutability)。

基本上,如果一个对象在创建之后其状态不再改变,它就是不可变的。相应地,如果一个类的实例都是不可变的,这个类就是不可变的。

支持使用不可变对象有一个重要的理由:它能极大简化并发编程。想一想:为什么编写正确的多线程代码如此困难?因为很难同步多个线程对资源(对象或其他操作系统资源)的访问。为什么同步这种访问很困难?因为很难保证多个线程对多个对象执行的多次读写操作之间不会发生竞态条件。如果不再有写操作呢?换句话说,如果线程所访问的对象的状态不再改变呢?那就不再需要同步了!

如果被引用对象的类没有公有字段、没有能改变其内部数据的方法,派生类的方法也无法改变其内部数据,那么这个类就是不可变的。而且由于值不能改变,在任何情况下都可以引用同一个对象,无需复制构造函数或赋值运算符。因此,建议把复制构造函数和赋值运算符设为私有、继承 boost::noncopyable,或者使用 C++11 的新特性“显式默认化和删除特殊成员函数”。

即使一个 C++ 类在设计时并未考虑不可变性,给其类型的变量加上 const 关键字也能奏效。不过,这种方案有一些缺点:

  • 您必须为每个希望不可变的变量声明都加上 const;一旦偶尔忘记,就可能在多线程环境中产生意想不到的缺陷。
  • 即使一个变量声明为 const,如果某些字段声明为 mutable,仍然可能在 const 方法内修改对象。在这种情况下,对象的状态仍可能被修改,这意味着对象是可变的。

例如,字符串(String)类在其他几种语言(如 C#、Java 和 D)中是出了名的不可变。字符串天然被广泛使用,让它们不可变尤其适合多线程环境。在 C++ 中,std::string 不是不可变的,所以替代方案是使用 const 关键字。

领域驱动设计与不可变性

领域驱动设计(DDD)是一种基于两个前提的软件设计方法:

  • 复杂领域的设计应当基于一个模型;
  • 对大多数软件项目而言,主要关注点应放在领域和领域逻辑上(而不是用来实现系统的特定技术)。

换句话说,DDD 的核心是模型,开始开发时最先要做的事之一就是定义模型。您创建的模型与设计应当相互塑造。模型应当表达业务领域的知识。

一般来说,线程之间共享的数据都涉及模型实体,让这些实体不可变有助于消除副作用。Wes Dyer 对此说得再好不过,我直接引用他的话:

We all know that generally, it is not a good idea to use global variables. This is basically the extreme of exposing side-effects (the global scope). Many of the programmers who don’t use global variables don’t realize that the same principles apply to fields, properties, parameters, and variables on a more limited scale: don’t mutate them unless you have a good reason.(…)

提高一个单元可靠性的一种方法是消除副作用。这让单元的组合与集成容易得多、也健壮得多。由于它们没有副作用,无论在什么环境下,它们的行为都始终如一。这被称为引用透明性(referential transparency)。

不可变类的另一个好处是,它们不可能违反里氏替换原则(LSP)。以下是引自维基百科页面的 LSP 定义:

Liskov’s notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).

对于不可变对象,状态不能改变,因此其属性也无法被更改。

在 C++ 源代码中检测不可变类

不可变性是可以在编译期强制的特性——换句话说,可以由静态分析工具来强制。CQLinq是静态分析工具CppDepend自带的查询语言,它提供了一个适用于类型的 IsImmutable 条件。想知道您的代码库中哪些类型是不可变的:

from t in Types where t.IsImmutable select t

除了类型上的 IsImmutable 条件,我们还可以使用两个有趣的条件:

ChangesObjectState 和 ChangesTypeState。顾名思义,ChangesObjectState 匹配对其类的实例字段赋值的方法,而 ChangesTypeState 匹配对其类的静态字段赋值的方法。

ChangesObjectState 条件匹配对其父类型的实例字段赋值的方法。

ChangesTypeState 条件匹配对其父类型的静态字段赋值的方法。

这两个条件让识别可能改变程序状态的方法——换句话说,造成副作用的方法——变得轻而易举。

不造成副作用的方法通常被称为纯方法(pure method)。要用 CQLinq 匹配纯方法,可以这样写:

from m in Methods

where !m. ChangesObjectState && !m. ChangesTypeState && !m.IsConstructor

select m

结语

消除副作用让单元的组合与集成容易得多、也健壮得多,并简化多线程编程。不可变类和纯方法是消除副作用的关键。

分享本文