𝚲

用类型表达业务规则

Designing with Types.

用户

case class User (
  name: String, 
  email: String, 
  phone: String
) 

注意看1,这是几乎所有业务系统都会定义的一个类,用户(User)。当然,实际应用中用户的属性要更多更复杂。这里姓名(name),邮箱(email)以及电话(phone)已经足够表达了。

二选一

系统的早期,需求文档里关于用户的描述,可能会有这么一条业务规则:

  • 用户的邮箱和电话二者至少要有一个;

为了体现这条规则,可能最容易实现方式如下:

object User:
  def emailOnly(name: String, email: String) = 
    new User(name, Some(email), None)
  
  def phoneOnly(name: String, phone: String) = 
    new User(name, None, Some(phone))
  
  def apply(name: String, email: String, phone: String) = 
    new User(name, Some(email), Some(phone))

case class User private (
  name: String, 
  email: Option[String], 
  phone: Option[String]
) 

这里 private 限制 class User构造器的可见性,以确保用户的实例化只能采用 object User 提供的工厂方法。2

联合类型(Union Type)

其实,更为优雅简洁的方法是:

enum Contact:
  case EmailOnly(value: String)
  case PhoneOnly(value: String)
  case EmailAndPhone(email: String, phone: String)

case class User(
  name: String, 
  contact: Contact
)

enum Contact就是联合类型,其字面含义就是,联系方式有三种情况,即:

  1. 只有邮箱
  2. 只有电话
  3. 二者皆有

相比较之前的版本,这在业务含义的表达上,更加显而易见了。

需求变更

大概率会有一天(通常不会让你等太久),你被告知系统需要支持用户添加备用的邮箱和电话。有了之前的改进,应对这样的变化并不难,就是需要机械的添加多出来的 case。只是这看似重复,但又有不同的组合,容易让人抠掉头发。

enum Contact:
  case EmailOnly(value: String)
  case PhoneOnly(value: String)
  case EmailAndPhone(email: String, phone: String)
  case EmailAndBackupEmailAndPhone(...)
  case EmailAndPhoneAndBackupPhone(...)
  case Email...(...)

其实,联系方式最基本的只有两种:邮箱和电话,其余则是二者组合的变种。由此可见,3

enum Contact:
  case Email(value: String)
  case Phone(value: String)
  case Multi(
    primary: Contact, 
    secondary: Contact,
    more: List[Contact]
  )

结语

编程语言的类型能力远比我们想象的强大,充分利用它们去表达业务(逻辑)规则,可以让编译器帮我们更早地发现问题。本文谈及的,仅仅只是九牛之一毛。然而,浮躁的环境让人疲于奔命,而放弃了主动发现、沉浸阅读、以及深度思考。希望我的短文能够帮你把它们都找回来,而不是在 卷不赢躺不平 之间来回摇摆。

Too busy to learn

本文是受「Designing with Types」4 系列文章的启发,而浓缩出来的。因此,非常推荐你去阅读原文,详见脚注中链接。同时,这里推荐一款浏览器的双语翻译插件「沉浸式翻译」5,方便提升阅读效率。我也是刚知道它,相见恨晚,这还要感谢「硬地骇客」6这当博客节目,也推荐订阅收听。Enjoy!

HardHacker Podcast

  1. 文中代码片段采用的是 Scala。↩︎

  2. 这里有意省略了邮箱和电话的有效性验证逻辑。↩︎

  3. 这也是 代数数据类型 在建模应用中的体现。↩︎

  4. https://fsharpforfunandprofit.com/series/designing-with-types/↩︎

  5. https://immersivetranslate.com/↩︎

  6. https://hardhacker.com/podcasts↩︎