首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

什么是面向故事编程?

关键时刻,第一时间送达!

【CSDN编者按】其实,本文并不是说让你将系统设计成故事一样,而是利用故事的心理技巧来发布设计决策和讨论。像讲故事一样设计系统的好处在于,可以让开发者将设计决策分发给那些能修辞叙述的人,而不必非得局限于某个领域,如高阶数学。

以下为译文:

▌精灵的世界

Handle 是一个小精灵。Handle 可以创造其他精灵。Handle 会按照其喜好的样子创造精灵,而他的想象总是成对出现。今天 Handle 创造了两个孩子 Reqla 和 Resnak。但他不知道为什么要创造这些孩子。只是为了存在而创造他们。缪斯神 Polys 启发了 Handle,于是 Handle 用自己的创造力诠释了灵感。创造完孩子以后,Handle 将他们送到了精灵学校,学校的精灵老师Routin 负责帮助他们寻找存在的意义。

Routin 马上明白了为什么 Resnak 很重要。他发现,Resnak 存在的意义就是将 Reqla 的意义传回给 Polys。每当有人告诉 Resnak 一些事情,他和 Reqla 就会融入以太。很重要的是,与 Resnak 交谈的人应该用最佳方式与他交流,因为 Resnak 可以直接与 Polys 交谈,而且他能够辨别 Reqla 的价值,并告诉 Polys,从而让 Polys 感受到自己的创作为人理解的喜悦。但他并不了解 Reqla 的目的。他与 Reqla 交谈,试图找出她如何才能最好地为 Polys 服务。

一般来说,当 Routin 在与精灵孩子打交道的时候,如果他能确定他们的目的,那么他会派出这些孩子来完成目的。否则,他会让能与 Polys 交谈的孩子(这里是 Resnak)去告诉 Polys,他不知道另一个孩子是来干什么的。

今天,Routin 发现 Polys 正在自己玩一种语言游戏。由于 Polys 对自己每天用的语言感到了厌倦,所以他希望听到其他语言。然而,Routin 不懂语言,不过他知道有人懂。于是他把 Reqla 和 Resnak 送到了唯一的一家精灵翻译公司。

当他们到那里的时候,他们遇到了公司的领班 Logon。Logon 掌握的语言程度刚好可以让他找出公司里最合适的人选。与 Reqla 进行了简短的交谈后,Logon 知道 Reqla 应该与谁交谈,才能翻译她带来的消息。

让我们暂停一下,多一点互动。

现在 Reqla 和 Resnak 与 Logon 在一起。Logon 知道有一个翻译员能够翻译 Reqla 的消息。接下来会发生什么?Reqla 和 Resnak 都应该去找那个翻译员吗?如果都去了,是应该由翻译员跟他们打招呼,还是 Reqla 应该跟翻译员打招呼呢?翻译完了以后,是应该由翻译员告诉 Resnak 翻译的结果,还是应该让 Reqla 告诉他呢?Resnak 是不是应该留在 Logon 那里?如果他留下,那么谁把翻译结果带回来呢?翻译员应该留在原地不动吗(也许他很胖,无法轻易挪动)?回来以后,捎信的人是应该直接告诉 Resnak,还是先告诉 Logon,然后由 Logon 告诉 Resnak?先告诉领班有意义吗?领班需要现在内部(或者与他外部的会计同事)交涉一下,再告诉 Resnak 吗?退一步说,或许让翻译人员直接面对另外三个人可能更加有效率。

无论你选择回答哪个问题,最终都会导向同一个问题:什么样的推理在逻辑上与人为的世界一致。如果你发现翻译人员都被关在象牙塔里,而你只能冲他们大喊,他们才会回答你,那么你就无法把他们叫到领班的办公室。

