201706 DDD DSL Design War Measurement System - xiaoxianfaye/Courses GitHub Wiki

1 Problem

需求一

实现一个长度(Length)库。通过这个库,用户可以以Mile为单位来表示一个长度,精度为1 Mile。可以对比两个长度的相等性:

3 Mile == 3 Mile
3 Mile != 2 Mile
3 Mile != 4 Mile

需求二

用户除了可以使用Mile为单位来表示长度之外,还可以使用Yard为单位来表示长度。

  • 当以Mile为单位来表示一个长度时,精度为1 Mile;
  • 当以Yard为单位来表示一个长度时,精度为1 Yard。

可以对比任意两个长度的相等性:

1 Mile == 1760 Yard
3 Yard == 3 Yard 
1 Mile != 1761 Yard
3 Yard != 4 Yard

需求三

增加两个新的长度单位:Feet和Inch,用户可以使用它们为单位来表现一个长度。

  • 当以Feet为单位来表示一个长度时,精度为1 Feet;
  • 当以Inch为单位来表示一个长度时,精度为1 Inch。

可以对比任意两个长度的相等性:

1 Yard == 3 Feet
1 Feet == 12 Inch

需求四

这套库发布之后,用户只能使用既有单位(Mile、Yard、Feet、Inch),而不应该有能力创建新的单位。以避免用户由于某些原因创建一些现实中不存在的单位,从而让系统变得不可理解。

需求五

任意两个长度可以进行加法运算。例如:

13 Inch + 11 Inch = 2 Feet
3 Feet + 2 Yard = 3 Yard

需求六

实现一个容量(Volume)库。通过这个库,用户可以使用TSP(茶匙)、TBSP(汤匙)、OZ(盎司)为单位来表示一个容量。

  • 当以TBSP为单位来表示一个容量时,精度为1 TBSP;
  • 当以TSP为单位来表示一个容量时,精度为1 TSP;
  • 当以OZ为单位来表示一个容量时,精度为1 OZ。

可以对比任意两个容量的相等性:

1 TBSP = 3 TSP
1 OZ = 2 TBSP

只允许用户使用现有的三个容量单位来表示容量。两个容量可以相加。

需求七

基于调试等目的需要,可以将一个长度对象输出到屏幕上。输入格式的规则如下:

  • 如果一个对象的“基准单位数量”在一个更大的单位上的倍数非0,则显示此对象在此单位上的倍数,以及此单位的名字;
  • 如果一个对象的“基准单位数量”在一个更大的单位上的倍数为0,则无须显示此对象在此单位上的倍数,以及此单位的名字;
  • 如果余数在一个较小的单位上倍数非0,则显示此对象在此单位上的倍数,以及此单位的名字;
  • 如果余数在一个较小的单位上倍数为0,则无须显示此对象在此单位上的倍数,以及此单位的名字;
  • 如果一个对象的“基准单位数量”为0,则显示0,以及基准单位的名字;
  • 如果存在多个“数量+单位”组合,则按照单位大小,从左向右排列;
  • 数量和单位之间,由一个空格分开;
  • “数量+单位”之间由一个空格分开。

需求八

增加一种新的长度对象输出格式,以Inch为单位输出任何Length对象。数量和单位之间以一个空格分隔。

2 Showcase & Discuss

请学员展示自己之前的设计思路和实现方式,大家可以互相点评、讨论。以学员为主,讲师为辅。

3 Analysis

分析:定义清楚问题是什么。

  • 需求一到需求五是英制长度计量系统的相关需求,要实现长度的相等性和加法运算;
  • 需求六是英制容量计量系统的相关需求,要实现容量的相等性和加法运算;
  • 需求七是计量系统的规格化表达需求;
  • 需求八是计量系统的基准化表达需求。

4 Design

设计:问题分析清楚以后,提出解决问题的逻辑框架。

4.1 Thinking with Tiny Steps

4.1.1 Quantity

1 Mile, 2 Yard, 3 Feet, 4 Inch, …
1 Mile 2 Yard, 1765 Yard 40 Inch, …
1 Mile 2 Yard 3 Feet 4 Inch, …

Q:What are these?
Length in Imperial Length Measurement System (英制长度计量系统中的长度)

1 OZ 2 TBSP 3 TSP,…
15 TSP,…

Q:What are these?
Volume in Imperial Volume Measurement System (英制容量计量系统中的容量)

Q:Can we give them an abstract name?
Quantity in Measurement System (计量系统中的数量)

既然是数量,我们就可以比较它们的大小(Equality),对它们进行加、减运算(Add)。

4.1.2 Equality

4.1.2.1 Quiz

以英制长度计量系统为例 (In Imperial Length Measurement System),先来一个小测验(Quiz)。

这里有一个容易犯错的地方:单位层级完全相同的情况下,对应层级单位的量值相等,一定相等;对应层级单位的量值不同,不一定不相等,因为“进位”的缘故。

Why? How? 仔细体会一下大脑的计算过程,慢下来。

Q1
3 Mile == 3 Mile ?  √
3 Yard == 4 Yard ?  ×
  • 特点:相同层级、一级单位。
  • 方法:两个长度的一级单位完全相同,且无需进位处理,直接判断对应层级单位的量值,量值相等则相等,反之则不等。
Q2
3 Yard 4 Feet == 3 Yard 4 Feet ?    √
3 Yard 4 Feet == 4 Yard 1 Feet ?    √
  • 特点:相同层级、多级单位、可以进位。
  • 方法:两个长度的多级单位完全相同,需要进位处理。有两种方法。

方法一:先自右向左进行进位处理,再判断对应层级单位的量值,量值相等则相等,反之则不等。

3 Yard 4 Feet -> 4 Yard 1 Feet

方法二:先自左向右换算成较小层级的单位,再判断较小层级单位的量值,量值相等则相等,反之则不等。

3 Yard 4 Feet -> 13 Feet
4 Yard 1 Feet -> 13 Feet
3 Yard 4 Feet == 4 Yard 1 Feet
Q3
1 Mile 2 Yard 3 Feet 4 Inch == 1 Mile 2 Yard 3 Feet 4 Inch ?    √
1 Mile 2 Yard 3 Feet 4 Inch == 1 Mile 3 Yard 0 Feet 4 Inch ?    √
  • 特点:相同的、完整的层级单位、可以进位。
  • 方法:两个长度都是完整的单位层级,需要进位处理。两种方法同上,只是方法二中的较小层级单位略有不同,这里就是基准层级单位(最小层级单位)。

方法一:

1 Mile 2 Yard 3 Feet 4 Inch -> 1 Mile 3 Yard 0 Feet 4 Inch

方法二:

1 Mile 2 Yard 3 Feet 4 Inch 
-> (1 * 1760 * 3 * 12) Inch + (2 * 3 * 12) Inch + (3 * 12) Inch + 4 Inch 
-> 63472 Inch

1 Mile 3 Yard 0 Feet 4 Inch 
-> (1 * 1760 * 3 * 12) Inch + (3 * 3 * 12) Inch + (0 * 12) Inch + 4 Inch 
-> 63472 Inch

以上都是单位层级相同的情况。下面再看一下单位层级不同的情况。

Q4
1 Mile 2 Yard == 1762 Yard ?    √
1 Mile 2 Yard == 2 Mile ?       ×
  • 特点:不同层级但未包含基准层级、多个单位。
  • 方法:有两种方法。

方法一:先按照较小层级的单位对齐单位层级,再进行自右向左的进位处理,然后再判断对应层级单位的量值,量值相等则相等,反之则不等。

1762 Yard -> 0 Mile 1762 Yard -> 1 Mile 2 Yard

方法二:先自左向右换算成较小层级的单位,再判断较小层级单位的量值,量值相等则相等,反之则不等。

