用类型表达业务规则
Designing with Types.
用户
case class User (
: String,
name: String,
email: String
phone)
注意看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 (
: String,
name: Option[String],
email: Option[String]
phone)
这里 private
限制
class User
构造器的可见性,以确保用户的实例化只能采用
object User
提供的工厂方法。2
联合类型(Union Type)
其实,更为优雅简洁的方法是:
:
enum Contactcase EmailOnly(value: String)
case PhoneOnly(value: String)
case EmailAndPhone(email: String, phone: String)
case class User(
: String,
name: Contact
contact)
enum Contact
就是联合类型,其字面含义就是,联系方式有三种情况,即:
- 只有邮箱
- 只有电话
- 二者皆有
相比较之前的版本,这在业务含义的表达上,更加显而易见了。
需求变更
大概率会有一天(通常不会让你等太久),你被告知系统需要支持用户添加备用的邮箱和电话。有了之前的改进,应对这样的变化并不难,就是需要机械的添加多出来的
case
。只是这看似重复,但又有不同的组合,容易让人抠掉头发。
:
enum Contactcase EmailOnly(value: String)
case PhoneOnly(value: String)
case EmailAndPhone(email: String, phone: String)
case EmailAndBackupEmailAndPhone(...)
case EmailAndPhoneAndBackupPhone(...)
case Email...(...)
其实,联系方式最基本的只有两种:邮箱和电话,其余则是二者组合的变种。由此可见,3
:
enum Contactcase Email(value: String)
case Phone(value: String)
case Multi(
: Contact,
primary: Contact,
secondary: List[Contact]
more)
结语
编程语言的类型能力远比我们想象的强大,充分利用它们去表达业务(逻辑)规则,可以让编译器帮我们更早地发现问题。本文谈及的,仅仅只是九牛之一毛。然而,浮躁的环境让人疲于奔命,而放弃了主动发现、沉浸阅读、以及深度思考。希望我的短文能够帮你把它们都找回来,而不是在 卷不赢 与 躺不平 之间来回摇摆。
本文是受「Designing with Types」4 系列文章的启发,而浓缩出来的。因此,非常推荐你去阅读原文,详见脚注中链接。同时,这里推荐一款浏览器的双语翻译插件「沉浸式翻译」5,方便提升阅读效率。我也是刚知道它,相见恨晚,这还要感谢「硬地骇客」6这当博客节目,也推荐订阅收听。Enjoy!
文中代码片段采用的是 Scala。↩︎
这里有意省略了邮箱和电话的有效性验证逻辑。↩︎
这也是 代数数据类型 在建模应用中的体现。↩︎
https://fsharpforfunandprofit.com/series/designing-with-types/↩︎