如果你有 web API 开发的背景,你可能会意识到上面我们讲述的是一个 Web 翻译服务稍稍拟人化的故事。用典型的行业术语来说就是:请求和响应由底层的 http 服务器成对生成。处理程序将请求/响应分发到路由,由它来决定调用哪个子处理程序。如果是翻译请求,那么语言子处理程序则根据语言检测的启发,将请求分发到翻译服务上,该服务可能不在同一个服务器上,但是可以通过子请求远程访问。翻译完成以后,通过响应体将翻译结果返回给请求者。

显然,用专业术语描述该服务要比故事简短得多。而且,短小精悍是一种非常诱人的优点。

但对于实现需求而言,这种简短是否会造成模棱两可或晦涩难懂呢?

当我向另一个开发人员描述该服务的时候(如果他们问及),肯定会采用第二种专业术语的方式。我当然不会像上面那样编造一个故事。(我花了至少 30 分钟才确保了故事叙述的一致性,且合乎大众读者的口味,虽然我仍然不确定是否合乎大众的口味,哈哈。)

但是,在我试图将上面的故事变得更详尽,同时强迫自己将各个组件分解成活跃的子进程(即精灵)并让他们的行为更像人类的过程中,我发现这里引发的横向思考往往被「正规」系统「设计」所忽略。

▌叙事推理

人类的直觉推理似乎与叙事的概念紧密相关,特别是涉及其他人类角色的叙事。尽管人类能够推理一些看似与其他人类没有明确关系的事物,如等边三角形等正几何体,但这种行为貌似仅限于钟型曲线的右侧。(注:这里钟型曲线指人类的智商分布呈正太分布,「钟型曲线的右侧」指高智商的那些人。见《钟型曲线:美国社会中的智力与阶层结构》,理查德‧赫恩斯坦、查尔斯‧默里,1994 年 9 月)

正常人类都有叙事推理的能力。这是最普通的人类的方式。在这无数的推理中,可能隐藏着一个突发问题的解决方案,即便是一个平常的人也可以解决。这类似于支持向量机(相当于一个非常聪明的人)等非常强力的分类器与链式决策树桩(相当于一群普通人)等可调节的增强版的弱分类器之间的区别,在机器学习中,水平一般的人由于其平庸无法贡献惊人的解决方案,但他可以向问题施加适当的压力从而得到解决方案。这反而让我想起了人民的智慧是无限的。有些人比所有人都聪明。但是现实中这样的人非常罕见,而且有趣的是聪明人无法与其他聪明人或不聪明的人很好地相处。

如果我们想对复杂事物(例如系统等)的推理进行民主化,特别是在推理引发这种复杂性的事物间相互作用时,一个似乎很明智的做法是将这些组件当作人类代理或意向载体,因为使用这种方法,普通人也可以进行推理。太多的情况下,我们会在推理过程中认为,人的意图是由于系统的组成部分而产生的。那么,不要将专业术语和行为类比混在一起使用,而是将组件当成小人,或小精灵,小旅鼠等等,会怎么样呢?如果我们不用高科技术语来发表关于系统开发的演讲,我们只是讲故事,会怎么样呢?

▌道德论点

我们的世界中存在必要的道德,软件也一样。当今世界受到软件的驱动。其中大部分都写得很糟糕,很难对其进行推理,但是很神奇的是 80% 的时间里它们依然能够正常工作。而 bug 肆意猖獗,即便在已解决的问题领域中,bug 也是层出不穷。很多时候,这些 bug 并不是由于推理单个组件运作的困难性,而是由于推理组件之间的关系和沟通模式的困难性。将软件分解成组件是消灭 bug 的良好开端,成千上万的文章都揭示了其中的缘由。

我们需要编写可靠的软件。人们的生活常常依赖软件,包括我自己。为了履行这最基本的道德标准,我们必须认识到目前关于系统的推理方法是不充分的。

▌解构历史

历史上出现过好几种构建系统的重要方法,试图增强系统的可推理性。一些例子如结构化编程,九十年代的面向对象编程,以及最近又兴起的函数式编程。然后,横跨所有这些方法的还有静态类型,试图在某些方面进行自动推理,以减轻人类的繁重工作。

