背景介绍
上一篇文章我们一起学习了面向对象编程,这一篇文章我们就一起学习函数式编程吧~
《架构整洁之道》系列:
函数式编程
函数式编程所依赖的原理,在很多方面其实是早于编程本身出现的。因为函数式编程这种范式强烈依赖于 Alonzo Church 在 20 世纪 30 年代发明的 λ 演算。
函数式编程语言中的变量(variable)是不可变(Vary) 的。
书中多次提到 Clojure 语言作为函数式编程的范例,大家如果感兴趣可以对其进行了解。
不可变性与软件架构
为什么不可变性是软件架构设计需要考虑的重点呢?为什么软件架构师要操心变量的可变性呢?
所有的竞争问题、死锁问题、并发更新问题都是由可变变量导致的。如果变量永远不会被更改,那就不可能产生竞争或者并发更新问题。如果锁状态是不可变的,那就永远不会产生死锁问题。
换句话说,一切并发应用遇到的问题,一切由于使用多线程、多处理器而引起的问题,如果没有可变变量的话都不可能发生。
我们需要确保自己设计的系统在多线程、多处理器环境中能稳定工作。所以在这里,我们实际应该要问的问题是: 不可变性是否实际可行?
如果我们能忽略存储器与处理器在速度上的限制,那么答案是肯定的。否则的话,不可变性只有在一定情况下是可行的。
下面让我们来看一下它具体该如何做到可行。
可变性隔离
一种常见方式是将应用程序,或者是应用程序的内部服务进行切分,划分为可变的和不可变的两种组件。不可变组件用纯函数的方式来执行任务,期间不更改任何状态。这些不可变的组件将通过与 一个或多个非函数式组件通信的方式来修改变量状态。
由于状态的修改会导致一系列并发问题的产生,所以我们通常会采用某种事务型内存来保护可变变量,避免同步更新和竞争状态的发生。
一个架构设计良好的应用程序应该将状态修改的部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可变量。软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好。
事件溯源
举个例子:
假设某个银行应用程序需要维护客户账户余额信息,当它执行存取款事务时,就要同时负责修改余额记录。但是如果我们不保存具体账户余额,仅仅保存事务日志,那么当有人想查询账户余额时,我们就将全部交易记录取出,并且每次都得从最开始到当下进行累计。当然,这样的设计就不需要维护任何可变变量了。
这就是事件溯源,在这种体系下,我们只存储事务记录,不存储具体状态。当需要具体状态时,我们只要从头开始计算所有的事务即可。
这种数据存储模式中不存在删除和更新的情况,我们的应用程序不是 CRUD,而是 CR。因为更新和删除这两种操作都不存在了,自然也就不存在并发问题。
如果我们有足够大的存储量和处理能力,应用程序就可以用完全不可变的、纯函数式的方式来编程。「什么叫足够大?即整个程序的生命周期内,我们有足够的存储和处理能力来满足它。」
结束语
三种编程范式到这里就一起学习完成了,下面我们进行总结:
- 结构化编程是对程序控制权的直接转移的限制。
- 面向对象编程是对程序控制权的间接转移的限制。
- 函数式编程是对程序中赋值操作的限制。
这三个编程范式都对程序员提出了新的限制。每个范式都约束了某种编写代码的方式,没有一个编程范式是在增加新能力。
也就是说,我们过去几十年学到的东西主要是什么不应该做。
总而言之,软件,或者说计算机程序无一例外是由顺序结构、分支结构、循环结构和间接转移这几种行为组合而成的,无可增加,也缺一不可。
最后
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
少年向来不识天高地厚
放眼处皆自负才高八斗
虽是自命风流
倒也坦诚无忧
我爱这样的少年
谦和而狂妄
骄傲又坦然☀️
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