Skip to main content
  1. internet/

单一职责与函数编程的一点思考

·1121 words·3 mins·

单一职责是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循环中做了两个事情:

  1. 计算double值,并加入double列表
  2. 判断是否是奇数,并加入奇数列表

解决问题 #

最简单的方式就是遍历两遍:

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的逻辑是:

  1. 获取CARS列表中的最后一个对象
  2. 获取对象中的in_stock属性

函数nameOfFirstCar的逻辑是:

  1. 获取CARS列表中的第一个对象
  2. 获取对象中的name属性

有没有感受到“单一职责”?!

每个函数只做一件事情,然后将函数组合起来,这就是函数式编程,没有中间状态,也没有副作用!