九十年代之前被广泛采用的是结构化编程,即依照一些启发式原则,将过程分解成子过程,以保证过程的正确行为。但子过程可以做任何事,它们可以通过不可预测的方式操作其他过程依赖的状态,等等。维基百科说:

结构化编程是一种编程模式,它广泛使用结构化控制流程,如选择、重复、块解构、子例程,试图增强计算机程序的清晰程度、质量并减少开发时间。结构化编程避免使用简单条件测试和 goto 等跳转语句,因为这些会导致「面条式代码」,增加阅读和维护的难度。

九十年代流行的分解问题的方式,现在依然被称为「面向对象编程」。不幸的是,Alan Kay 博士早在七十年代发明了面向对象的概念,其实与当下流行的面向对象并不相同,却完全被人遗忘了。这种面向对象编程的主要问题是,事物拟人化经常会造成扭曲的设计,如「电梯对象询问楼层对象以得知是否需要停止运行」。什么意思?楼层又不会说话,电梯也不会说话。但人们都说,只要看看需求文档,里面的名词就是对象的候选者。对于后来成为 OOP 的概念也出现过许多批评。

由于面向对象的这种荒谬的模仿,许多人选择了完全抛弃面向对象,转而重新拥抱数学上的圣杯,即函数式编程。函数式编程不会作拟人式的地板和电梯,而是用向量表示电梯的位置和楼层的位置,将这些向量传递给某个分析算法,同时传递呼叫按钮的状态向量。这些向量的组合就会产生一个动作描述,控制哪部电梯应该停在哪一层。

谈论数学的人总是看上去比那些不会数学的人聪明些。而真正理解你所谈论的数学会令人钦佩。当人们说「单子不过是自函子范畴上的一个幺半群而已」(a monad is just a monoid in the category of endofunctors)这句名言时,如果你确实能理解这句话,那么恭喜你,你是为数不多的聪明人之一。

所有这些编程的形式中都包含了类型,以针对程序的正确性做出某种程度的自动推理。事实证明,如果你真的追求简单,那么编写用于抽象对象创建的类型(class),或者用于抽象数据结构之间的数学属性的类型(Haskell 的 typeclass)实际上是很困难的。让那些想把事情做好却受教育程度不高的人使用有类型的语言,通常会导致类型爆炸。类型 A 和类型 B 可能有 50% 的功能是一样的,因为它们都是某个底层类型 C 的特殊实现(ad-hoc realization),但发现底层抽象 C 可不是简单复制粘贴功能相同的那 50% 代码就能做到的事情。当 A 和 B 以某种不应该出现的方式耦合在一起时,这种特殊继承就会导致极其脆弱的代码。

Alan Perlis 的一句著名的格言说:「100 个函数操作同一个数据结构,要好过 10 个函数操作 10 个数据结构。」这个简介的断言通常可以由抽象代数中的概念论证。但是,找到一个能支持 100 个有用的函数的数据结构有时很简单(第一个想到的就是列表),有时却十分困难(程序的输入输出)。

▌提议

让系统设计能够被众包,从实用性、美学和性能方面都很有价值。

还有道德价值。

系统的组成结构应该尽量模仿人类。我的意思是,组件应该拥有目的,或者说拥有直接的责任,它们的交互应该可以类比为两个或多个人类之间有意义的交互。用这种方式分解系统,任何熟悉系统的人就能像叙述人类行为一样描述系统的行为。同时这种方式还能让非技术的人推理这个系统。这就保证了更多的人能够更现实地使用他们用了一辈子的办法:考虑人与人之间的关系,借此来找到系统设计问题的解决方案。这就是系统设计的民主。这就是面向故事编程。

这种想法并不是我的原创,只不过是前面的思想在我脑海里的转述而已。James Coplein 在 GOTO 2017 大会上有一次演讲,他说原始的概念化 OOP,就是用户思维的本体直接反映到系统设计的本体。如果在屏幕上看到一只狗,那么就应该能在代码中操纵(以某种形式)一只狗。这直接反映出某些面向对象设计的荒谬(如角色走向墙的对象,然后墙对象告诉角色两者碰撞了)。这个思想引发了我的一连串思考。

