专栏名称: 逸言
文学与软件,诗意地想念。
目录
相关文章推荐
OSC开源社区  ·  继V3之后,沐曦GPU再完成DeepSeek ... ·  昨天  
OSC开源社区  ·  Gitee邀您参与SBOM行业调研:共建可信 ... ·  2 天前  
程序员小灰  ·  这款AI编程工具,将会取代Cursor! ·  昨天  
程序猿  ·  清晰的、模块化的编码风格 ·  2 天前  
51好读  ›  专栏  ›  逸言

代数数据类型与领域建模

逸言  · 公众号  · 程序员  · 2019-05-25 08:16

正文

| 逸派胡言


本文是函数式编程思想与领域建模的第一部分,重点讲解代数数据类型与领域模型之间的关系。



函数范式


REA的Ken Scambler认为函数范式的主要特征为:模块化(Modularity),抽象化(Abstraction)和可组合(Composability)。这三个特征可以帮助我们编写简单的程序。


通常,为了降低系统的复杂度,都需要将系统分解为多个功能的组成部分,每个组成部分有着清晰的边界。模块化的编码范式需要支持实现者能够轻易地对模块进行替换,这就要求模块具有隔离性,避免在模块之间出现太多的纠缠。函数范式以“函数”为核心,作为模块化的重要组成部分。函数范式要求函数均为没有副作用的纯函数(pure function)。在推断每个函数的功能时,由于函数没有产生副作用,就可以不考虑该函数当前所处的上下文,形成清晰的隔离边界。这种相互隔离的纯函数使得模块化成为可能。


函数的抽象能力不言而喻,因为它本质上是一种将输入类型转换为输出类型的转换行为。任何一个函数都可以视为一种转换(transform),这是对行为的最高抽象,代表了类型(type)之间的某种动作。极端情况下,我们甚至不用考虑函数的名称和类型,只需要关注其数学本质:f(x) = y。其中,x是输入,y是输出,f就是极度抽象的函数。


函数范式领域模型的核心要素为代数数据类型(Algebraic Data Type, ADT)和纯函数。代数数据类型表达领域概念,纯函数表达领域行为。由于二者皆被定义为不变的、原子的,因此在类型的约束规则下可以对它们进行组合。可组合的特征使得函数范式建立的领域模型可以由简单到复杂,利用组合子来表现复杂的领域逻辑。

代数数据类型


代数数据类型借鉴了代数学中的概念,作为一种函数式数据结构,体现了函数范式的数学意义。通常,代数数据类型不包含任何行为。它利用和类型(Sum Type)来展示相同抽象概念的不同组合,使用积类型(Product Type)来展示同一个概念不同属性的组合。


和与积是代数中的概念,它们在函数范式中体现了类型的两种组合模式。和就是加,用以表达一种类型是它的所有子类型之和。例如表达时间单位的TimeUnit类型:


sealed trait TimeUnit

case object Days extends TimeUnit
case object Hours extends TimeUnit
case object Minutes extends TimeUnit
case object Seconds extends TimeUnit
case object MilliSeconds extends TimeUnit
case object MicroSeconds extends TimeUnit
case object NanoSeconds extends TimeUnit


在上述模型中,TimeUnit是对时间单位概念的一个抽象。定义为和类型,说明它的实例只能是以下的任意一种:Days、Hours、Minutes、Seconds、MilliSeconds、MicroSeconds或NanoSeconds。这是一种逻辑或的关系,用加号来表示:


type TimeUnit = Days + Hours + Minutes + Seconds + MilliSeconds + MicroSeconds + NanoSeconds


积类型体现了一个代数数据类型是其属性组合的笛卡尔积,例如一个员工类型:


case class Employee(number: String, name: String, email: String, onboardingDate: Date)


它表示Employee类型是(String, String, String, Date)组合的集合,也就是这四种数据类型的笛卡尔积,在类型语言中可以表达为:


type Employee = (String, String, String, Date)


也可以用乘号来表示这个类型的定义:


type Employee = String * String * String * Date