1 Mile 2 Yard -> (1 * 1760) Yard + 2 Yard -> 1762 Yard
Q5
1 Mile 2 Yard 3 Feet 4 Inch == 63472 Inch ?     √
1765 Yard 40 Inch == 1 Mile 6 Yard 4 Inch ?     √
  • 特点:不同层级且包含基准层级、多个单位。
  • 方法:两种方法同上,只是方法二中的较小层级单位略有不同,这里就是基准层级单位。

方法一:

1 Mile 2 Yard 3 Feet 4 Inch -> 1 Mile 3 Yard 0 Feet 4 Inch

63472 Inch -> 0 Mile 0 Yard 0 Feet 63472 Inch -> 1 Mile 3 Yard 0 Feet 4 Inch

1765 Yard 40 Inch -> 0 Mile 1765 Yard 0 Feet 40 Inch -> 1 Mile 6 Yard 0 Feet 4 Inch

1 Mile 6 Yard 4 Inch -> 1 Mile 6 Yard 0 Feet 4 Inch

方法二:

1 Mile 2 Yard 3 Feet 4 Inch 
-> (1 * 1760 * 3 * 12) Inch + (2 * 3 * 12) Inch + (3 * 12) Inch + 4 Inch 
-> 63472 Inch

1765 Yard 40 Inch -> (1765 * 3 * 12) Inch + 40 Inch -> 63580 Inch

1 Mile 6 Yard 4 Inch 
-> (1 * 1760 * 3 * 12) Inch + (6 * 3 * 12) Inch + 4 Inch 
-> 63580 Inch
4.1.2.2 Equaling Process

Can we unify and simplify this process?

方法一:先按照基准层级的单位对齐单位层级,再进行自右向左的进位处理,然后再判断对应层级单位的量值,量值相等则相等,反之则不等。

方法二:先自左向右换算成基准层级的单位,再判断基准层级单位的量值,量值相等则相等,反之则不等。

这些方法不仅适用于英制长度计量系统,而且也适用于英制容量计量系统、甚至其它计量系统。

4.1.3 Something hidden before is coming up

Conversion of Units of Measurement System (计量系统的单位换算体系)

每个计量系统都有自己的单位换算体系。例如:

  • Imperial Length Measurement System
System Units:Mile,Yard,Feet,Inch
Conversion of Units:1 Mile = 1760 Yard,1 Yard = 3 Feet,1 Feet = 12 Inch
  • Imperial Volume Measurement System
System Units:OZ,TBSP,TSP
Conversion of Units:1 OZ = 2 TBSP,1 TBSP = 3 TSP

4.1.4 Add

4.1.4.1 Quiz

以英制长度计量系统为例 (In Imperial Length Measurement System),再来一个小测验(Quiz)。

Why? How? 仔细体会一下大脑的计算过程,慢下来。

Q1
1 Mile + 2 Mile = ? 
3 Feet + 4 Feet = ?
1 Yard 1 Feet + 2 Yard 2 Feet = ?
1 Mile 2 Yard 1 Feet 13 Inch + 1 Mile 2 Yard 1 Feet 11 Inch = ?
  • 特点:相同层级、一级或多级单位,可以进位。
  • 方法:在单位层级相同的情况下,先将对应层级单位的量值相加,再进行自右向左的进位处理。
1 Mile + 2 Mile = 3 Mile

3 Feet + 4 Feet = 7 Feet -> 2 Yard 1 Feet

1 Yard 1 Feet + 2 Yard 2 Feet = 3 Yard 3 Feet -> 4 Yard

1 Mile 2 Yard 1 Feet 13 Inch + 1 Mile 2 Yard 1 Feet 11 Inch 
= 2 Mile 4 Yard 2 Feet 24 Inch 
-> 2 Mile 5 Yard 1 Feet

以上都是单位层级相同的情况。下面再看一下单位层级不同的情况。

Q2
1 Mile + 2 Yard = ? 
3 Feet + 2 Yard = ? 
  • 特点:不同层级但未包含基准层级、多个单位。
  • 方法:有两种方法。

方法一:先按照较小层级的单位对齐单位层级,再将对应层级单位的量值相加,然后再进行自右向左的进位处理。

1 Mile -> 1 Mile 0 Yard
2 Yard -> 0 Mile 2 Yard
1 Mile + 2 Yard = 1 Mile 0 Yard + 0 Mile 2 Yard = 1 Mile 2 Yard

3 Feet -> 0 Yard 3 Feet
2 Yard -> 2 Yard 0 Feet
3 Feet + 2 Yard = 0 Yard 3 Feet + 2 Yard 0 Feet = 2 Yard 3 Feet -> 3 Yard

方法二:先自左向右换算成较小层级的单位,再将较小层级单位的量值相加,然后再进行自右向左的进位处理。

1 Mile + 2 Yard = 1760 Yard + 2 Yard = 1762 Yard -> 1 Mile 2 Yard
3 Feet + 2 Yard = 3 Feet + 6 Feet = 9 Feet -> 3 Yard
Q3
1 Mile 3 Feet + 2 Yard 4 Inch ?
  • 特点:不同层级且包含基准层级、多个单位。
  • 方法:两种方法同上,只是方法二中的较小层级单位略有不同,这里就是基准层级单位。

方法一:

1 Mile 3 Feet + 2 Yard 4 Inch 
= 1 Mile 0 Yard 3 Feet 0 Inch + 0 Mile 2 Yard 0 Feet 4 Inch 
= 1 Mile 2 Yard 3 Feet 4 Inch -> 1 Mile 3 Yard 4 Inch

方法二:

1 Mile 3 Feet + 2 Yard 4 Inch = 63396 Inch + 76 Inch = 63472 Inch = 1 Mile 3 Yard 4 Inch
4.1.4.2 Adding Process

Can we unify and simplify this process?

方法一:先按照基准层级的单位对齐单位层级,再将对应层级单位的量值相加,然后再进行自右向左的进位处理。

方法二:先自左向右换算成基准层级的单位,再将基准层级单位的量值相加,然后再进行自右向左的进位处理。

这些方法不仅适用于英制长度计量系统,而且也适用于英制容量计量系统、甚至其它计量系统。

4.2 Core Concepts

经过上述的思考,问题领域中的核心概念如下图所示:

core concepts imperial length

core concepts imperial volume

上面两张图可以“抽象”为:

core concepts imperial volume

核心概念包括:

  • Quantity (数量)
  • Conversion of Units (单位换算体系)
  • 对Quantity的操作:
    • equal (相等)、add (相加)
    • normalize (规格化)、base (基准化)

其中equal和add来自于外部功能需求,normalize和base是为了满足外部功能需求而抽取的内部概念。这里的base当作动词用。

4.3 Semantics

设计的第一步:找到合适的语义。

如何找到合适的语义呢?

我们在做领域驱动设计时,首先考虑是否能将问题领域映射到一个熟悉的同构领域。如果能,就可以借用那个领域的机制来表达问题领域的概念,而不是重新发明。如果能找到这样一种同构领域,应该是一个最好的结果,如果实在找不到,再自己发明。但一般来说,最终一定能在数学层面上找到一个同构领域,有可能只是没找到而已。

4.3.1 What does Quantity mean?

In Imperial Length Measurement System,
    1 Mile 2 Yard 3 Feet 4 Inch
    1 Mile 2 Yard
    3 Yard 6 Inch

In Imperial Volume Measurement System,
    1 OZ 2 TBSP 3 TSP
    15 TSP

Quantity可以映射为行向量(Row Vector)。行向量的维数是计量系统的单位层级数。行向量的分量(Component)自左向右对应的单位层级从大到小。

虽然向量在数学领域有严格的定义,但是在这里我们可以给出一个最直接的定义:把数值罗列起来就是向量。横着罗列就是行向量,竖着罗列就是列向量。

1 Mile 2 Yard 3 Feet 4 Inch -> (1, 2, 3, 4)
1 Mile 2 Yard -> (1, 2, 0, 0)
3 Yard 6 Inch -> (0, 3, 0, 6)