这次思考还产生了些新的东西,就是它内在和外在的同像性(homoiconicity)。YouTube 上有许多 Alan Kay 的谈话,他谈到了他最初关于规模扩展的想法来源于已经支持扩展的东西:人体的细胞模型。人体和大脑是个单例组件(singular component)在巨大规模的深度网络中能很好工作的典型例子。但是,并没有哪个细胞比其他细胞更「聪明」,这很像是我们谈论某个人比其他人聪明一样。

这给我带来了一个问题:如果人体本质上是生命对组成细胞的某种分布和民主,那么其他形式的民主也许可以用来创造大规模的人造物。Alan Kay 博士提议,在计算机系统中像细胞那样将任务民主化成组件,就能催生可扩展的系统。这无疑是给九十年代的 OOP 一记响亮的耳光,后者在某些情况下显然并不成功。回到 OOP 的原始概念来,重新思考下。这次不想象成细胞,而是将系统换成一系列角色、精灵、人类或者随便什么东西,让他们参与到故事中,我们就创造了一种推理系统,几乎任何人都可以参与这种推理。用扩展系统的同样方式,我们扩展了系统设计的过程。

▌反对意见

在构思并充实这篇文章的过程中,我收到了许多内部的反对意见,在这里我想一一介绍下。这些意见分为两种:美学的,和实用的。美学上的反对意见,指任何可变的因素(美学)造成的偏见。实用的反对意见,指那些认为某些提议由于社会限制或心理限制从而无法实现的观点。

▌美学

归谬法,滑坡谬误,如果在一个地方做了,那么每个地方都要做。

这种观点认为,一切事物,甚至包括最简单的一个数字,都应该是个角色。我也不知道是否应该同意这种观点,我只是展示下这种观点而已。

现实的宇宙似乎已经证实了,交流性的解构可以一直进行下去。我们可以说(实际上确实在说)夸克互相交换带有颜色的胶子。质子、电子等带电粒子交换光子。这些例子用人类的交流或意图来比喻极其简单的现象。

有个现象领域与物理没有任何关系。正规思维体系的产物,比如数学,是不存在于物理世界的。这可以作为一条明确的分界线,判断某个事物是否应该作为角色,或者作为实体过程。

因此,我们不会说数字 5 跟另一个数字 5 说「相加」。相反,我们会直接用那个正规领域的语法结构。类似地,集合论有它自己的组合数学,范畴论也是,高中代数也是,这些思维体系都构成了封闭且自我约束的推理系统,不需要任何其他交流方式。

关于这个问题的一点警告是,必须谨慎地理解,纯净性很容易被破坏。你可以把数字和字符串当做纯粹的数学结构来处理(因此不需要通过交流性的结构去分解它们),但一旦它们的纯粹性被打破(如一个可改变的字符串,或者很大的数字导致必须考虑内存分配的问题),那么就又进入了与物理有关的世界,可以再次进行叙述性解构了。

一个与现实世界关系更紧密的场景就是 HTTP Web 服务器的例子。HTTP 协议规定了字节必须以某种规定的格式传输。这种格式由字符串组成,进一步又规定什么才能组成字符串。但是,协议的其中一部分可以不遵循这种格式,比如请求体。实际过程中,你可以将请求看作一个字符串,并当做纯粹的数学实体来处理。这种方式一般没有问题,但当你遇到真实世界的限制,如很大的请求体导致耗尽所有内存,或导致性能问题的时候。这种限制会打破字符串的抽象,显露出请求的本质:对网线上传输的电信号的一种解释而已。在低层次上对其进行处理,还反映了现实世界的物理过程的本质。因此,应该避开字符串抽象,将请求看作一个角色,它能与底层进程交流其字节,这样就能更精确地讨论前面提到的优化过程。

