声明式编程到底是什么?
你很可能在某个时候听人提起过声明式编程的概念。也许是在Medium上看到过一篇文章,或者是在Twitter上看到过有人提到过。也可能你当时正在参加一个本地的技术社交活动,突然间,某个名不见经传的房地产颠覆性创业公司的天才CTO(一个精神变态的家伙)开始在吧台上砸空啤酒瓶,挥舞着这简陋的玻璃武器,威胁说如果他们不停止使用if/else语句,就要砍死在场的所有人。
它们大概长这样。
“声明式编程?”你心想,“或许维基百科能用简单易懂的方式概括一下,方便那些对这个主题感兴趣的新手入门。” 但你不会这么想,因为你知道,在维基百科上阅读任何技术性内容都会让你头痛欲裂,那种感觉恐怕只有连续十二小时狂饮当地酒铺里最便宜的麦芽酒后宿醉才能与之媲美。你遇到的所有文章都像是同一种痛苦的不同版本。一个冗长的术语最终会引出下一个,直到你陷入一个永无止境的、自我毁灭式的网络搜索的兔子洞,等你终于找到出口时,你甚至都认不出镜子里的自己了。
这是我为撰写本文所做的研究的实际照片。
好吧……我刚才可能有点夸张了,但希望我能稍微减轻你的痛苦。很多人会争论什么才是真正意义上的声明式语句;我不是在写博士论文,所以我们会用轻松有趣的方式学习(如果你想要博士级别的讲解,请参考这篇StackOverflow 回答)。
如果你曾经查阅过声明式编程是什么,你很可能对以下常见答案的某种变体非常熟悉:
声明式编程描述的是“做什么”,命令式编程描述的是“怎么做”。
好的,但这到底意味着什么呢?首先需要解释几点:声明式编程有一个对立面,叫做命令式编程。你几乎总能看到对这两种对立范式的比较。但关键在于,虽然这两种方法在执行上截然相反,但这并不意味着它们不能共存。这就引出了我的第一个要点:
第一课:声明式编程离不开命令式抽象(它只是层层叠加)
我知道我说过这是篇面向初学者的指南,所以让我用更简单的语言解释一下。我公司有一台很奇特的、很高级的咖啡机,上面列了两页不同的咖啡配方,但实际上你只会喝到其中的两种。
第二页有一个“香草巧克力拿铁”的选项,但没有人有勇气去探究这些口味的来源。
想想看,用这台造型奇特的机器和用法压壶有什么区别?假设你特别不想冒险,决定还是喝普通的咖啡。你走到这台庞然大物般的咖啡机前,按下“启动”按钮。机器发出震耳欲聋的轰鸣声,咖啡灌进你的杯子里。你完全不用关心从按下按钮到拿到咖啡这段时间发生了什么——你只需要拿到你想要的那杯咖啡。这台咖啡机是声明式编程的一个简单例子。实现细节被隐藏起来;你只需表达你想要 什么,而无需指定如何实现。接下来,我们来看看法压壶的命令式编程:
- 采摘豆子并磨碎。
- 用烧水壶烧开水。
- 从法压壶中取出压杆,然后倒入咖啡粉。
- 将沸水倒入法压壶中。
- 3-4 分钟后(或所需的浸泡时间),慢慢向下按压活塞,将咖啡粉与水分离。
- 将成品倒入杯中享用。
控制流程清晰明确;流程中的每一步都安排得井井有条,执行得也十分到位。告诉应用程序你想让它做什么固然很好,但幕后仍然需要有某种机制在操控这些操作!
map这里将同样的概念应用到一个更实际的场景中。你可能熟悉ES6 中新增的高阶函数。如果你不熟悉,让我快速概括一下: map`map` 是 JavaScriptArray对象的一个属性,它会遍历调用它的数组,并对每个元素执行回调函数。它返回一个数组的新实例;不会对原始对象进行任何修改。让我们来比较一下两个函数(声明式和命令式),它们都遍历一个字符串数组,并在每个字符串的末尾添加一个章鱼表情符号“🐙”(客观来说,这是最好的表情符号)。
// Declarative
const addOctopusEmoji = arr => arr.map(str => str + "🐙");
// Imperative
const addOctopusEmoji = arr => {
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i] + "🐙"
}
return arr;
}
相当简单明了,也很好地演示了第一课的内容。map它比传统的循环机制更具声明性。你无需编写控制流来决定如何遍历数组的每个索引并执行必要的操作。map它会为你完成这些繁重的工作。但它map具有命令式的抽象。它并非魔法,底层也需要进行一些操作。区别在于,你无需关心它如何执行操作的具体实现细节(而且,它还会返回一个新的数组实例。这意味着你不会像命令式示例那样修改任何现有引用,从而避免产生任何意外的副作用;稍后会详细介绍)。它只是分层而已,朋友们!好了,现在你离成为声明式编程高手又近了一步。
基本上就是你现在的状态。
第二课:声明式编程不是函数式编程
这并不是说它们是完全不同的概念。很多人认为函数式编程是声明式编程的一个子集。一个真正的声明式程序是用表达式编写的,该表达式会被执行/求值,并且可以指定你想要的结果(再次强调,这和你到处都能看到的描述一致)。SQL 就是一个很好的声明式语言的例子。
SELECT
*
FROM
tough_guys
WHERE
name = 'Keith Brewster'
Query returned 0 results.
对不起,其他所有叫 Keith Brewster 的人,别装作我没在谷歌上搜索过你们。
你不需要手动解析表中的“名称”列,找出所有名叫“Keith Brewster”的硬汉。你只需以表达式的形式提供约束条件,SQL 就会返回你想要的结果。谢谢你,SQL。
现在我们来看看 JavaScript。你不能只是把一个表达式塞进应用程序里,就指望 JavaScript 引擎帮你运行所有程序。你必须用一系列函数来构建应用程序的功能(明白我的意思了吧?)。这并不意味着 JavaScript 本身就是一种函数式编程语言,因为函数式编程有它自己的一套规则和约束。但是,你可以在代码中应用这些概念,像使用函数式编程语言一样使用 JavaScript,就像你可以在 JavaScript 中使用类和继承,像使用面向对象编程语言一样。这只是构建应用程序架构的另一种方式。
老板,这房子应该用什么建筑风格来建造?
函数式编程被认为是声明式编程的一个子集,因为它也力求避免以命令式或过程式的方式编写代码。我在这里不会深入探讨函数式编程(也许这是为以后的文章埋下的伏笔)。你现在真正需要知道的是,声明式编程不等同于函数式编程,但函数式编程又是声明式的。
第三课:相当多的现代框架以声明式方式处理 UI
题外话:大学时我一直沉浸在Java的世界里。每个学期我们都在学习越来越多的Java。偶尔也会接触其他语言(C++、C#、PHP),但大多数时候我们都在编写各种计算器的变体,或者解决一些我们已经用Java学过的数学题。可想而知,毕业后发现就业市场并非95%都是Java,这让我非常震惊,尽管我的教育背景让我对这种现实有所准备。大学时我对Web开发并没有太大兴趣,但毕业后我很快就投入其中。接触JavaScript对我来说是一个巨大的转变;我开始看到人们用各种各样令人兴奋的方式编写代码。如果我能在这篇文章中提出一个建议,那就是要保持开放的心态,接受不同的视角。了解其他人如何解决问题对我作为一名开发者的成长至关重要。
好了,言归正传。什么是声明式 UI?它其实就是另一种抽象,但不同之处在于,它隐藏的不是函数的实现细节,而是 UI 变更的实现细节——请继续往下看。我们来看看 React 是如何运用声明式方法实现 UI 的:
<PotentiallyGreenButton
handleClick={toggleIsButtonGreen}
buttonGreen={isGreen}
>
{buttonText}
</PotentiallyGreenButton>
所以,这里我们有一个“可能为绿色的按钮”。它可能是绿色的,也可能不是。我们永远不会知道。以前,如果你想更新一个 DOM 元素,你需要创建一个对它的引用,然后直接将更改应用到该元素上。这非常不方便;你的功能与单个元素(或者根据你选择元素的方式,与所有元素)耦合在一起。React 对 DOM 的更新进行了抽象,因此你无需管理它。你只需专注于开发组件——无需负责每次渲染周期中 DOM 元素更新的具体实现细节。你也不需要管理 DOM 事件监听器。React 提供了一个易于使用的SyntheticEvents库,它抽象了所有 DOM 事件逻辑,使你可以专注于重要的业务逻辑(在本例中,就是这个“可能为绿色”的按钮是否为绿色)。
第四课:最终,没有对错之分
我喜欢用声明式的方式编写代码。也许你不喜欢,也许你喜欢明确地描述控制流。也许这种方式更容易理解,或者更符合你的习惯。这完全没问题!这并不会降低你作为程序员的价值,所以如果你不习惯这种方式,也不要感到难过(也不要让任何人告诉你相反的说法)。最重要的是能够理解不同方法背后的理念。做你自己就好!
在结束之前,我想重点强调一下我喜欢采用声明式编程方法的几个原因:
与上下文无关:
声明式编程风格能带来更高的模块化程度。如果你的功能不与任何应用程序状态耦合,它就与上下文无关。你可以在任何应用程序中重用相同的代码,并且它应该以完全相同的方式运行。这意味着你应该避免修改函数上下文之外的任何数据(例如全局变量)。
可读性
这或许有点另类,但我认为声明式编程更易读,前提是你得花心思让函数和变量名具有自解释性。有些人可能觉得查看控制流(循环、if/else语句)并逐步理解代码更容易,所以这更多的是一种主观优势。
无副作用
还记得我在第一点中括号里那段“稍后详述”的小字吗?好,我们现在就来谈谈这个!副作用是指在应用程序的某个地方修改值时,会对其他地方产生意想不到的影响。在声明式应用程序中,你应该将所有内容都视为不可变的。这意味着,变量初始化后就不能再修改它。如果你想更新某个值,你应该基于该项初始化一个新的变量,并将你想要进行的修改存储进去(就像我们在章鱼“🐙”示例中使用 `array.map` 那样)。如果你没有改变应用程序的状态,那么它就不应该在应用程序的其他地方产生副作用。
真有趣!
尝试新的编码方式是一项有趣的挑战,你可能会发现解决问题的新方法。因为你不再依赖循环,所以你会更多地使用递归。尝试减少对 if/else 语句的依赖可能会让你接触到函子。至少,这是一种很好的实践!
终于结束了,你成功了。
呼!感谢你耐心看到这里,我知道信息量有点大。如果你喜欢我的内容,可以考虑在推特上关注我。希望今天的内容对你有所帮助!
干杯!
文章来源:https://dev.to/brewsterbhg/what-the-heck-is-declarative-programming-anyways-2bj2