1 OZ 2 TBSP 3 TSP -> (1, 2, 3)
15 TSP -> (0, 0, 15)
1 OZ 2 TSP -> (1, 0, 2)

后续equal、add、normalize、base等操作的语义都基于向量化的Quantity定义。向量化的Quantity以下简称为Quantity向量。

4.3.2 What does Conversion of Units mean?

In Imperial Length Measurement System,
    1 Mile = 1760 Yard, 1 Yard = 3 Feet, 1 Feet = 12 Inch

In Imperial Volume Measurement System,
    1 OZ = 2 TBSP, 1 TBSP = 3 TSP

计量系统的单位换算体系可以分成两个部分:一系列转换因子(Factor)和一系列单位(Unit)。

Factors可以映射为列向量(Column Vector)。列向量的维数设计为计量系统的单位层级数。列向量的分量自上向下对应的单位层级从大到小。

Factors有两种计算方法:

  • 步进式(Step-Factors)
  • 基准式(Base-Factors)
1 Mile = 1760 Yard, 1 Yard = 3 Feet, 1 Feet = 12 Inch ->
    Step-Factors: (0x7fffffff, 1760, 3, 12)T
    Base-Factors: (1760 * 3 * 12, 3 * 12, 12, 1)T

1 OZ = 2 TBSP, 1 TBSP = 3 TSP -> 
    Step-Factors: (0x7fffffff, 2, 3)T
    Base-Factors: (2 * 3, 3, 1)T

T: Transposed

4.3.3 What does Base mean?

对Quantity向量进行**base(基准化)**操作是指:根据Quantity所属计量系统的单位换算体系,将Quantity向量的分量自左向右(从最大单位向最小单位)换算到最小单位(基准单位)的过程。

对Quantity向量进行base操作可以映射为Quantity行向量与Base-Factors列向量的点积

向量的点积(Dot Product,也叫内积)公式为:

vector dot product

对“1 Mile 2 Yard 3 Feet 4 Inch”进行base操作:

Quantity行向量: (1, 2, 3, 4)
Base-Factors列向量: (1760 * 3 * 12, 3 * 12, 12, 1)T
(1, 2, 3, 4)·(1760 * 3 * 12, 3 * 12, 12, 1)T
= 1 * (1760 * 3 * 12) + 2 * (3 * 12) + 3 * 12 + 4 * 1
= 63472

对“1 Mile 2 Yard 3 Feet 4 Inch”进行base操作的结果是63472。

对“1 OZ 0 TBSP 1 TSP”进行base操作:

Quantity行向量: (1, 0, 1)
Base-Factors列向量: (2 * 3, 3, 1)T
(1, 0, 1)·(2 * 3, 3, 1)T
= 1 * (2 * 3) + 0 * 3 + 1 * 1
= 7

对“1 OZ 0 TBSP 1 TSP”进行base操作的结果是7。

4.3.4 What does Normalize mean?

对Quantity向量进行**normalize(规格化)**操作是指:根据Quantity所属计量系统的单位换算体系,将Quantity向量的分量自右向左(从最小单位向最大单位)逐级“进位”的过程。

对Quantity向量进行normalize操作有两种映射方法

  • 方法一:映射为Quantity向量与Step-Factors向量的除法(divide)

在数学领域中,向量的除法是没有定义的。我们可以根据问题领域的需要增加向量除法这个语义,只要和已有的语义不矛盾就可以。

向量除法的计算要点:不区分行向量和列向量。以两个行向量相除为例。自右向左,将两个向量的分量逐个相除,将余数自右向左存放到结果向量中,将商作为进位(Carry)和紧邻的左边的分量相加,作为被除数继续向左计算。

使用方法一对“24 Inch”进行normalize操作:

Quantity向量: (0, 0, 0, 24)
Step-Factors向量: (0x7fffffff, 1760, 3, 12)
 (0, 0, 0, 24) / (0x7fffffff, 1760, 3, 12) = (0, 0, 2, 0)

对“24 Inch”进行normalize(规格化)操作的结果是(0, 0, 2, 0)。

使用方法一对“2 TBSP 1 TSP”进行normalize操作:

Quantity向量: (0, 2, 1)
Step-Factors向量: (0x7fffffff, 2, 3)
 (0, 2, 1) / (0x7fffffff, 2, 3) = (1, 0, 1)

对“2 TBSP 1 TSP”进行normalize操作的结果是(1, 0, 1)。

  • 方法二:映射为Quantity向量的基准值(Value)与Base-Factors向量的点除(Dot Divide)

Quantity向量的基准值是指Quantity向量经过base操作以后得到的数值。

在数学领域中,向量的点除是没有定义的。我们可以根据问题领域的需要增加点除这个语义,只要和已有的语义不矛盾就可以。

向量点除的计算要点:自左向右,用数值逐个除以向量的分量,将商自左向右存放到结果向量中,将余数作为被除数继续向右计算。

使用方法二对“24 Inch”进行normalize操作:

Quantity向量: (0, 0, 0, 24),基准值为24
Base-Factors向量: (1760 * 3 * 12, 3 * 12, 12, 1)
24 ·/ (1760 * 3 * 12, 3 * 12, 12, 1) = (0, 0, 2, 0)

对“24 Inch”进行normalize操作的结果是(0, 0, 2, 0)。

使用方法二对“2 TBSP 1 TSP”进行normalize操作:

Quantity向量: (0, 2, 1),基准值为7
Base-Factors向量: (2 * 3, 3, 1)
7 ·/ (2 * 3, 3, 1) = (1, 0, 1)

对“2 TBSP 1 TSP”进行normalize操作的结果是(1, 0, 1)。

4.3.5 What does Equal mean?

两种方法判断两个Quantity向量的相等性。

  • 方法一:映射为判断Quantity向量normalize(规格化)后的向量的相等性

使用方法一判断“1765 Yard 40 Inch”和“1 Mile 5 Yard 3 Feet 4 Inch”的相等性:

Quantity向量1: (0, 1765, 0, 40), normalize后的向量为(1, 6, 0, 4)
Quantity向量2: (1, 5, 3, 4), normalize后的向量为(1, 6, 0, 4)
∵ (1, 6, 0, 4) == (1, 6, 0, 4)
∴ 1765 Yard 40 Inch == 1 Mile 5 Yard 3 Feet 4 Inch

使用方法一判断“2 TBSP 1 TSP”和“1 OZ 1 TSP”的相等性:

Quantity向量1: (0, 2, 1), normalize后的向量为(1, 0, 1)
Quantity向量2: (1, 0, 1), normalize后的向量还是(1, 0, 1)
∵ (1, 0, 1) == (1, 0, 1)
∴ 2 TBSP 1 TSP == 1 OZ 1 TSP
  • 方法二:映射为判断Quantity向量base(基准化)后的基准值(Value)的相等性

映射为判断Quantity向量base(基准化)后的基准值(Value)的相等性。

使用方法二判断“1765 Yard 40 Inch”和“1 Mile 5 Yard 3 Feet 4 Inch”的相等性:

Quantity向量1: (0, 1765, 0, 40), base后的基准值为63580
Quantity向量2: (1, 5, 3, 4), base后的基准值为63580
∵ 63580 == 63580
∴ 1765 Yard 40 Inch == 1 Mile 5 Yard 3 Feet 4 Inch

使用方法二判断“2 TBSP 1 TSP”和“1 OZ 1 TSP” 的相等性:

Quantity向量1: (0, 2, 1), base后的基准值为7
Quantity向量2: (1, 0, 1), base后的基准值为7
∵ 7 == 7
∴ 2 TBSP 1 TSP == 1 OZ 1 TSP

4.3.6 What does Add mean?

两种方法对两个Quantity向量进行相加操作。

  • 方法一:映射为两个向量相加,并将相加后的向量normalize(规格化)

向量的加法公式为:

vector add

使用方法一对“13 Inch”和“11 Inch”进行相加操作:

Quantity向量1: (0, 0, 0, 13)
Quantity向量2: (0, 0, 0, 11)
(0, 0, 0, 13) + (0, 0, 0, 11) = (0, 0, 0, 24), normalize后的向量为(0, 0, 2, 0)
13 Inch + 11 Inch = 2 Feet

使用方法一对“1 OZ”和“3 TBSP 3 TSP”进行相加操作:

Quantity向量1: (1, 0, 0)
Quantity向量2: (0, 3, 3)
(1, 0, 0) + (0, 3, 3) = (1, 3, 3), normalize后的向量为(3, 0, 0)
1 OZ + 3 TBSP 3 TSP = 3 OZ
  • 方法二:映射为Quantity向量base(基准化)后的基准值(Value)相加,并将相加后的值normalize(规格化)

使用方法二对“13 Inch”和“11 Inch”进行相加操作:

Quantity向量1: (0, 0, 0, 13), base后的基准值为13
Quantity向量2: (0, 0, 0, 11), base后的基准值为11
13 + 11 = 24, normalize后的向量为(0, 0, 2, 0)
13 Inch + 11 Inch = 2 Feet

使用方法二对“1 OZ”和“3 TBSP 3 TSP”进行相加操作:

Quantity向量1: (1, 0, 0), base后的基准值为6
Quantity向量2: (0, 3, 3), base后的基准值为12
6 + 12 = 18, normalize后的向量为(3, 0, 0)
1 OZ + 3 TBSP 3 TSP = 3 OZ

4.3.7 Core Semantics Domain

这样,我们就可以把计量系统问题领域映射到“向量空间”的语义领域,或者映射到“向量空间+算术运算”的语义领域。

core semantics domain vector

core semantics domain vector arithmetic

4.4 Formalization

设计的第二步:形式化地表达语义。

语义(Semantics)需要形式化地表达(Formalize),否则没法可计算化。一旦要表达,就必须选择一种记法,用符号来表达。可能有很多种表达方式,选择的记法一定要是可计算的,可以教给计算机去做的,而且语义层次还要高。注意先不要考虑如何实现。

4.4.1 Conversion of Units

System Units: <u1, …, un> System Unit Level: <ul1, …, uln>

Both <*u1, …, un*> and <*ul1, …, uln*> are not vectors, 
and not belong to the core semantics domain. 
The elements of <*u1, …, un*> are mapped to the elements of <*ul1, …, uln*> one by one.
The n is the number of system unit levels.
uli > ulj   (i < j, 1 ≤ i, j ≤ n)

Step-Factors Vector: (0x7fffffff, sfvc2,… , sfvcn)T

The n is the number of system unit levels.
The first component of the step-factors is 0x7fffffff.
The sfvci is the ith component of the step-factors vector. 
The components are mapped to the elements of <ul1, …, uln> one by one.

Base-Factors Vector: (bfvc1, … , bfvcn-1, 1)T

The n is the number of system unit levels.
The last component of the base-factors is 1.
The bfvci is the ith component of the base-factors vector.
The components are mapped to the elements of <ul1, …, uln> one by one.

4.4.2 Quantity Vector

Quantity Vector: (qvc1, qvc2, …, qvcn)

The n is the number of system unit levels.
The qvci is the ith component of the quantity vector.
The components are mapped to the elements of <ul1, …, uln> one by one.

4.4.3 Base

(qvc1, qvc2, …, qvcn)·(bfvc1, … , bfvcn-1, 1)T

4.4.4 Normalize

Vector Space: (qvc1, qvc2, …, qvcn) / (0x7fffffff, sfvc2,… , sfvcn)

Vector Space + Arithmetic: base (qvc1, qvc2, …, qvcn) ·/ (bfvc1, … , bfvcn-1, 1)

4.4.5 Equal

Quantity Vector 1: (qvc1, qvc2, …, qvcn)1

Quantity Vector 2: (qvc1, qvc2, …, qvcn)2

Vector Space: normalize ((qvc1, qvc2, …, qvcn)1) == normalize ((qvc1, qvc2, …, qvcn)2)

Vector Space + Arithmetic: base ((qvc1, qvc2, …, qvcn)1) == base ((qvc1, qvc2, …, qvcn)2)

4.4.6 Add

Quantity Vector 1: (qvc1, qvc2, …, qvcn)1

Quantity Vector 2: (qvc1, qvc2, …, qvcn)2

Vector Space: normalize ((qvc1, qvc2, …, qvcn)1 + (qvc1, qvc2, …, qvcn)2)

Vector Space + Arithmetic: normalize (base (qvc1, qvc2, …, qvcn)1 + base (qvc1, qvc2, …, qvcn)2)

4.5 Measurement System (Formalized Semantics)

formalized semantics vector

formalized semantics vector arithmetic

做个小结:

  • 以上都是语义层面的,不涉及到任何实现,也是系统的核心。

  • 可以看到,通过这样一种方式,不用考虑实现,只需要考虑语义。将计量系统问题领域映射到了“向量空间”或者“向量空间+算术运算”的数学领域。用“向量空间”或者“向量空间+算术运算”的语言形式化地表达问题领域的整个语义模型。还可以根据问题领域的需要增加映射领域中没有的语义,只要和已有的语义不矛盾就可以。

  • 两个步骤:

  1. 找到合适的同构语义领域;
  2. 用同构语义领域的语言精确地形式化地表达语义。 这就是“建模”的本质。
  • 思考一下,这两种映射有何异同?

两种映射都比OO好太多。例如,如果想新增一个单位,两种映射都只需要改一下“Conversion of Units”即可,可如果是OO呢,……。根本原因是两种映射对应的计算模型的语义与计量系统问题领域的根本需求相匹配,而OO则不然。

比较两种映射,映射到“向量空间”的语义层次更高一些。在实现映射到“向量空间和算术运算”语义的时候,需要在两个层次之间上上下下。比如,我们的大脑在计算“1 Mile 2 Yard + 1 Mile 2 Yard ”可能会直接得出 “2 Mile 4 Yard”的结果,而不会说先基准化为Inch,再相加,再规格化。

4.6 Measure System UI

计量系统的核心计算模型已经有了,现在再来看一下计量系统的外围部分,即计量系统和用户的交互部分Measure System UI (UI:User Interface)

我们可以为用户提供一套小语言

用户可以这样告诉我们计量系统的名字单位换算体系

Imperial Length
Mile 1760 Yard 3 Feet 12 Inch

Imperial Volume
OZ 2 TBSP 3 TSP

说明:

  • 计量系统的名字目前暂时没有用到,只是作为一个系统标识。如果后续继续开发跨计量系统的功能时就会用到了。
  • 单位换算体系的格式特点是:按层级从高到低、自左向右排列单位,在单位排列间隔中放置换算因子,中间均以空格分隔。
  • 前后或者中间不小心多敲了空格没有关系。

用户可以这样告诉我们计量系统中的数量

1 Mile 6 Yard 4 Inch
2 OZ 1 TBSP 1 TSP

说明:

  • 数量的格式特点是:按层级从高到低、自左向右排列单位,单位的左边放置该单位的量值,中间均以空格分隔。
  • 前后或者中间不小心多敲了空格没有关系。

所以,Measure System UI需要提供parseformat功能。

  • parse:负责将用户输入的小语言解析成系统核心内部数据结构。
    • 将用户输入的“单位换算体系”解析成System Units和Step-Factors Vector
    • 将用户输入的“数量”解析成Quantity Vector
  • format:负责将系统核心数据结构格式化为满足需求、便于用户理解的小语言。
    • 将Quantity Vector格式化
    • 将Quantity Vector基准格式化
  • System Units只需要保存在Measure System UI中。在Measure System中只需要Step-Factors和Base-Factors。可以将System Units理解为“Labels”,只用于显示,不参与核心计算。