这些概念的一种迷人的组合形式就是,首先用叙述性方式设计一个子系统,然后,如果它的内部交流可以被安全地抽象,就可以创建一个库,把流程当做数据对象来处理,用流程填充整个系统,并以纯粹的方式交换这些流程。这使得程序员可以根据实际场景,在纯粹的操作和叙述性操作之间做出选择。另外,可以像企业之间的交易那样,一组子处理的集合可以根据某个获得共识的通信协议,以抽象的方式把其他子处理的集合当做一个宏处理。

数学比自然语言更简单。数学模型让事物简化,自然语言让事物晦涩。

有一种美学的观点是,交流应该以尽可能简洁的方式完成。然而简洁通常很难达到。简洁的事物通常会倾向于通用。数学语言很简洁,而正由于其简洁,它变得很难理解。当然,哲学评论家会说最简洁的语言是最通用的语言。但这与实用性是矛盾的。

人类并不是一生下来就懂得数学。人类学习的第一种语言工具是完全混乱的,充斥了各种例外和不规则。这就是自然语言:各种语调,语气,还有语境。在这之上,年轻人必须学会剥离语境,用正式的抽象语言进行交流。这种技能是许多人根本学不会的。那么从实用角度来说,不论是抽象数学还是具体数学,尽管它有潜力成为通用语言,但它需要一定水平的能力或智力来达到流利的水平,这就阻碍了它成为民主的语言。

一个利用数学作为通用语言的基本例子,就是编程语言的交流和副作用。这方面的许多事物,如时间、空间、分歧,都可以用某种记号来表示,在应用范畴论中称为单子(monad)。如果你用过某种新闻阅读器(如 Hacker News)阅读编程的文章,那么你对这个词应该不陌生。你还可能遇到过函子(functor)、加强函子(applicative)、类型类(typeclass)、组合(composition)、恒等(identity)、密度(density)、端(end)、锥(cone)、同构(isomorphism)、anamorphism、catamorphism、米田引理(yoneda lemma)、kan 扩展(kan extensions)……

这就是最棘手的部分。绝大多数人连顺利地阅读范畴论中的语言都需要花费很长时间,更不用说让他们具有足够的水平去顺利地应用这些语言,或者用这些语言与他人交流了。对于大多数人来说,数学就是个怪物,完全是看不懂的符号语言。他们绝不会学的。

但是——这就涉及到道德了——为什么他们会这样?在计算机语言中,单子是概括带有额外数据的函数组合的正式数学结构了。其中的「额外数据」正是交流中晦涩的部分。任何输出为单子的函数都在正常的结果之外交流了某些东西。交流的东西可能是失败,可能是日志,也可能是为某个外部系统进行针对交流的描述。这些交流可以用单子组合有意义地连在一起。也就是说,单子能用抽象、通用、数学的方式概括所有交流。

我必须解释一下。我得解释下单子。大部分程序员都知道怎么说话。他们或多或少都明白人类之间有关系,这些关系通过交流进行。这些都是我们的本能,使用这些能力都是下意识的。所以为什么我要用另一种方式跟别人说,「嘿,你有一种强大的能力,我来告诉你怎样才能用你理解不了的方式来交流」?我不知道该怎么回答。单子的抽象对于我这样拥有分析思维的人来说的确很美,所以我错误地假设所有人都有同样的美学观点。

最后,我认为对于这种美学欲望的最后一击就是关于对问题的数学表现形式是否属于过早优化的争论。对于不想关心无关细节的人来说,数学表现形式远远不是最准确的优化。尽管墨菲定律对于这些无关细节能保持无关多久给出了时间限制。如果你很幸运,那么你能遇上当抽象被破坏时,你要维护的代码恰好不存在了。不过更多的情况是,你越是激进地抽象,得到报应就越快。

