type
status
date
slug
summary
tags
category
icon
password
《软件设计的哲学》作者、斯坦福教授John Ousterhout:AI编码越多,软件设计越重要
看到前几天播客The Pragmatic Engineer对斯坦福大学计算机科学教授John Ousterhout做了一个访谈,谈AI时代的软件。John Ousterhout可能不用多做介绍,他是《软件设计的哲学》(A Philosophy of Software Design)一书的作者,他不仅在学术界有深厚造诣,还创办过两家科技公司,在Sun Microsystems工作过,创造了TCL脚本语言,并发明了被MongoDB、CockroachDB、Kafka等系统广泛使用的 RAFT 共识算法。
一、AI工具与软件工程的未来
John Ousterhout 认为,随着AI工具在软件开发中的应用越来越广泛,软件设计的重要性将会提升而非降低。他指出,当前的AI工具主要擅长生成低层次的代码,类似于"术业有专攻的龙卷风"(tactical tornado),能够快速输出代码但可能缺乏整体设计思考。
"AI工具将使生成低层次代码变得更容易。自动补全将越来越好,它可能会产出相当高质量的代码。但大问题是,AI工具能在多大程度上取代更高层次的设计任务?目前,我在现有工具中还没有看到让我认为它们能做到这一点的迹象。"John解释道。
他进一步强调,随着AI工具处理更多低层次的编程任务,软件设计师的工作将更多地集中在设计上:"AI 处理越来越多的低层次编程任务,软件设计师的工作将越来越多地转向设计。软件设计将变得越来越重要。这将占据开发人员时间的更大比例,这使得我们在大学里根本不教授软件设计这一点变得更加令人遗憾。"
这个观点揭示了一个悖论:尽管AI可能改变代码生成的方式,但它可能使真正的软件设计技能变得更加稀缺和宝贵。学校教授的技能可能正是将被AI工具取代的技能,而设计思维这一核心能力反而没有得到足够重视。
二、软件设计的本质:分解复杂性的艺术
在讨论软件设计的本质时,John将其定义为一个分解问题的过程,它是应对复杂性的关键策略。这种分解是计算机科学中最重要的理念之一。
"我认为设计是一个分解问题。它是如何将一个大型复杂系统分解成可以相对独立实现的较小单元。当我做演讲时,我经常问人们,你认为计算机科学中最重要的理念是什么?对我来说,我认为是分解。这是贯穿我们在计算机科学中所做一切的关键。如何将大型复杂问题分解?这就是设计。"
John提出了处理复杂性的两种主要方法:一是完全消除复杂性,二是通过模块化设计隐藏复杂性。第一种方法是最强大的,但无法消除所有复杂性。第二种方法则是将相对复杂的事物放在一边,使得系统中的其他人不必意识到这种复杂性。
"好的软件设计会帮助我们应对复杂性。它既会给你消除复杂性的想法,也会通过模块化让大多数人不必意识到大部分复杂性的存在。"
这种思考框架帮助我们理解为什么某些设计决策比其他的更有效,它也解释了为什么适当的抽象和封装对于构建可维护系统如此重要。
三、"设计两次"原则:突破第一直觉的限制
John在书中提出的一个引人注目的观点是"设计两次"原则。他指出,许多聪明人常常实现脑海中出现的第一个想法,这使他们无法发挥真正的潜力。
"不幸的是,我经常看到聪明人实现脑海中出现的第一个想法,这使他们无法发挥真正的潜力。这也使他们变得令人沮丧,难以共事。"John在书中写道。
他解释了这一现象的根源,特别是在顶尖大学的研究生中:"所有这些年来,一切对他们来说都很容易。他们总是在所做的一切中表现最好。在高中,他们比老师更聪明。在大学,也许和教授一样聪明,班上最优秀。他们头脑中冒出的第一个想法总是足够好,能获得优异的成绩。所以他们从来没有真正的动力去思考两次。"
John分享了自己设计TCL Toolkit时的经历,他花时间开发了两种不同的设计,最终选择了第二种:"在比较之后,我最终选择了第二个想法。老实说,这是我职业生涯中最好的想法之一。TickleTK受欢迎的原因之一是TK的API是一个非常简洁、简单而强大的API。而它是我的第二选择,是我脑海中出现的第二个想法。"
这个原则提醒我们,即使是经验丰富的设计师也应该挑战自己的第一直觉,探索替代方案,这往往会带来更好的结果。
四、深度模块与浅层模块:软件设计的关键区别
在《软件设计的哲学》中,John引入了"深度模块"和"浅层模块"的概念,这成为了理解好的软件设计的关键框架。深度模块是指拥有简单接口但功能丰富的组件,而浅层模块则有着宽广的接口但功能相对简单。
"深度模块是我们对抗复杂性的杠杆。它通过提供非常简单的接口来实现这一点,使用该模块的人几乎没有认知负担,非常容易学习。但在模块内部,有大量的功能和复杂性对其他人隐藏。"John解释道。
他强调,深度模块捕捉了一种权衡:"基本上,这是接口复杂性和模块功能之间的权衡。你想要做的是,在最简单的可能接口下获得最多的功能。"
这个概念之所以强大,是因为它为评估设计决策提供了一个明确的标准。好的设计往往会创建深度模块,让使用者能够获得强大的功能,同时不必理解实现细节的复杂性。这种思维方式对于构建可维护和可进化的系统至关重要。
五、错误处理:将错误定义出局的艺术
John在书中有一章题为"将错误定义出局",探讨了错误处理对软件复杂性的巨大影响,以及如何通过设计减少错误处理的负担。
"任何构建过大量软件的人都知道,错误处理是复杂性的一个巨大来源。基本上,它是所有特殊情况,所有你必须处理的奇怪特殊情况。因此,错误处理很容易给软件带来巨大的复杂性。"
John提出了一种思路,通过设计改变来消除某些类型的错误,而不是仅仅处理它们:
"在那一章中,我试图论证的是,异常越多并不是越好。有时候它是必要的,但有时会设计者认为'我从一个类中抛出的异常越多,我就是一个越好的程序员,我是一个更谨慎、更小心的程序员'。
但问题是,你抛出的每一个异常都给你的类的用户增加了复杂性。所以如果你能减少抛出的异常数量,减少你产生的特殊情况的数量,那将减少系统复杂性。"
他提到,通过稍微改变系统设计,整类错误可能会消失:"我在那一章中给出了很多例子,事实上,仅仅通过对系统设计的轻微改变,整类错误就会消失。它们不可能发生。没有错误需要处理。"
不过,John也警告这种方法需要谨慎使用:"这是我必须警告人们的一章。真的很容易把这一点理解错误。这就像一种调味料。你在烹饪中使用微量就能得到好结果,但如果使用太多,就会一团糟。"
六、软件设计中的共情能力:设计思维的关键特质
John谈到了在软件设计中一个被低估但至关重要的特质——共情能力,或者说能够从不同视角思考问题的能力。这一特质对于创建真正以用户为中心的设计至关重要。
"我认为一个真正优秀的设计师最重要的属性之一是,他们能够改变思维方式,从非常不同的视角思考问题。所以当我设计一个模块时,我会思考该模块的所有细节。但随后我可以改变思维方式,思考这个模块的用户,并意识到我不想了解那些细节。"
他进一步解释,这种能力不仅在工程环境中有价值,在社交环境中同样宝贵:"我认为这种技能在社交环境中有着巨大的价值,就像在工程环境中一样。能够从其他人的视角思考问题的能力。顺便说一下,我喜欢计算机科学的一件事是,人们认为我们是这些呆板、书呆子般的人,但我们在计算机系统中使用的许多想法实际上在社交系统中也有有趣的类比。"
这种洞察揭示了为什么某些经验丰富的工程师能够创建特别优秀的设计——他们能够超越自己的思维框架,真正理解他人的需求和困惑。在软件设计中培养这种共情能力,可能与学习技术技能同等重要。
七、设计评审与白板思考:集体智慧的力量
John强调了设计评审和集体讨论在软件开发中的价值,尤其是使用白板进行实时协作的效果。他认为,通过汇集多人的想法,几乎总能产生更好的设计。
"如果你能让多个头脑思考一个话题,很明显你会想出更好的想法,比只有一个人做要好。"
他分享了一种特别有效的白板技术,用于解决复杂问题:"我经常使用一种技术来使用白板解决复杂问题,这些问题甚至可能不是技术性的,而是公司中其他类型的管理问题。我发现人们在会议中经常互相讲不到点子上,重复相同的论点和反驳,就这样兜圈子,永远无法达成结论。所以在这些情况下,我会站在白板前,列出所有支持和反对的论点。讨论的规则是,你可以提出任何你认为合理的论点,没有人可以告诉你你的论点必须被删除。每个论点都会被写在白板上。但是你不允许重复已经在白板上的论点。"
这种方法的神奇之处在于它如何将看似分歧的讨论转变为共识:"这真的很重要。所以让论点显示出来,让每个人都能看到。每个人的论点都是有效的。每个人都可以贡献。没有人可以阻止你把你的论点写在白板上。所以每个人的论点都会被记录。然后讨论自然会达到一个没有人再有话要说的点。这样就缩短了讨论时间。然后我会进行一次投票。对我来说令人惊讶的是...有时人们会在双方激烈争论,你会认为存在完全的分歧。然后在最后,我们进行投票,结果往往是一致的。真令人震惊。"
这种集体设计方法不仅提高了结果质量,还促进了团队凝聚力和共识,这对于软件项目的成功至关重要。
八、对测试驱动开发(TDD)的批判性思考
John对测试驱动开发(TDD)持批判态度,认为它可能不利于良好的软件设计。尽管他强调单元测试的重要性,但他认为TDD鼓励的工作流程可能会导致过度战术性的开发方式。
"我不是TDD的粉丝,因为我认为它不利于设计。对我来说,软件开发过程应该以设计为中心。我认为开发过程中我们所做的一切都应该围绕获得最佳设计来组织。我认为TDD不利于这一点,因为它鼓励你做一点点增量设计。我写一个测试,然后实现功能使该测试通过。然后我再写一个测试,实现功能使该测试通过。在这个过程中没有任何点能鼓励你退一步,思考整体任务,大局。所有这些部分如何组合?什么是最令人愉悦、最简单、最干净的架构,可以解决10个问题,而不是为单个问题想出10个点解决方案?"
John认为,这种增量方法可能导致过于战术性的开发风格:"它导致的风险是,你可能会陷入一种极其战术性的开发风格,产生可怕的结果。"
他提出的替代方法是按抽象单位进行开发,而不是单个测试:"我认为开发的单位应该是抽象,而不是单个测试。这块太小了。你想要思考足够大的块,以便考虑权衡,并尝试提出相当通用的解决方案,可以解决许多问题。"
这种观点挑战了软件工程中一些流行的实践,强调了更全面的设计思考在开发过程中的价值。
九、代码注释:价值与最佳实践
关于代码注释,John持有与许多现代观点相反的立场。他认为,虽然代码应该尽可能自解释,但注释仍然是必要的,能提供代码本身无法表达的信息。
"如果代码能完全自解释,那会很wonderful。我不会有任何异议。但它不能,而且永远不会。"John坚定地表示,"代码中有太多东西无法从代码本身描述出来。"
John认为,注释对接口特别重要:"我认为注释最重要的地方是接口。这里它们真的非常非常重要,因为接口的假设是,你不希望人们不得不阅读你正在与之沟通交流的事物的代码。你只希望他们看接口。在模块的函数签名中,不可能提供人们使用该模块所需的所有信息。所以这里是注释最重要的地方。"
他还指出了类成员变量文档的重要性:"第二重要的是记录类的成员变量。这需要非常广泛的文档。"
关于方法内部的注释,John持有更谨慎的态度:"在方法内部,我倾向于不需要太多注释,因为如果你知道该方法试图做什么,代码通常会在那里很好地自我表达。我倾向于在事情很棘手或有我只在发现bug时才发现的意外情况时添加注释。但通常我的方法内部不会有任何注释,只有接口注释。"
John的观点提醒我们,虽然干净的代码是理想的,但适当的注释能显著提高代码的可维护性和可理解性,特别是对于那些需要使用你的代码但不一定了解所有实现细节的人。