4.7 Layered System

计量系统问题领域 -> “向量空间”语义领域。

layered system vector

计量系统问题领域 -> “向量空间+算术运算”语义领域。

layered system vector arithmetic

因为系统分了层,所以即使两种映射不一样,Measurement System UI和User都不会受到任何影响,一模一样。

时序图。

measurement system seq

measurement system seq vector

measurement system seq vector arithmetic

5 Implementation

实现:选择实现技术把逻辑框架的软件模型实现出来。

在课堂上,我们一起来实现将计量系统问题领域映射到“向量空间”语义领域。将计量系统问题领域映射到“向量空间+算术运算”语义领域实现留作课后作业。

layered system vector

按照系统层次,从下往上实现。这里选用Java语言实现并详细讲解实现过程。其他语言的实现过程是一样的,只是落实到实现语言层面的语言细节略有不同。

代码包路径:fayelab.ddd.measure.vector.original

5.1 Vector Space: VecOp

  • 有的语言已经提供了向量操作的库,比如Python的numpy包。Java语言没有提供,需要我们打造自己的工具箱。
  • 向量的英文应该是Vector,但这样命名容易和java.util.Vector混淆,所以命名为Vec。
  • 用Java语言实现的话,Vector用List数据结构是比较合适的。
  • VecOp作为向量操作的工具类,应该是需要考虑参数校验的,但因为不是本次课程的重点,这里就不实现了。
  • 虽然两种映射需要的向量操作不太一样,但都可以在VecOp中统一实现。

5.1.1 equal

VecOpTest.java

package fayelab.ddd.measure.vector.original;

public class VecOpTest extends TestCase
{
    public void test_equal()
    {
        assertTrue(equal(asList(1, 2, 3), asList(1, 2, 3)));
        assertTrue(equal(asList(), asList()));
        assertFalse(equal(asList(1, 2), asList(1, 2, 3)));
        assertFalse(equal(asList(1, 3, 4), asList(1, 2, 3)));
    }
}

VecOp.java

package fayelab.ddd.measure.vector.original;

public class VecOp
{
    public static boolean equal(List<Integer> vec1, List<Integer> vec2)
    {
        return vec1.equals(vec2);
    }
}

5.1.2 add

VecOpTest.java

    public void test_add()
    {
        assertEquals(asList(5, 7, 9), add(asList(1, 2, 3), asList(4, 5, 6)));
        assertEquals(asList(), add(asList(), asList()));
    }

VecOp.java

    public static List<Integer> add(List<Integer> vec1, List<Integer> vec2)
    {
        return IntStream.range(0, vec1.size())
                        .mapToObj(i -> vec1.get(i) + vec2.get(i))
                        .collect(Collectors.toList());
    }

5.1.3 dotProduct

VecOpTest.java

    public void test_dotProduct()
    {
        assertEquals(102, dotProduct(asList(1, 0, 2), asList(100, 10, 1)));
        assertEquals(0, dotProduct(asList(), asList()));
    }

VecOp.java

    public static int dotProduct(List<Integer> rowVec, List<Integer> colVec)
    {
        return IntStream.range(0, rowVec.size())
                        .mapToObj(i -> rowVec.get(i) * colVec.get(i))
                        .reduce(0, (x, y) -> x + y);
    }

5.1.4 div

VecOpTest.java

    public void test_div()
    {
        assertEquals(asList(0, 0, 2, 0), div(asList(0, 0, 0, 24), asList(Integer.MAX_VALUE, 1760, 3, 12)));
        assertEquals(asList(2, 6, 0, 4), div(asList(1, 1765, 0, 40), asList(Integer.MAX_VALUE, 1760, 3, 12)));
    }

VecOp.java

    public static List<Integer> div(List<Integer> dividendVec, List<Integer> divisorVec)
    {
        List<Integer> rDividendVec = reverse(dividendVec);
        List<Integer> rDivisorVec = reverse(divisorVec);
        int[] quotient = new int[]{0};
        
        List<Integer> rResultVec = IntStream.range(0, rDividendVec.size())
                                            .collect(() -> new ArrayList<>(),
                                                     (acc, i) -> {
                                                         int dividend = rDividendVec.get(i) + quotient[0];
                                                         int divisor = rDivisorVec.get(i);
                                                         quotient[0] = dividend / divisor;
                                                         acc.add(dividend % divisor);
                                                     },
                                                     (acc1, acc2) -> acc1.addAll(acc2));
        return reverse(rResultVec);
    }

    private static List<Integer> reverse(List<Integer> vec)
    {
        List<Integer> rVec = new ArrayList<>(vec);
        Collections.reverse(rVec);
        return rVec;
    }

5.1.5 dotDiv

VecOpTest.java

    public void test_dotDiv()
    {
        assertEquals(asList(1, 0, 2), dotDiv(102, asList(100, 10, 1)));
    }

VecOp.java

    public static List<Integer> dotDiv(int baseValue, List<Integer> vec)
    {
        int[] remainder = new int[]{baseValue};

        return vec.stream()
                  .collect(() -> new ArrayList<>(),
                           (acc, c) -> {
                               int quotient = remainder[0] / c;
                               remainder[0] = remainder[0] % c;
                               acc.add(quotient);
                           },
                           (acc1, acc2) -> acc1.addAll(acc2));
    }

5.2 Measurement System: MeasureSystem

5.2.1 toBaseFactors

根据之前的设计,MeasureSystemUI构造MeasureSystem时,会传入stepFactors,但在MeasureSystem内部还会用到baseFactors,所以在构造MeasureSystem时,要将stepFactors转换成baseFactors,并将两者都保存为成员变量,供其它方法使用。

MeasureSystemTest.java

package fayelab.ddd.measure.vector.original;

public class MeasureSystemTest extends TestCase
{
    public void test_toBaseFactors()
    {
        assertEquals(asList(1760 * 3 * 12, 3 * 12, 12, 1), 
                     MeasureSystem.toBaseFactors(asList(Integer.MAX_VALUE, 1760, 3, 12)));
    }
}

MeasureSystem.java

package fayelab.ddd.measure.vector.original;

public class MeasureSystem
{
    private List<Integer> stepFactors;
    private List<Integer> baseFactors;

    public MeasureSystem(List<Integer> stepFactors)
    {
        this.stepFactors = stepFactors;
        this.baseFactors = toBaseFactors(stepFactors);
    }

    static List<Integer> toBaseFactors(List<Integer> stepFactors)
    {
        int size = stepFactors.size();
        return IntStream.range(0, size)
                        .mapToObj(i -> stepFactors.subList(i + 1, size)
                                                  .stream()
                                                  .reduce(1, (acc, factor) -> acc * factor))
                        .collect(Collectors.toList());
    }
}

5.2.2 base

MeasureSystemTest.java

    private MeasureSystem ms;

    @Override
    protected void setUp()
    {
        ms = new MeasureSystem(asList(Integer.MAX_VALUE, 1760, 3, 12));
    }

    public void test_base()
    {
        assertEquals(63472, ms.base(asList(1, 2, 3, 4)));
        assertEquals(63396, ms.base(asList(1, 0, 3, 0)));
    }

MeasureSystem.java

    public int base(List<Integer> quantityVec)
    {
        return VecOp.dotProduct(quantityVec, baseFactors);
    }

5.2.3 normalize

要实现equal,需要先实现normalize。

MeasureSystemTest.java

    public void test_normalize()
    {
        assertEquals(asList(1, 4, 0, 1), ms.normalize(asList(0, 1762, 5, 13)));
    }

MeasureSystem.java

    public List<Integer> normalize(List<Integer> quantityVec)
    {
        return VecOp.div(quantityVec, stepFactors);
    }

5.2.4 equal

