序言
正月十五是一年一度的元宵节,是春节之后的第一个重要节日。在我们中国人眼里,过完正月十五元宵节,这年才算是真正地过完了。猜灯谜、赏花灯、舞龙狮、吃元宵、看晚会,老百姓把元宵节过得有滋有味,热闹非凡。还值得一提的是,古城西安今年在元宵节推出了“西安年·最中国”的压轴大戏,即500架无人机灯光秀,点亮了元宵节的璀璨夜空。
除了过元宵节,在朋友圈也看到“猿霄节“的说法:
猿霄节,是程序猿通宵赶代码的中国传统节日,在此佳节来临之际,恭祝全国程序猿节日快乐。
笔者感觉“猿霄节“非常好玩,就有了即兴写篇文章的想法,于是在过完元宵节后,找了一个题目:数三角形。这个题目是2015年参加一个软件设计类培训的练习题之一,当时是用Erlang语言实现的,今天打算用Golang实现一下,并将整个建模和实现过程通过文章记录下来,以便作为“猿霄节“的一个礼物送给广大读者,尤其是Gophers。
题目
数数下图中一共包含多少个三角形?
答案是24,你数对了吗?
记得当时参加培训的同学给出的答案有很多个,有少于24的,也有多于24的,每个人的数法不尽相同。但如果让计算机来数的话,就必须将数三角形的方法描述成计算机可以执行的形式化算法。这种描述方法可以有很多种,而我们希望找到一套抽象层次高且非常贴合领域的描述,以便降低后续的维护成本。
领域模型设计
我们考虑一下:什么是三角形?
如果没有记错的话,我们在小学就学过:三角形就是三个点,两两相连,但是三个点不同时在一条直线上。
我们用形式化的方法表达一下:
is_triangle(x, y, z) ->
connected(x, y) and
connected(y, z) and
connected(x, z) and
not(in_same_line(a, b, c))
领域模型设计的核心是解决下面两个问题:
- 三角形的三个点
(x, y, z)
都有哪些? - 计算机怎么知道
connected
和in_same_line
?
(x, y, z)的集合
从题目中,我们可以知道点的集合points
,不妨用字符串来表示:
points = "abcdefghijk"
(x, y, z)
的集合简单讲就是从points
的集合中取三个点的子集,每个子集是个字符串,所有子集就是一个字符串数组切片,可以简单用subset(points, 3)
来描述。不过为了通用,我们直接用subset(points, n) : (string, int) -> []string
来描述:
subset(points, n) when len(points) < n -> []
subset(points, n) when len(points) == n -> [points]
subset(points, n) when n == 1 -> [points[0], points[1], ..., points[len(points) - 1]]
subset(points, n) ->
firsts = subset(points[1:], n - 1) with points[0] +
lasts = subset(points[1:], n)
firsts append lasts
connected和in_same_line
从题目中,可以知道线的集合lines
,不妨用字符串数组切片来表示:
lines = {"abh", "acgi", "adfj", "aek", "bcde", "hgfe", "hick"}
目标是要知道点与点之间关系:2个点的关系connected
,3个点的关系in_same_line
,等价于2个点或3个点组成的集合points
是否为lines
中任一元素的子集,不妨用belong(points, lines) : (string, []string) -> bool
来表示这种关系,而belong
已经是一个原子语义。
我们下面用belong
描述一下connected(x, y) : (byte, byte) -> bool
和in_same_line(x, y, z) : (byte, byte, byte) -> bool
:
connected(x, y) ->
belong(xy, lines)
in_same_line(x, y, z) ->
belong(xyz, lines)
领域模型实现
subset
的实现:
func subset(points string, n int) []string {
l := len(points)
if l < n {
return nil
}
if l == n {
return []string{points}
}
results := make([]string, 0)
if n == 1 {
for i, _ := range points {
bs := []byte{points[i]}
results = append(results, string(bs))
}
return results
}
firsts := subset(points[1:], n - 1)
for _, first := range firsts {
results = append(results, string([]byte{points[0]}) + first)
}
lasts := subset(points[1:], n)
results = append(results, lasts...)
return results
}
belong
的实现:
func belong(points string, lines []string) bool {
flag := false
for _, line := range lines {
flag = true
for _, r := range points {
if !strings.ContainsRune(line, r) {
flag = false
break
}
}
if flag {
return true
}
}
return false
}
not
的实现:
func not(flag bool) bool {
return !flag
}
CountingTriangles
的实现中准确的表达了领域模型is_triangle
:
func CountingTriangles(points string, lines []string) []string {
connected := func(x, y byte) bool {
bs := make([]byte, 2)
bs[0] = x
bs[1] = y
return belong(string(bs), lines)
}
in_same_line := func(x, y, z byte) bool {
bs := make([]byte, 3)
bs[0] = x
bs[1] = y
bs[2] = z
return belong(string(bs), lines)
}
is_triangle := func(x, y, z byte) bool {
if connected(x, y) &&
connected(y, z) &&
connected(x, z) &&
not(in_same_line(x, y, z)) {
return true
}
return false
}
num := 0
matches := make([]string, 0)
rs := subset(points, 3)
for _, r := range rs {
if len(r) != 3 {
continue
}
if is_triangle(r[0], r[1], r[2]) {
matches = append(matches, r)
num++
}
}
return matches
}
领域模型应用
在应用层表达题目:
package main
import (
"fmt"
"triangle/domain"
)
func main() {
points := "abcdefghijk"
lines := []string{"abh", "acgi", "adfj", "aek", "bcde", "hgfe", "hijk"}
matches := domain.CountingTriangles(points, lines)
fmt.Println("counting triangles result: ")
fmt.Println("num of triangles: ", len(matches))
fmt.Println("detail of triangles: ", matches)
}
运行main
函数:
counting triangles result:
num of triangles: 24
detail of triangles: [abc abd abe acd ace ade aef aeg aeh afg afh agh ahi ahj ahk aij aik ajk beh ceg def ehk fhj ghi]
小结
领域建模在于不断的挖掘领域的本质,然后用优秀的代码简洁地表达领域模型。我们通常习惯于将所有问题按OO
范式的思维来建模,而FP
范式非常适合将领域映射到数学本质上。今后,我们在解决特定领域问题时要选择最合适的编程范式,从而简单高效的解决问题。