为什么说写代码如盗墓笔记呢,是因为写代码确实有一些特点和盗墓笔记很像,比如坑比较多,也都处于暗处,没有在明面上,在进入之前是无法知晓整个结构的,只有亲身深入其中,详细探索完每一个地方之后才能知晓。
但是这个探索过程是非常艰难的,因为一切都是未知的,也没有任何导航可用,只能两眼抹黑的往前瞎拱,自然免不了各种迷路,各种碰壁,各种掉坑。
这种艰难的状况经历了多了之后,我们就应该思考了,是什么原因导致了这种状况?那么又有没有方法可以解决这种状况吗?
我们重新审视分析下程序代码的特点,只有了解了这些特点,才能探究出原因。
程序代码主要是由数据变量和逻辑控制流构成的,其中逻辑控制流占据了绝大部分。而逻辑控制流包含了许多分支,就像本来是一条道路,却分叉出了许多支路,选择一条支路,走着走着又继续分叉出更多的支路出来,密密麻麻,乱乱糟糟的,这就导致了这些道路非常难走。并且更令人抓狂的是,这些道路没有导航,没有地图,没有路标,非得抹黑走到路的尽头才能知道尽头到底是什么。
数据变量基本上有两个职能,一个是用来标记逻辑控制流的运行状态;另一个就是用来组成数据结构。而实际上离散的数据变量也算是数据结构的一种,只是不同于经典数据结构,不太明显而已,容易让人误认为其仅是一个离散的变量,而忽略其影响并左右逻辑控制流的结构性的意义。
所以整体看来就是,数据变量和逻辑控制流是互相关联互相影响的。
而从分析过程中也可以知道,程序代码的特点就是分支散碎,藏于尽头,难以获知。导致这一特点的原因也是逻辑控制流的复杂性、散碎、藏于尽头。也即,程序代码的这些不好的特点都是由逻辑控制流体现的。
那么逻辑控制流能避免这些缺点吗?答案是不能。因为这是由于逻辑控制流的根本复杂性和本质复杂性所决定的。逻辑控制流说白了就是平常说的过程,所以也是无法跳过的,因为你不能跳过过程而直接获得结果。
所以从这里看来,程序代码的这些缺点也依然存在,无法避免。
不过有一点非常值得思考,逻辑控制流的目的是什么?是进行数据转换,也就是进行数据结构的转换,也就是描述并控制数据的流向。
这也是程序的本质:从输入数据结构,来获取输出数据结构。
所以,写逻辑控制流的意义不是纯粹为了写逻辑控制流,而是为了进行数据结构的转换,描述并控制数据的流向。因此,我们可以获得一个非常重要的有用信息,只要能够理清数据的流向,就能显著降低阅读代码的难度。
我们阅读代码的目的其实也就是为了弄清楚数据的流向。
那么我们就把重心转移到理清数据的流向上来,分析下如何才能更好的理清数据的流向。
因为逻辑控制流的本质复杂性,所以我们无法通过逻辑控制流来清晰的理清数据的流向,这也正是我们一直以来读写代码所采用的方式,事实证明,这不是好的方式,数据流向散落在逻辑控制流的各个地方,根本没有好的办法可以串联起来,形成一个有导航意义的地图。
前文说过,程序代码是由数据变量和逻辑控制流构成的,也提到过,数据变量和逻辑控制流是互相关联互相影响的,并且数据流向自然也离不开数据变量。既然没有好的办法通过逻辑控制流来理清数据流向,那么就需要思考下在数据变量上是否有好的办法。
数据变量是什么?前文解释过,数据变量就是数据结构。
请着重注意 结构 这个概念,结构表示了层级和关联,而关联就是流向,我们正是想理清流向。所以在数据结构身上,我们看到了理清数据流向的希望。
在开始分析如何通过数据结构来理清数据流向之前,我觉得有必要,也非常有必要重新强化对结构的认知。因为有太多的小伙伴们对结构没有足够的重视,不以为然,莫不关心,没有认知到结构的重要性以及不可或缺性以及根本本质性。
我们眼前的一切,周围的一切,手机、电脑、衣食住行、山川大海、以及你我,都是由结构构成的,更确切的讲,这一切就是结构。我们第一次认知这个世界就是从结构开始,第一次睁眼看到父母,第一次拆解玩具,第一次感受花草。人类学习结构,利用结构,组成各种各样的新的结构、新的工具来帮助我们生活。人类研究周围的各种物体,以及日月星辰、宇宙天体,其实就是在探索它们的结构,和结构之间的关联。只有知晓了它们的结构与之间的关联,我们才能组装更高级的结构来生产工具,才能进一步知晓周围万物的原理和世界的运行规律。
我们都知道我们身处的宏观世界是三维的立体世界,这个世界中的任何物体都有其形状,有其结构,形状就是结构,没有结构将无法存在于这个世界中。即使加入看不见摸不着的时间维度组成四维的时空世界,结构依然存在于任何一个维度之中。我们都知道万有引力,其中引力并不是虚无飘渺的能量,而是质量在时空中引起的时空扭曲,正是因为时空可以被扭曲,所以任何一个维度都是有结构的,人类一直在探索其中的结构和关联。
在微观世界中,物质是由分子构成的,人类对分子的结构进行探索发现了原子,对原子的结构进行探索发现了原子核,继续不断对结构进行探索发现了夸克。反过来讲就是,各种更小的粒子结构通过 关联 组合成较大的粒子结构,层层组合最终构成了宏观世界。化学的本质是物理的粒子运动,粒子运动其实是结构之间的关联,人类的情感、想法等都是细胞间的化学信号,而化学信号又是物理的粒子运动,本质又是结构之间的关联,所以我们人类的一切行为,生物的一切行为都是结构之间的关联。
所以,结构是“神”的语言,用结构和结构之间的关联来描述整个世界。
数据结构之于程序代码也同样如此,其是程序的根本,数据结构与之间的关联描述了整个程序世界,逻辑控制流依存于数据结构,若离开其,则程序将崩塌不复存在。
现在我们开始分析如何通过数据结构来理清数据流向。
由前文可知,数据的流向是由逻辑控制流来控制的,但是逻辑控制流本身就是复杂的,现在我们换个说法,数据的流向是由逻辑控制流来驱动的,逻辑控制流只是通过一些条件、规则来驱动数据进行流动,其流向并不是在写逻辑控制流的过程中而突然打通的,而是在写逻辑控制流之前,在数据结构定义的时候,就已经开始设计构思其流向了。只是几乎我们每一个程序员都忽略了这一点,我们错误的以为数据流向是存在于逻辑控制流之中的。
首先,程序的原作者在定义数据变量也即数据结构的时候,就已经在脑中设计构思其流向了,但他并没有在该数据结构上做说明来描述数据的流向。然后,他认为数据的流向应该用逻辑控制流来描述就可以,就够用了,所以就零零散散的用代码在逻辑控制流中进行描述。这就导致了别人不得不完整阅读逻辑控制流中的散碎的、反人类的代码来弄清楚数据的流向,原作者留给后来人的仅有这一堆蜿蜒曲折,支离破碎的代码,而清晰的导航地图只留在了原作者的脑海中,别人无法获取。只有神和原作者知道。时间久一点后,就只有神知道了。
阅读到此,我们已经非常确定数据的流向是隐含于数据结构之上的,只是数据结构太沉默,缺乏表达。而逻辑控制流又太活跃,喧宾夺主。
所以,我们要强化数据结构的表达,在数据结构上描述数据的流向,也就是结构之间的关联。
事实上,当我们把几个独立的数据结构,编排组织在一起成为一个较大的新的结构时,这种关联就已经建立了,只是我们并未对其进行更多的、更详细的描述,关联隐晦其中。我们只是把它当作了数据的存储,然后通过逻辑控制流再对其进行读读写写。
有两种方案可以更好的描述结构之间的关联。
一种是注释型方案,在数据结构上通过注解来解释并描述结构之间数据的流动,可以描述流动的方向性,然后通过预处理,静态编译来校验结构之间关联的正确性。我们需要实现一个静态的编译检查工具。
依然还是在逻辑控制流中对数据的流向进行控制,只是额外增加了在数据结构定义时的说明,弊端是程序员可以偷懒不进行注解,这样就又退化回了原始的蛮荒。更主要的弊端是,还是采取了以逻辑控制流为主的编码方式,也就保留了前文说过的逻辑控制流的所有缺点,虽然通过注解有了数据流向的地图,但是辅助作用有限,阅读并梳理代码还是有较大难度,治标不治本。
另一种方案是,以数据结构为主,以逻辑控制流为辅,也就是面向数据结构编程(数据结构化编程),而非面向过程。
注意,这不是为了提出新概念,也不是为了喊口号,而是为了改变以往的过程式思维方式,转到结构化思维方式上来。意识形态决定思维方式,思维方式决定实际行为。
这就是为何一直要强调结构的重要性来建立意识形态,从而塑造结构化思维,从而决定实际的代码编写。这个初始的思维转变可能不太习惯,请耐心些,因为我们已经越过了逻辑控制流的层层迷雾,开始直接面对程序的核心“数据结构”,握住了程序的命脉。
在结构化思维方式中,我们首先要改变把数据结构仅仅当作数据存储这一根深蒂固的看法。
或许你会认为这违背了以往的准则,离经叛道,数据结构就应该也仅应该当作逻辑控制的存储区域,这样才有简单性。貌似没错,但实际上整个程序代码变简单了吗?用事实说话就是并没有!唯一保持“简单”的只有数据结构,能不简单吗,就是声明定义了一个变量而已,当然简单了。但为此付出的代价是无比高昂的,仅为了保持变量声明的简单性,而不得不在逻辑控制流中来控制其流向,操纵其行为,通过各种令人绝望的骚操作来让其运动起来,引发绝望的复杂性。
这就好比,太阳和地球纯粹就是简单的球体而已,是固定静止的,没有地球绕着太阳旋转,而如果要让地球绕着太阳旋转的话,则只能由上帝额外施加一个 神力 来让地球绕着太阳旋转。如果我们的宇宙真是这样的话,我们永远无法弄清楚宇宙天体的运行规律,绝不可能!这直接丢弃了万有引力,而是通过神力来模拟引力的效果,而神力却是我们永远无法触及的,除非我们是神。更恐怖的是,一旦引入这个神力,就会破坏原来世界的平衡(蝴蝶效应),而为了维持平衡,则会引入更多的神力,从而陷入无尽的循环。一个充满神力的世界,是极致复杂的,永远不可能弄清楚。
而我们却习惯于在程序代码中充当神的角色,用各种神力来实现目的,并艰难的维护着平衡,以至于自己也难以看懂。摘去神力的面具,这其实是幽灵之力,魔鬼之力,地狱之力,想当神的我们,却实际成为了撒旦。
改变了看法之后,数据结构就不应该是一个静静的数据存储了,数据结构本身一定是有自己的规则。 就像太阳和地球一样,其自身就具有引力的规则,可以自我解释清楚,而不是借助外部的神力。宇宙的规则“物理”的简单之处也就是所有的规则一定是存在于其物质本身的,通过对物质本身的结构和规则进行探索,就能掌握万物的奥秘。
所以我们要在数据结构上定义规则,用其本身的规则来描述结构之间的关联。
在结构化思维方式中,我们思考的角度应该立足于数据结构上。 这就产生一个非常有意思的思考流程:
可以发现结构化的三大基本性质:易于读写、流向清晰、高度复用。
和高级特性:安全、并发。字段会对数据进行完备校验(不仅校验类型,也校验数据值是否符合预期规则),每个字段都是安全的,那么整个程序就会是安全的。由于字段赋值是不要求顺序的,因而可以有更好的并发性。
实际编程需要做的只有 2 、6 两步,这也正是结构化思维后数据结构的两个根本构成:字段和规则。在以往只有字段的情况下,新添加了规则这一个概念,整个程序世界变得完全不同了,开始变得清晰明朗了,一直萦绕的迷雾消失不见了,如透明般展示于我们的眼前。就像有了货币规则之后,人类的整个文明世界都变得不同了,社会飞速进步。
显而易见的,在走完整个思考流程后,最终我们对程序有了简单易懂的全新认知,获益是巨大的。整个程序如树形结构一般清晰的展示于我们眼前,对于我们阅读来讲,基本上只需要关心字段的输入和输出,而不必探究详细的实现,因为字段是如此的清晰易读。每一个子结构都可以使用同样的方式,扁平展开,阅读复杂度是恒定平缓的,而这在过程式编程中是无法想象的,是不可能的,其复杂度如蜗牛一般是螺旋状的,总能把人搞晕,代码量越多,复杂度就越恐怖。而现在我们终于找到了结构化编程,将复杂度变得平缓,有一种打破枷锁的解放感。
耳听为虚,眼见为实,接下来通过一个示例来体会下结构化编程的简单易懂。(此为演示代码,现在已经有 TypeScript 版可用: https://rainforesters.github.io/rainforest-js )
// 定义结构
// 定义跳跃的信号结构
typedef JumpSignal {
jump bool
event Event
stage Stage
}
// 定义规则函数
// 这个规则应该解读为:将用户的操作转化为跳跃信号
funcdef JumpSignal (
// 声明待观察的字段(可以有多个)
// 一旦所有字段都准备好(被赋值),则触发规则函数
event
) => {
if (self.event.type == "tap") {
self.jump = true // 为 jump 字段赋值,表示 jump 字段已经准备好
}
}
funcdef JumpSignal (
stage
) => {
self.stage.addEventListener("tap", (e) => {
self.event = e
})
}
typedef Avatar {
...
}
typedef Player {
avatar Avatar
jumpSignal JumpSignal
}
// 这个规则应该解读为:当跳跃信号发生时,将角色变成跳跃状态
funcdef Player (
jumpSignal {
// 可以直接观察子结构的字段,而无需关心 jumpSignal 是否为空,
// 因为,遵循结构化编程的思维,这里只需要声明待观察的目标字段,
// 也就是,我们只期望 jump 准备好,
// 这也就间接表明了 jumpSignal 肯定会准备好。
// 这种简单直白的条件声明,是以往过程式编程所无法实现的。
jump
}
) => {
if (self.jumpSignal.jump) {
self.avatar.state = "jump"
}
}
func main() {
// 初始化结构实例
// avatar jumpSignal 会自动初始化
const player = typeinit(Player)
player.jumpSignal.stage = stage // 将舞台赋值给跳跃信号
}
核心是以数据结构为主,在其上定义规则,充分利用规则的被动自动执行。
整个程序的结构现在是完全可视的了,子结构都层次分明,职责明确,可以快速的弄懂结构的意图,梳理整个程序的架构,理清数据的流向。我们能避免像以往过程式编程那样掉入逻辑漩涡,无法自拔,可以松快的读写代码。
以往的过程式编程的写法是,需要通过逻辑控制流,一步一步小心翼翼的线性编写,同时还要考虑最令人烦恼的分支判断等,需要费很大的力气来编写。
现在,编程方式开始有变化了,通过结构化编程,我们获得了三个恒定的写法:
得益于结构的高复用性,所以,最常用的写法就是为字段赋值,这个活很好干。不用引入逻辑控制流,不用操心赋值顺序,只要需要被赋值,那就赋值好了,只要需要准备的都准备好了,系统会聪明的遵循规则自动运行。这就像是在设置配置文件,简单直白,通俗易懂。
可能依然有些同学会觉得上述示例晦涩难懂,发现不了结构化编程的好处,说白了就是一时还没有转过弯来,依然用过程式的思维来阅读上述代码,旧思维太固执,太根深蒂固。
正如前面花大力气强调的,一定要认知到结构的重要性,一定要改变数据结构是数据存储的固执看法,一定要秉承结构化的思维,以数据结构为主,着眼于结构本身去理解规则。 意识形态决定思维方式,思维方式决定实际的阅读和编写行为。
总之,要扭转过程式思维到结构化思维上来,想通了,就明朗了。
使用结构化编程,我们能获得一些前所未有的令人欣喜的好处,如我们可以直接从结构化的三大基本性质(易于读写、流向清晰、高度复用)中获益,也可以从更高级的安全和并发特性中获益。还有就是得益于结构化编程的新写法,我们可以实现更多工具在静态检查阶段或运行阶段,通过可视化的方案来一览整个程序的数据流向,可以知道哪些字段是输入数据;哪些字段是输出数据;哪些字段是被依赖的,又间接依赖哪些字段;哪些字段已经准备好;哪些字段尚未准备好,等等等等。 这在以往的过程式编程中是难以实现的。
还有一个不得不说的性质:高度复用。
同样得益于结构化编程的新写法,我们统一了结构引用定义的方式,和为字段赋值的方式。
这就表示,你自己定义的或者他人定义的结构,都能够复用、通用。这也表示了,如果大家开放自己定义的结构给所有人使用,那么随着积累、沉淀,我们写的代码将会越来越少。
以往,我们受限于组件的不可检索性,只能通过一个项目的文字简介来获知是否对自己适用,而现在得益于结构的完整自描述性(自洽),可以归纳出输入字段的类型和输出字段的类型,构建索引,从而可以提供终端的用户检索。
同时,由于数据结构代表数据本身,可以方便进行序列化,这就表示,可以抹平不同编程语言之间的差异,可以方便的跨语言执行规则,也易于分布式执行规则。
另一个不得不说的特性:安全。
毫不客气的讲,绝大多数程序员最容易忽视的就是安全,除了讨厌写注释之外,就是讨厌写安全校验,不喜欢穿衣服就出门。还有一个长久困扰程序员的麻烦是 Debug,除去明显的逻辑错误,大多数 Debug 的过程其实是把异常数据校正的过程,但是追踪问题根源却要耗费九牛二虎之力。
现在,受益于结构的自洽,我们也可以让类型自洽,对类型进行自我描述,不但要校验输入数据的类型,同时也校验数据的值是否合规,是否符合预期。也即类型是可以有明确语义的,可信的,可以预期的,是安全的。
比如期望是偶数,那么就定义一个偶数类型,然后作为数据结构的字段类型,那么对任何偶数类型字段的赋值都会自动进行校验,校验的目的就是要确保符合预期,保持确定性。
每个字段都是符合预期的,那么整个结构就是符合预期的,从而整个程序也是符合预期的,是安全的。整个程序的状态在任何时刻都是确定的,任何时刻都是安全的。
清晰的数据流向也提高了 可维护性。
现在,整个程序的流向地图不是隐藏于原作者的脑海了,在编写程序的时候就已经开始绘制出来了,这对于后续的维护者来说是天大的福音。
同时,得益于上面所说的安全,让结构保持在确定的、可以预期的状态,这也能减少 Bug 的产生,降低 Debug 的难度。
结构化思维带来的编程写法也更有助于 多人协作。
多人协作的开发方式,提高了开发速度,但也增加了整个程序的不确定性和沟通成本。每个人的思维逻辑都不同,所以代码实现也五花八门,对接接口也千奇百怪。再好的开发规范,也只能约束代码的编写行为,却没办法规范统一人们的逻辑实现。不同的对接风格,增加了沟通成本,也无法保证确定性。
遵循结构化思维,能够将开发人员从以往的主观逻辑实现,拉回到客观的结构规则上来,消除主观差异性,回归到结构本质的输入和输出,令其自洽,易于大家共识。为字段赋值的写法,也简单直白的统一了对接风格,降低沟通成本。并且得益于安全特性,能够更好的消除多人协作带来的不确定性,以更低成本,保证整个程序的安全稳定。
结构化编程的方式,除了常规的前端、后端,对于运维等也是大有裨益,就像写配置文件。
由于结构的自洽性,其相当于是物理概念的物质,所以理论上只要结构自身的规则准确,那么可以更容易的对任何场景进行模拟,比如对互相干涉性较强的星系模拟、气候模拟、神经模拟等也非常有潜力。
如前所述,此文的意义在于,打破根深蒂固的陈旧过程式编程观念和思维,找到结构化编程的新观念、新思维,站在更好的视角,拥有更广阔的视野,重塑对程序的认知,建立新的意识形态,发挥优势,提高生产力。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.