MeasureSystemTest.java

    public void test_equal()
    {
        assertTrue(ms.equal(asList(1, 2, 3, 4), asList(0, 0, 0, 63472)));
        assertTrue(ms.equal(asList(0, 1765, 0, 40), asList(1, 6, 0, 4)));
        assertFalse(ms.equal(asList(0, 1765, 0, 41), asList(1, 6, 0, 4)));
    }

MeasureSystem.java

    public boolean equal(List<Integer> quantityVec1, List<Integer> quantityVec2)
    {
        return VecOp.equal(normalize(quantityVec1), normalize(quantityVec2));
    }

5.2.5 add

MeasureSystemTest.java

    public void test_add()
    {
        assertEquals(asList(0, 0, 2, 0), ms.add(asList(0, 0, 0, 13), asList(0, 0, 0, 11)));
        assertEquals(asList(0, 3, 0, 0), ms.add(asList(0, 0, 3, 0), asList(0, 2, 0, 0)));
    }

MeasureSystem.java

    public List<Integer> add(List<Integer> quantityVec1, List<Integer> quantityVec2)
    {
        return normalize(VecOp.add(quantityVec1, quantityVec2));
    }

5.3 Measurement System UI: MeasureSystemUI

用户输入的参数校验不是本次课程的重点,这里就不实现了。

5.3.1 parseUnitConversionDesc

根据之前的设计,用户在构造MeasureSystemUI时,会传入单位换算体系(Conversion of Units)的描述,所以在构造MeasureSystemUI时,要从传入的unitConversionDesc中解析出sysUnits和stepFactors,将sysUnits保存为成员变量,将stepFactors作为构造参数传入MeasureSystem的构造函数。

用了两种方法实现。

MeasureSystemUITest.java

package fayelab.ddd.measure.vector.original;

public class MeasureSystemUITest extends TestCase
{
    public void test_parseUnitConversionDesc()
    {
        List<List<?>> actual = MeasureSystemUI.parseUnitConversionDesc(" Mile  1760 Yard 3 Feet 12 Inch  ");
        assertEquals(2, actual.size());
        assertEquals(asList(Integer.MAX_VALUE, 1760, 3, 12), actual.get(0));
        assertEquals(asList("Mile", "Yard", "Feet", "Inch"), actual.get(1));

        actual = MeasureSystemUI.parseUnitConversionDesc2(" Mile  1760 Yard 3 Feet 12 Inch  ");
        assertEquals(2, actual.size());
        assertEquals(asList(Integer.MAX_VALUE, 1760, 3, 12), actual.get(0));
        assertEquals(asList("Mile", "Yard", "Feet", "Inch"), actual.get(1));
    }
}

MeasureSystemUI.java

package fayelab.ddd.measure.vector.original;

public class MeasureSystemUI
{
    @SuppressWarnings("unused")
    private String sysName;

    private MeasureSystem ms;
    private List<String> sysUnits;

    @SuppressWarnings("unchecked")
    public MeasureSystemUI(String sysName, String unitConversionDesc)
    {
        this.sysName = sysName;
        List<List<?>> unitConversion = parseUnitConversionDesc(unitConversionDesc);
        this.ms = new MeasureSystem((List<Integer>)unitConversion.get(0));
        this.sysUnits = (List<String>)unitConversion.get(1);
    }

    static List<List<?>> parseUnitConversionDesc(String desc)
    {
        List<String> tokens = asList(desc.trim().split(" +"));

        Map<Boolean, List<String>> partitioned = tokens.stream()
                                                       .collect(Collectors.partitioningBy(token -> isEvenIndex(token, tokens)));

        List<Integer> stepFactors = partitioned.get(Boolean.FALSE).stream().map(Integer::parseInt).collect(Collectors.toList());
        stepFactors.add(0, Integer.MAX_VALUE);
        List<String> units = partitioned.get(Boolean.TRUE);

        return asList(stepFactors, units);
    }

    @SuppressWarnings("unchecked")
    static List<List<?>> parseUnitConversionDesc2(String desc)
    {
        List<String> tokens = asList(desc.trim().split(" +"));

        return tokens.stream()
                     .collect(() -> asList(new ArrayList<Integer>(asList(Integer.MAX_VALUE)), new ArrayList<String>()),
                              (acc, token) -> {
                                  if(isEvenIndex(token, tokens))
                                  {
                                      ((List<String>)acc.get(1)).add(token);
                                  }
                                  else
                                  {
                                      ((List<Integer>)acc.get(0)).add(Integer.parseInt(token));
                                  }
                              },
                              (acc1, acc2) -> {
                                  ((List<Integer>)acc1.get(0)).addAll((List<Integer>)acc2.get(0));
                                  ((List<String>)acc1.get(1)).addAll((List<String>)acc2.get(1));
                              });
    }

    private static boolean isEvenIndex(String token, List<String> tokens)
    {
        return tokens.indexOf(token) % 2 == 0;
    }
}

5.3.2 parseQuantity

根据之前的设计,用户在调用MeasureSystemUI提供的equal、add等方法时,会传入数量(Quantity)的描述,需要先解析为Quantity Vector。

MeasureSystemUITest.java

    private MeasureSystemUI ui;

    @Override
    protected void setUp()
    {
        ui = new MeasureSystemUI("Imperial Length", "Mile 1760 Yard 3 Feet 12 Inch");
    }

    public void test_parseQuantityDesc()
    {
        assertEquals(asList(1, 2, 3, 4), ui.parseQuantityDesc(" 1 Mile  2 Yard 3 Feet 4 Inch  "));
        assertEquals(asList(1, 2, 0, 4), ui.parseQuantityDesc("1 Mile 4 Inch 2 Yard"));
        assertEquals(asList(1, 2, 0, 4), ui.parseQuantityDesc("1 Mile 2 Yard 4 Inch"));
        assertEquals(asList(0, 0, 3, 0), ui.parseQuantityDesc("3 Feet"));
    }

MeasureSystemUI.java

    List<Integer> parseQuantityDesc(String desc)
    {
        List<String> tokens = asList(desc.trim().split(" +"));

        Map<Boolean, List<String>> partitioned = tokens.stream()
                                                       .collect(Collectors.partitioningBy(token -> isEvenIndex(token, tokens)));

        List<Integer> values = partitioned.get(Boolean.TRUE).stream().map(Integer::parseInt).collect(Collectors.toList());
        List<String> units = partitioned.get(Boolean.FALSE);
        
        return sysUnits.stream()
                       .map(sysUnit -> units.contains(sysUnit) ? values.get(units.indexOf(sysUnit)) : 0)
                       .collect(Collectors.toList());
    }

parseUnitConversionDesc()方法和parseQuantityDesc()方法之间存在重复代码,提取tokenize()、partitioningByIndexParity()、toIntegers()等静态方法去除重复。

MeasureSystemUI.java

    static List<List<?>> parseUnitConversionDesc(String desc)
    {
        Map<Boolean, List<String>> partitioned = partitioningByIndexParity(tokenize(desc));

        List<Integer> stepFactors = toIntegers(partitioned.get(Boolean.FALSE));
        stepFactors.add(0, Integer.MAX_VALUE);
        List<String> units = partitioned.get(Boolean.TRUE);

        return asList(stepFactors, units);
    }

    List<Integer> parseQuantityDesc(String desc)
    {
        Map<Boolean, List<String>> partitioned = partitioningByIndexParity(tokenize(desc));

        List<Integer> values = toIntegers(partitioned.get(Boolean.TRUE));
        List<String> units = partitioned.get(Boolean.FALSE);

        return sysUnits.stream()
                       .map(sysUnit -> units.contains(sysUnit) ? values.get(units.indexOf(sysUnit)) : 0)
                       .collect(Collectors.toList());
    }

    private static List<String> tokenize(String desc)
    {
        return asList(desc.trim().split(" +"));
    }

    private static Map<Boolean, List<String>> partitioningByIndexParity(List<String> tokens)
    {
        return tokens.stream().collect(Collectors.partitioningBy(token -> isEvenIndex(token, tokens)));
    }

    private static List<Integer> toIntegers(List<String> strs)
    {
        return strs.stream().map(Integer::parseInt).collect(Collectors.toList());
    }