函数 f(x, y) = x + y 完全没有问题,除非 x 和 y 不同时存在。这种情况有个单子可以描述(future/stream)。或者无法从同一个来源获得。也有个单子可以描述(environment/reader)。能理解单子的人真的很少。能理解单子变换(monad transformer,组合单子的效果)的人就更少了。在没有类型检查的语言中实现单子变换就是个灾难。所以,一旦将美丽的数学抽象强行引入到新的需求域,事情就一下子变得复杂了。

▌务实

除了这些美学方面的考虑之外,还有一些实用的反对意见。

愚蠢和自大

对于面向故事编程概念的一种可能的反应是:像我给孩子讲故事一样讨论系统的组件太难为情了,我自己也有过这样的经历,当我大声地向同事讲述小精灵的故事的时候,觉得自己好傻。仔细想想,这也并不意外。这种情况下,系统的组件不是深刻而又细致入微的人类,所以讨论它们不像讨论《警犬追杀令》那么简单,除非我们讨论一些演员欠缺的方面。但是,无论预期结果怎样,要想重新构建问题让任何孩子都能解决,那都是一件骇人听闻的事情。解构的过程需要将门槛降到最低,比如 5 岁的孩子都可以做到。

害怕难为情,犯傻,或自大都不是好的理由,我们要排除万难。如果你是个非常聪明的人,而且你发现自己很难放下做好一切事情的想法,那么你可能会导致已然艰难的软件推理雪上加霜。

屈尊

我所想到的第二个反对意见是过渡的实际问题。由于分解技术的解构特性,我们很容易给非技术人员解释系统问题,因此他们可以提供自己的叙述推理。然而,可以想象到在一些组织中,如果你听到通常使用行业术语的人突然用给孩子讲故事的口气讲话,会感觉有点自降身份或屈尊。

如果只对一小撮明白这种练习带来的价值的人进行练习,那么相对会容易一点,同事要培养外部的人,或只向非技术人员(项目经理等)解释其中的好处,至于由谁来讨论问题并不重要。语言并不会减弱对听众的阐述,它可以降低门槛。

身份动态

最后一个我想到的顾虑是纯粹与不纯粹的考虑。有时人们会依靠数学来解决人事问题。方程不会涉及时间,也不会论及性别或种族。

优秀的代码库曾引起过争议,主要是围绕身份,美国黑人之外的「主人/仆从」等概念上,或人们在代码中(0x8008135)不恰当地引用性别概念。这是一个值得关注的问题,除了非常正规的数学世界以外,其他非科学系统的讨论中也必然会掺杂人们所关注语言编码问题。在文本上述的故事中我只有一位女性代词的角色。

我对于这种批评的回应是:这些问题超出了我提出的解决方案。仅通过参与色盲或无性别差异编程,无法改变你的种族歧视或性别歧视。微软的种族主义者 Tay twitter 聊天机器人就是一个例子,我们构建的系统扩展并将网络用语编码到系统中,因而产生了这种机器人。为你的系统排除这些忧患意味着排除系统产生的问题,也就是你的团队。

▌结论

本文介绍了面向故事的程序设计,以及值得尝试的原因。坦白来说,这个想法对我来说似乎非常激进,因为面对那些希望数学成为计算机通用语的人群时我会华丽丽的倒下。鉴于与通用语相关的产品以及这种产品周围的系统的优点以及其与生俱来的无法改变规模的特性,我想用这篇文章来表达自己完全站在对立面的立场。

也许这可以引起你的共鸣。如果真是这样,我鼓励你大胆尝试。我肯定会在自己的个人项目中将这些想法付诸实践。之后,我会勇敢地在公司和产品系统内开展关于这些概念的更大的讨论。这种方法的意义非常深远,所以他们带来的改变将席卷全球。

人类善于叙述。我们可以将叙述直接编入我们的软件方法中。让我们进入面向故事的编程时代。

原文:http://www.brandonkeown.com/2018/05/story-oriented-programming.html

作者:Brandon Keown

译者:弯月,责编:杨丽

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180610A15FWE00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券