和类型和积类型的这一特点体现了代数数据类型的组合(combinable)特性。代数数据类型的这两种类型并非互斥的,有的代数数据类型既是和类型,又是积类型,例如银行的账户类型:


sealed trait Currency
case object RMB extends Currency
case object USD extends Currency
case object EUR extends Currency

case class Balance(amount: BigDecimal, currency: Currency)

sealed trait Account {
  def number: String
  def name: String
}

case class SavingsAccount(number: String, name: String, dateOfOpening: Date) extends Account
case class BilledAccount(number: String, name: String, dateOfOpening: Date, balance: Balance) extends Account


代码中的Currency被定义为和类型,Balance为积类型。Account首先是和类型,它的值要么是SavingsAccount,要么是BilledAccount;同时,每个类型的Account又是一个积类型。


代数数据类型与对象范式的抽象数据类型有着本质的区别。前者体现了数学计算的特性,具有不变性。使用Scala的case object或case class语法糖会帮助我们创建一个不可变的抽象。当我们创建了如下的账户对象时,它的值就已经确定,不可改变:


val today = Calendar.getInstance.getTime
val balance = Balance(10.0, RMB)
val account = BilledAccount("980130111110043", "Bruce Zhang", today, balance)


数据的不变性使得代码可以更好地支持并发,可以随意共享值而无需承受对可变状态的担忧。不可变数据是函数式编程中实践的重要原则之一,它可以与纯函数更好地结合。


代数数据类型既体现了领域概念的知识,同时还通过和类型和积类型定义了约束规则,从而建立了严格的抽象。例如类型组合(String, String, Date)是一种高度的抽象,但它却丢失了领域知识,因为它缺乏类型标签,如果采用积类型方式进行定义,则在抽象的同时,还约束了各自的类型。和类型在约束上更进了一步,它将变化建模在一个特定数据类型内部,并限制了类型的取值范围。和类型与积类型结合起来,与操作代数数据类型的函数放在一起,然后利用模式匹配来实现表达业务规则的领域行为。


我们以Robert Martin在《敏捷软件开发》一书中给出的薪资管理系统需求为例,利用函数范式的建模方式来说明代数数据类型的优势。需求描述如下:


公司雇员有三种类型。一种雇员是钟点工,系统会按照雇员记录中每小时报酬字段的值对他们进行支付。他们每天会提交工作时间卡,其中记录了日期以及工作小时数。如果他们每天工作超过8小时,超过部分会按照正常报酬的1.5倍进行支付。支付日期为每周五。

月薪制的雇员以月薪进行支付。每个月的最后一个工作日对他们进行支付。在雇员记录中有月薪字段。

销售人员会根据他们的销售情况支付一定数量的酬金(Commssion)。他们会提交销售凭条,其中记录了销售的日期和数量。在他们的雇员记录中有一个酬金报酬字段。每隔一周的周五对他们进行支付。


我们现在要计算公司雇员的薪资。从需求看,我们需要建立的领域模型是雇员,它是一个积类型。注意,需求虽然清晰地勾勒出三种类型的雇员,但实则它们的差异体现在收入的类型上,这种差异体现为和类型不同的值。于是,可以得到由如下代数数据类型呈现的领域模型:


// ADT定义,体现了领域概念
// Amount是一个积类型,Currency则为前面定义的和类型
calse class Amount(value: BigDecimal, currency: Currency) {
  // 实现了运算符重载,支持Amount的组合运算
  def +(that: Amount): Amount = {
    require(that.currency == currency)
    Amount(value + that.value, currency)
  }
  def *(times: BigDecimal): Amount = {
    Amount(value * times, currency)
  }
}

// 以下类型皆为积类型,分别体现了工作时间卡与销售凭条领域概念
case class TimeCard(startTime: Date, endTimeDate)
case class SalesReceipt(date: Date, amount: Amount)

// 支付周期是一个隐藏概念,不同类型的雇员支付周期不同
case class PayrollPeriod(startDate: Date, endDate: Date)

// Income的抽象表示成和类型与乘积类型的组合






请到「今天看啥」查看全文