单一职责与函数编程的一点思考
Table of Contents
单一职责是SOLID原则中的一个思想,使用函数式编程能够更天然的实现单一职责。
一个简单的反例 #
由于某种特殊原因,我们需要计算一个整数列表的double列表和奇数列表,实现很简单:
func main() {
counts := []int{1, 2, 4, 8}
var doubleCounts []int
var oddCounts []int
for _, cnt := range counts {
doubleCounts = append(doubleCounts, cnt*2)
if cnt%2 == 1 {
oddCounts = append(oddCounts, cnt)
}
}
fmt.Println(oddCounts, "\n", doubleCounts)
}
但是在这个简单的例子中,我们违反了“单一职责原则”——一个for循环中做了两个事情:
- 计算double值,并加入double列表
- 判断是否是奇数,并加入奇数列表
解决问题 #
最简单的方式就是遍历两遍:
func main() {
counts := []int{1, 2, 4, 8}
var doubleCounts []int
var oddCounts []int
for _, cnt := range counts {
doubleCounts = append(doubleCounts, cnt*2)
}
for _, cnt := range counts {
if cnt%2 == 1 {
oddCounts = append(oddCounts, cnt)
}
}
fmt.Println(oddCounts, "\n", doubleCounts)
}
看起来有点蠢?是的,那我们优化下——将两个逻辑抽象为函数。
func main() {
counts := []int{1, 2, 4, 8}
var doubleCounts []int
var oddCounts []int
doubles := func(cnt int) {
doubleCounts = append(doubleCounts, cnt*2)
}
odds := func(cnt int) {
if cnt%2 == 1 {
oddCounts = append(oddCounts, cnt)
}
}
for _, cnt := range counts {
doubles(cnt)
odds(cnt)
}
fmt.Println(oddCounts, "\n", doubleCounts)
}
问题得以解决!
尽管看上去for循环中还是做了这两件事情,但是通过将逻辑抽离出来,for循环中实际上没有处理任何逻辑,它只是起了一个聚合作用。
到底有什么好处 #
这样写到底有什么好处呢?我们再举一个例子。
现在需求需要变更:double列表长度不能超过3!
在原代码的基础上很容易实现:
func main() {
counts := []int{1, 2, 4, 8}
var doubleCounts []int
var oddCounts []int
for _, cnt := range counts {
doubleCounts = append(doubleCounts, cnt*2)
if len(doubleCounts) >= 3 {
break
}
if cnt%2 == 1 {
oddCounts = append(oddCounts, cnt)
}
}
fmt.Println(oddCounts, "\n", doubleCounts)
}
随着功能的实现,bug也产生了!在double列表长度超过3之后不再继续遍历,这进而影响了奇数列表的逻辑!
如果是“单一责任”的代码,则不会有任何问题:
func main() {
counts := []int{1, 2, 4, 8}
var doubleCounts []int
var oddCounts []int
doubles := func(cnt int) {
doubleCounts = append(doubleCounts, cnt*2)
if len(doubleCounts) >= 3 { // 改动的代码
return
}
}
odds := func(cnt int) {
if cnt%2 == 1 {
oddCounts = append(oddCounts, cnt)
}
}
for _, cnt := range counts {
doubles(cnt)
odds(cnt)
}
fmt.Println(oddCounts, "\n", doubleCounts)
}
单一职责在做什么 #
单一职责的任务,就是将各个逻辑抽离出来,不要互相影响!
我们在修改double列表的逻辑时,不应该影响奇数列表的逻辑;我们在修改奇数列表的逻辑时,也不应该影响double列表的逻辑。
函数式编程? #
这段代码很难说是函数式编程,但是却体现了函数式编程中“无副作用”的特点。
“单一职责”就是保护代码逻辑不会受到其他逻辑的“副作用”影响!
让我们看一段《函数式编程指北》中的代码:
var CARS = [
{name: "Ferrari FF", horsepower: 660, dollar_value: 700000, in_stock: true},
{name: "Spyker C12 Zagato", horsepower: 650, dollar_value: 648000, in_stock: false},
{name: "Jaguar XKR-S", horsepower: 550, dollar_value: 132000, in_stock: false},
{name: "Audi R8", horsepower: 525, dollar_value: 114200, in_stock: false},
{name: "Aston Martin One-77", horsepower: 750, dollar_value: 1850000, in_stock: true},
{name: "Pagani Huayra", horsepower: 700, dollar_value: 1300000, in_stock: false}
];
// ============
var isLastInStock = _.compose(_.prop('in_stock'), _.last);
console.log(isLastInStock(CARS)); // false
// ============
var nameOfFirstCar = _.compose(_.prop('name'), _.head);
console.log(nameOfFirstCar(CARS)); // Ferrari FF
函数isLastInStock
的逻辑是:
- 获取CARS列表中的最后一个对象
- 获取对象中的in_stock属性
函数nameOfFirstCar
的逻辑是:
- 获取CARS列表中的第一个对象
- 获取对象中的name属性
有没有感受到“单一职责”?!
每个函数只做一件事情,然后将函数组合起来,这就是函数式编程,没有中间状态,也没有副作用!