5.3.3 equal

MeasureSystemUITest.java

    public void test_equal()
    {
        assertTrue(ui.equal("1 Mile 2 Yard 3 Feet 4 Inch", "63472 Inch"));
        assertTrue(ui.equal("1765 Yard 40 Inch", "1 Mile 6 Yard 4 Inch"));
        assertFalse(ui.equal("1765 Yard 41 Inch", "1 Mile 6 Yard 4 Inch"));
    }

MeasureSystemUI.java

    public boolean equal(String quantityDesc1, String quantityDesc2)
    {
        return ms.equal(parseQuantityDesc(quantityDesc1), parseQuantityDesc(quantityDesc2));
    }

5.3.4 format

MeasureSystemUITest.java

    public void test_format()
    {
        assertEquals("1 Mile 3 Yard 4 Inch", ui.format(asList(1, 3, 0, 4)));
        assertEquals("2 Feet 0 Inch", ui.format(asList(0, 0, 2, 0)));
    }

MeasureSystemUI.java

    public String format(List<Integer> quantityVec)
    {
        return IntStream.range(0, quantityVec.size())
                        .filter(i -> quantityVec.get(i) != 0 || i == quantityVec.size() - 1)
                        .mapToObj(i -> String.format("%d %s", quantityVec.get(i), sysUnits.get(i)))
                        .reduce((acc, valueAndUnit) -> String.join(" ", acc, valueAndUnit))
                        .orElse("");
    }

5.3.5 add

MeasureSystemUITest.java

    public void test_add()
    {
        assertEquals("2 Feet 0 Inch", ui.add("13 Inch", "11 Inch"));
        assertEquals("3 Yard 0 Inch", ui.add("3 Feet", "2 Yard"));
    }

MeasureSystemUI.java

    public String add(String quantityDesc1, String quantityDesc2)
    {
        return format(ms.add(parseQuantityDesc(quantityDesc1), parseQuantityDesc(quantityDesc2)));
    }

5.3.6 baseFormat

MeasureSystemUITest.java

    public void test_baseFormat()
    {
        assertEquals("63472 Inch", ui.baseFormat(asList(1, 3, 0, 4)));
    }

MeasureSystemUI.java

    public String baseFormat(List<Integer> quantityVec)
    {
        return String.format("%d %s", ms.base(quantityVec), sysUnits.get(sysUnits.size() - 1));
    }

5.4 Test Suite

AllTest.java

package fayelab.ddd.measure.vector.original;

public class AllTests
{
    public static Test suite()
    {
        TestSuite suite = new TestSuite(AllTests.class.getName());
        //$JUnit-BEGIN$
        suite.addTestSuite(MeasureSystemTest.class);
        suite.addTestSuite(MeasureSystemUITest.class);
        suite.addTestSuite(VecOpTest.class);
        //$JUnit-END$
        return suite;
    }
}

5.5 User

MeasureSystemUI.java

    public static void main(String[] args)
    {
        MeasureSystemUI impLen = new MeasureSystemUI("Imperial Length", "Mile 1760 Yard 3 Feet 12 Inch");
        System.out.println("1765 Yard 40 Inch == 1 Mile 6 Yard 4 Inch ? " + impLen.equal("1765 Yard 40 Inch", "1 Mile 6 Yard 4 Inch"));
        System.out.println("13 Inch + 11 Inch = " + impLen.add("13 Inch", "11 Inch"));

        MeasureSystemUI impVol = new MeasureSystemUI("Imperial Volume", "OZ 2 TBSP 3 TSP");
        System.out.println("1 OZ 10 TSP == 2 OZ 1 TBSP 1 TSP ? " + impVol.equal("1 OZ 10 TSP", "2 OZ 1 TBSP 1 TSP"));
        System.out.println("1 OZ + 3 TBSP 3 TSP = " + impVol.add("1 OZ", "3 TBSP 3 TSP"));
    }

运行结果如下:

1765 Yard 40 Inch == 1 Mile 6 Yard 4 Inch ? true
13 Inch + 11 Inch = 2 Feet 0 Inch
1 OZ 10 TSP == 2 OZ 1 TBSP 1 TSP ? true
1 OZ + 3 TBSP 3 TSP = 3 OZ 0 TSP

6 Summary

6.1 What is Design?

设计就是把问题变成可计算的。

design computable

6.2 What is Good Design?

设计出的计算模型所提供的语义和问题领域的根本需求是否匹配,匹配就是好的设计,不匹配就是不好的设计。

good design

6.3 How to do Design?

how to do design

  • 对问题领域进行深入分析,发现问题领域的核心需求;
  • 通过核心需求驱动出计算模型和语义;
  • 再围绕这个计算模型提供一套语言,给外面的人使用这个计算模型提供一个接口,这个接口可以是API、可以是数据表达、也可以是语言;
  • 最终要实现这个计算模型,实现的方法有解释器和编译器两种。

这就是DDD!这才是DDD!
这就是DSL!这才是DSL!

6.4 What is Programming?

“编程”不过是在某个计算模型上用某种语言去表达计算。 用DSL编程不过是在问题领域的计算模型上用DSL来表达计算。
计算模型是相应领域中的“通用机器”。 问题领域的计算模型是问题领域的通用机器。
编程语言不过是描述计算机器的一种方法。 DSL描述的是DSL语言的计算机器。
程序是对特定机器的描述,这个特定机器可以被通用机器仿真。 用DSL程序实现了问题领域中的某个功能,这个DSL程序就是对这个功能(特定机器)的描述。

6.5 Design & Programming

design and programming

  • 从问题领域导出核心需求,得到领域的计算模型(通用计算机器),在上面可以包装一个DSL语言或者数据或者API,基于这些开发程序和应用。
  • 领域的计算模型和通用语言的计算模型之间存在鸿沟,可以用解释器或者编译器来填补。
  • 解释器和编译器听起来很复杂,其实思想很简单,而且我们没有必要实现一个工业级别的、非常全面的解释器和编译器,只要借鉴这个思想实现我们的计算模型就够了。

Common Languages(Java/C) UML对应Java通用语言。
Common Languages(Java/C) UM对应Java通用语言提供的计算模型(通用计算机器)。

6.6 How to improve Design Capability?

提升设计能力的根本在于提升计算模型构造和语义定义能力。

Thinking, thinking and thinking …
Practice, practice and practice …
No shortcuts.

7 Homework

  1. 【必选】增加时间的单位换算体系“1 Day = 24 Hour,1 Hour = 60 Minute,1 Minute = 60 Second”,以下表达式的计算结果是什么?
7 Hour 59 Minute 60 Second == 8 Hour ?
1 Day 23 Hour 59 Minute 59 Second + 1 Second = ?
  1. 【必选】假设英制长度的单位换算体系由“1 Mile = 1760 Yard,1 Yard = 3 Feet,1 Feet = 12 Inch”改为“1 Mile = 4 Miya,1 Miya = 440 Yard,1 Yard = 3 Feet,1 Feet = 12 Inch”,以下表达式的计算结果是什么?
4 Miya 446 Yard 40 Inch == 1 Mile 1 Miya 7 Yard 4 Inch ?
3 Miya 13 Inch + 445 Yard 11 Inch = ?
  1. 【必选】任意长度对象或容量对象可以被放大N倍,其中N为大于等于0的整数,放大以后还可以和其它任意长度或容量对象进行加法运算。例如:
2 * 2 Yard 3 Feet = 6 Yard 0 Inch
3 * 1 OZ 10 TSP = 8 OZ 0 TSP
2 * 2 Yard 3 Feet + 13 Inch = 6 Yard 1 Feet 1 Inch
3 * 1 OZ 10 TSP + 3 TBSP 1 TSP = 9 OZ 1 TBSP 1 TSP

提示:这里会用到向量的标量乘法(scale),公式如下:

vector scale

其中,c为任意常数。

  1. 【可选】实现将计量系统问题领域映射到“向量空间+算术运算”领域的设计。

8 My Homework

8.1 Conversion of Time Units

【必选】增加时间的单位换算体系“1 Day = 24 Hour,1 Hour = 60 Minute,1 Minute = 60 Second”,以下表达式的计算结果是什么?

7 Hour 59 Minute 60 Second == 8 Hour ?
1 Day 23 Hour 59 Minute 59 Second + 1 Second = ?

MeasureSystemUI.java

    public static void main(String[] args)
    {
        ...

        MeasureSystemUI time = new MeasureSystemUI("Time", "Day 24 Hour 60 Minute 60 Second");
        System.out.println("7 Hour 59 Minute 60 Second == 8 Hour ? " + time.equal("7 Hour 59 Minute 60 Second", "8 Hour"));
        System.out.println("1 Day 23 Hour 59 Minute 59 Second + 1 Second = " + time.add("1 Day 23 Hour 59 Minute 59 Second", "1 Second"));
    }

运行结果如下:

7 Hour 59 Minute 60 Second == 8 Hour ? true
1 Day 23 Hour 59 Minute 59 Second + 1 Second = 2 Day 0 Second

8.2 Changing Conversion of Units

【必选】假设英制长度的单位换算体系由“1 Mile = 1760 Yard,1 Yard = 3 Feet,1 Feet = 12 Inch”改为“1 Mile = 4 Miya,1 Miya = 440 Yard,1 Yard = 3 Feet,1 Feet = 12 Inch”,以下表达式的计算结果是什么?

4 Miya 446 Yard 40 Inch == 1 Mile 1 Miya 7 Yard 4 Inch ?
3 Miya 13 Inch + 445 Yard 11 Inch = ?

MeasureSystemUI.java

    public static void main(String[] args)
    {
        ...

        MeasureSystemUI impLen2 = new MeasureSystemUI("Imperial Length 2", "Mile 4 Miya 440 Yard 3 Feet 12 Inch");
        System.out.println("4 Miya 446 Yard 40 Inch == 1 Mile 1 Miya 7 Yard 4 Inch ? " + impLen2.equal("4 Miya 446 Yard 40 Inch", "1 Mile 1 Miya 7 Yard 4 Inch"));
        System.out.println("3 Miya 13 Inch + 445 Yard 11 Inch = " + impLen2.add("3 Miya 13 Inch", "445 Yard 11 Inch"));
    }

运行结果如下:

4 Miya 446 Yard 40 Inch == 1 Mile 1 Miya 7 Yard 4 Inch ? true
3 Miya 13 Inch + 445 Yard 11 Inch = 1 Mile 5 Yard 2 Feet 0 Inch

8.3 Scale

【必选】任意长度对象或容量对象可以被放大N倍,其中N为大于等于0的整数,放大以后还可以和其它任意长度或容量对象进行加法运算。例如:

2 * 2 Yard 3 Feet = 6 Yard 0 Inch
3 * 1 OZ 10 TSP = 8 OZ 0 TSP
2 * 2 Yard 3 Feet + 13 Inch = 6 Yard 1 Feet 1 Inch
3 * 1 OZ 10 TSP + 3 TBSP 1 TSP = 9 OZ 1 TBSP 1 TSP

代码包路径:fayelab.ddd.measure.vector.scale

8.3.1 VecOp

VecOpTest.java

    public void test_scale()
    {
        assertEquals(asList(2, 4, 6), scale(2, asList(1, 2, 3)));
        assertEquals(asList(0, 0), scale(0, asList(1, 2)));
        assertEquals(asList(), scale(2, asList()));
    }

VecOp.java

    public static List<Integer> scale(int c, List<Integer> vec)
    {
        return vec.stream().map(x -> c * x).collect(Collectors.toList());
    }

8.3.2 MeasureSystem

MeasureSystemTest.java

    public void test_scale()
    {
        assertEquals(asList(0, 6, 0, 0), ms.scale(2, asList(0, 2, 3, 0)));
        assertEquals(asList(0, 6, 1, 2), ms.scale(2, asList(0, 2, 3, 7)));
    }

MeasureSystem.java

    public List<Integer> scale(int c, List<Integer> quantityVec)
    {
        return normalize(VecOp.scale(c, quantityVec));
    }

8.3.3 MeasureSystemUI

MeasureSystemUITest.java

    public void test_scale()
    {
        assertEquals("6 Yard 0 Inch", ui.scale("2", "2 Yard 3 Feet"));
        assertEquals("6 Yard 1 Feet 2 Inch", ui.scale("2", "2 Yard 3 Feet 7 Inch"));
    }

MeasureSystemUI.java

    public String scale(String c, String quantityDesc)
    {
        return format(ms.scale(Integer.parseInt(c), parseQuantityDesc(quantityDesc)));
    }


    public static void main(String[] args)
    {
        ...

        System.out.println("2 * 2 Yard 3 Feet = " + impLen.scale("2", "2 Yard 3 Feet"));
        System.out.println("2 * 2 Yard 3 Feet + 13 Inch = " + impLen.add(impLen.scale("2", "2 Yard 3 Feet"), "13 Inch"));

        System.out.println("3 * 1 OZ 10 TSP = " + impVol.scale("3", "1 OZ 10 TSP"));
        System.out.println("3 * 1 OZ 10 TSP + 3 TBSP 1 TSP = " + impVol.add(impVol.scale("3", "1 OZ 10 TSP"), "3 TBSP 1 TSP"));
    }

运行结果如下:

2 * 2 Yard 3 Feet = 6 Yard 0 Inch
2 * 2 Yard 3 Feet + 13 Inch = 6 Yard 1 Feet 1 Inch
3 * 1 OZ 10 TSP = 8 OZ 0 TSP
3 * 1 OZ 10 TSP + 3 TBSP 1 TSP = 9 OZ 1 TBSP 1 TSP

8.4 Mapping to Vector Space & Arithmetic Domain

映射到“向量空间+算术运算”的实现与映射到“向量空间”的实现相比较,最底层的VecOp和最顶层的MeasureSystemUI均无需修改。MeasureSystem的测试代码和产品代码需要修改。

代码包路径:fayelab.ddd.measure.vecarith

8.4.1 equal

equal不再依赖于normalize。

MeasureSystem.java

    public boolean equal(List<Integer> quantityVec1, List<Integer> quantityVec2)
    {
        return base(quantityVec1) == base(quantityVec2);
    }

8.4.2 normalize

normalize()不再传入数量向量而是传入数量数值。

MeasureSystemTest.java

    public void test_normalize()
    {
        assertEquals(asList(1, 4, 0, 1), ms.normalize(63505));
    }

MeasureSystem.java

    public List<Integer> normalize(int quantityValue)
    {
        return VecOp.dotDiv(quantityValue, baseFactors);
    }

此时,MeasureSystem中的add()和scale()因为都调用了normalize()还没来得及修改,相关测试代码和产品代码需要先暂时注释掉,再一个一个放开。

8.4.3 add

MeasureSystem.java

    public List<Integer> add(List<Integer> quantityVec1, List<Integer> quantityVec2)
    {
        return normalize(base(quantityVec1) + base(quantityVec2));
    }

8.4.4 scale

MeasureSystem.java

    public List<Integer> scale(int c, List<Integer> quantityVec)
    {
        return normalize(c * base(quantityVec));
    }

8.4.5 Delete stepFactors

不再需要stepFactors成员变量,删除。

MeasureSystem.java

    private List<Integer> baseFactors;

    public MeasureSystem(List<Integer> stepFactors)
    {
        this.baseFactors = toBaseFactors(stepFactors);
    }
⚠️ **GitHub.com Fallback** ⚠️