现代面向对象编程最佳实践-全-

现代面向对象编程最佳实践(全)

原文:zh.annas-archive.org/md5/e917250e64ed34cdb1d4bccab9b9e5ad

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:前言

关于

本节简要介绍了作者和本书的内容。

关于本书

你的经验和知识始终会影响你采取的方法和编写程序时使用的工具。通过对如何实现目标以及使用哪些软件范式有良好的理解,你可以快速高效地创建高性能的应用程序。

在这本两卷书中,你将发现面向对象编程未被充分利用的特性,并使用其他软件工具来快速高效地编写应用程序。本书的第一部分从讨论面向对象编程的当前应用开始,然后分析面向对象编程没有解决的问题。接着,它通过分解面向对象编程的复杂性,向你展示其根本上的简单核心。你会发现,通过使用面向对象编程的独特元素,你可以学会更轻松地构建你的应用程序。

本书接下来的部分将讨论获得成为更好的程序员所需的技能。你将了解各种工具,如版本控制和构建管理,如何帮助你更容易地生活。本书还讨论了其他编程范式(如面向方面编程和函数式编程)的优缺点,并帮助你为项目选择正确的方法。最后,本书讨论了设计软件背后的哲学以及成为一名“优秀”的开发者意味着什么。

到本书两卷结束的时候,你将了解到面向对象编程并不总是复杂的,你将知道如何通过学习道德、团队合作和文档来成为一个更好的程序员。

关于作者

格雷厄姆·李是一位经验丰富的程序员和作家。他撰写了包括《专业 Cocoa 应用程序安全》、《测试驱动 iOS 开发》、《APPropriate Behaviour》和《APPosite Concerns》在内的书籍。他是一位从业时间足够长,以至于想要开始告诉其他人他犯过的错误,希望他们能避免重蹈覆辙的开发者。在他的情况下,这意味着他作为一名专业人士工作了大约 12 年。他的第一次编程经历几乎不能称为专业:那是在 Dragon 32 微型计算机上用 BASIC 完成的。他为用 Perl、Python、Bash、LISP、Pascal、C、Objective-C、Java、Applescript、Ruby、C++、JavaScript 编写的软件获得了报酬,可能还有一些其他的东西。

请在Twittertwitter.com/iwasleeg)、quitterquitter.se/leeg)或我的博客sicpers.info)上找到我,对这里的内容进行评论或提问。

学习目标

  • 通过将其分解为其基本构建块,解开面向对象编程的复杂性。

  • 充分发挥面向对象编程(OOP)的潜力,设计高效、可维护的程序。

  • 利用编码最佳实践,包括测试驱动开发TDD)、结对编程和代码审查,以提高你的工作质量。

  • 使用工具,如版本控制和集成开发环境(IDEs),以提高工作效率。

  • 学习如何与开发者最有效地合作。

  • 构建你自己的软件开发哲学。

目标受众

本书非常适合想要理解软件开发背后的哲学以及“设计软件优秀”意味着什么的程序员。想要解构面向对象(OOP)范式并看到它如何以清晰、直接的方式重建的程序员也会发现这本书很有用。要理解本书中表达的思想,你必须是一位经验丰富的程序员,并且希望发展自己的实践。

方法

本书采用自传体方法来解释各种概念。本书中的信息基于作者的观点和期望的未来方向。作者介绍了关键思想和概念,然后详细解释它们,概述它们的优缺点,并指导你如何在你的开发中最有效地使用它们。

致谢

本书是长期研究活动的成果,我希望我所依赖的任何工作都得到了适当的引用。尽管如此,这里的思想并非完全是我个人的(这种区别留给了错误),而且许多在线对话、会议和与同事的交流塑造了我对对象思考的方式。构建一个完整的列表是不可能的,一个不完整的列表也是不公平的。所以,我只想说谢谢。

第一部分 – 面向对象编程的简单方法

什么是面向对象编程?我的猜测是,面向对象编程将在 20 世纪 80 年代成为像结构化编程在 20 世纪 70 年代一样。每个人都会支持它。每个制造商都会推广他的产品以支持它。每个经理都会口头上支持它。每个程序员都会实践它(不同)。但没有人会真正知道它是什么。

Tim Rentsch面向对象编程dl.acm.org/citation.cfm?id=947961

)

面向对象编程OOP)起源于 Simula 编程语言的模拟功能,但由施乐帕洛阿尔托研究中心的 Smalltalk 团队著名地开发和推广。他们设计了一个旨在个人使用的计算系统,通过在计算机上模拟现实世界问题,使孩子们能够同时了解世界和计算机,从而拥有一个易于儿童使用的编程环境。

我最近研究了 OOP 从 PARC 传播到更广泛的软件工程社区的情况,这构成了我的论文《我们需要(小型)谈话:带有图形代码浏览器的面向对象编程》的背景——www.academia.edu/34882629/We_need_to_Small_talk_object-oriented_programming_with_graphical_code_browsers。我发现的结果让我感到困惑:这种简单的儿童构建计算机程序的设计语言是如何变得如此复杂和麻烦,以至于专业软件工程师在将其宣布为失败并寻求其他范例之前,都难以理解它?

我书架上的一本教科书,“A Touch of Class”,由 Bertrand Meyer 所著,声称是一本“革命性的入门级编程教科书,使学习编程变得有趣且有益。”876 页的篇幅使其成为一项很好的锻炼:不是针对学校儿童,而是针对“进入计算机科学领域的学生”。

深入挖掘表明,对象思维、对象技术、OOP 或你愿意称之为什么的领域,已经受到两种力量的影响:

  • 加性复杂性。顾问、学者和建筑师们渴望在世界上留下自己的印记,他们扩展了基本的基本思想,以提供他们自己独特、可销售的贡献。虽然这些贡献在孤立的情况下可能很有价值,但它们的累积(正如我们将看到的,在某些情况下是故意累积的)产生了一个复杂的迷宫。

  • 结构化入门。为了使面向对象编程(OOP)看起来更容易、更易于访问,人们开发了针对现有编程工具和过程的“面向对象”扩展。虽然这使得访问 OOP 的可观察特性变得容易,但它讽刺地使得访问所需的思维转变更加困难,而这种转变本质上是一种思维过程和解决问题的技术。通过将对象模型纳入现有系统,技术人员注定使其停留在现有的思维模式中。

关于示例代码

在本书的这一部分,我故意选择尽可能使用“主流”的流行编程语言。我没有坚持使用任何一种语言,而是使用大多数经验丰富的程序员应该能够一眼看懂的语言:Ruby、Python 和 JavaScript 将是常见的。当我使用其他语言时,我这样做是为了表达特定的历史背景(Smalltalk、Erlang 和 Eiffel 将在这里普遍存在)或展示来自某些社区的思想(Haskell 或 Lisp)。

本书这一部分的一个观点是,作为认知工具,面向对象编程(OOP)并不特定于任何编程语言,实际上,许多被标榜为面向对象语言的编程语言使得(至少是大部分)面向对象编程变得更难。因此,选择任何一种语言作为示例代码意味着只能展示面向对象编程的一个子集。

第二章:第一章

反面

告诉对象要做什么

这个大思想是“消息传递”——这正是 Smalltalk/Squeak 的核心所在(而且这是我们 Xerox PARC 阶段从未完全完成的事情)。日本人有一个小词——ma——表示“介于两者之间”——也许最接近的英语对应词是“interstitial”。在构建伟大且可扩展的系统时,关键更多地在于设计模块之间的通信方式,而不是它们的内部属性和行为应该是什么。

艾伦·凯,(squeak-dev 邮件列表 — lists.squeakfoundation.org/pipermail/squeak-dev/1998-October/017019.html)

在大多数编程语言中(如 C++、Java、Python 等),调用对象的方法通常采用anObject.methodName()的形式,这意味着“在anObject实例所属的类或其祖先类中,将存在一个名为methodName的方法,请找到它并运行它,其中selfthis值被别名替换为anObject。”例如,在 Java 中,我们期望在anObject的类或父类中找到(非抽象的)public void methodName() { /* ... */ }

这个保证在调用者和持有方法的对象之间引入了大量的耦合:

  1. 调用者知道该对象是某个类的实例(与继承相关的问题如此之多,以至于它将在后面的章节中单独讨论)。

  2. 调用者知道对象的类或其祖先类提供了一个具有给定名称的方法。

  3. 该方法将在当前上下文中运行到完成,然后控制权将返回给调用者(这并不是从孤立的角度看语法就能特别明显,但确实是被假设的)。

放弃这些假设意味着什么?这将使该对象成为一个真正独立的计算机程序,通过基于消息传递的协议从远处进行通信。该对象做什么,它是如何做的,甚至它实现的是哪种编程语言,所有这些都是该对象的私有信息。它是与一个类协作来找出如何响应消息吗?那个类有一个父类还是多个父类?

消息传递背后的理念正是这种关注点的分离,但即使是基于消息传递方案构建的编程语言,通常也将其视为“查找方法”的特殊情况,只有在常规方法解析失败时才会遵循。这些语言通常有一个特定的命名方法,当请求的方法找不到时将会执行。在 Smalltalk 中,它被称为doesNotUnderstand:,而在 Ruby 中则称为method_missing()。每个方法都接收选择器(即调用者希望调用的方法的唯一名称)来决定如何处理它。这使我们获得更高层次的解耦:对象可以相互发送消息,而无需查看其他对象的实现以确定它们是否实现了匹配该消息的方法。

为什么这种解耦有价值呢?它让我们能够将对象构建成真正的独立程序,只考虑它们与外部世界的合同以及它们的实现如何支持该合同。例如,要求一个对象只有在它是包含具有相同名称的 Java 函数的类的实例时才会接收消息,即使是通过 Java 接口(Java 类可以提供的方法列表),我们就对消息接收者的实现做出了一系列假设,将它们转化为程序员在构建发送者时必须处理的约束。我们没有独立、解耦的程序通过消息接口协作,而是一个具有有限模块化的刚性系统。理解一个对象意味着引入关于系统其他部分的信息。

这不仅仅是一个学术上的区别,因为它限制了真实系统的设计。考虑一个用于可视化公司员工某些信息的应用程序,这些信息存储在键值存储中。如果需要视图和存储之间的每个对象都知道所有可用的方法,那么我必须在应用程序的每个地方重复我的数据模式,通过定义像salary()payrollNumber()这样的方法,或者提供无意义的通用接口,如getValue(String key),这些接口移除了我正在处理的人的表示的有用信息。

相反,我可以说对我的Employee对象:“如果你收到一个你不认识的消息,但它看起来像键值存储中的一个键,就回复你找到的那个键的值。”我可以说对我的视图对象:“如果你收到一个你不认识的消息,但Employee在响应它时给你一个值,准备那个值以供显示,并使用选择器名称作为该值的标签。”行为——在键值存储中查找任意值——保持不变,但消息网络告诉我们更多关于为什么应用程序正在做什么的信息。

通过提供像method_missing这样的延迟解析路径,像 Ruby 这样的系统部分地消除了这些假设,并提供了工具来启用更大的解耦和网络中对象的独立性。为了充分利用这一点,我们必须改变使用的语言和思考这些功能的方式。

一本关于 Ruby 中面向对象编程(OOP)的指南可能会告诉你,方法是通过名称查找的,但如果失败了,类可以可选地实现method_missing来提供自定义行为。这完全相反:说对象是一系列命名方法,直到它们不再起作用,这时它们获得了一些自主性。

翻转这种语言:一个对象负责决定如何处理消息,一个特别的便利之处是它们会自动运行与接收到的选择器匹配的方法,而无需任何额外处理。现在你的对象真正成为一个自主的演员,响应消息,而不是在程序性程序中存储特定命名例程的地方。

有一些对象系统公开了这种关于对象的想法,一个好的例子是 CMU Mach 系统。Mach 是一个操作系统内核,它通过消息传递在(相同或不同的任务)线程之间提供通信。发送者不需要知道接收者除了其端口(放置输出消息的地方)以及如何安排消息放入端口之外的信息。接收者对发送者一无所知;只知道在其端口上出现了一条消息,可以对其进行处理。这两个对象可以在同一个任务中,或者甚至不在同一台计算机上。它们甚至不需要用同一种语言编写,只需要知道消息的内容以及如何将它们放入端口。

在面向服务的架构世界中,一个微服务是一个独立的程序,它通过一个松散耦合的接口与同伴协作,该接口由通过某些实现无关的传输机制发送的消息组成——通常是 HTTPS 或协议缓冲区。这也听起来很像 OOP。

采用微服务的用户能够使用不同的技术实现不同的服务,仅从它们如何满足消息合同的角度来考虑给定服务的变更,并且可以独立替换单个服务,而不会影响整个系统。这也听起来很像面向对象编程(OOP)。

设计一个对象

面向对象的方法试图通过抽象出知识并将其封装在对象中来管理现实世界问题中固有的复杂性。找到或创建这些对象是结构知识和活动的问题。

丽贝卡·维夫斯-布洛克、布莱恩·威尔克森和劳伦·维纳,面向对象软件设计

面向对象编程的早期目标是通过将大问题“设计这个大型系统来解决这些问题”分解为小问题“设计这些小系统”以及“将这些小系统组合起来以共同解决问题”,从而简化软件系统设计的工作。布拉德·科克斯,这位构建了 Objective-C 语言并共同创立了一家利用该语言的公司面向对象技术专家,撰写了一篇名为“如果有一个银弹...而竞争者先得到了它?*”的文章,他在文章中声称面向对象代表了软件复杂性的显著降低。

在最广泛的意义上,“面向对象”指的是战争而不是武器,是目的而不是手段,是目标而不是实现它的技术。这意味着以对象为导向而不是以过程为导向来构建它们;运用程序员可以调动的所有工具,从经过充分验证的古董如 Cobol 到尚未出现的如规格/测试语言,以使软件消费者能够使用我们所有人都用来理解日常经验中可触摸对象的常识技能来推理软件产品。

这意味着放弃传统的以程序员-机器关系为中心的过程中心范式,转而采用以产品为中心的范式,其中生产者-消费者关系处于中心。

尽管如此,许多“面向对象”的设计技术仍然依赖于将系统作为一个整体来考虑,从头开始构建定制的、手工制作的对象,这些对象将构成满足客户需求的系统。从这个意义上说,科克斯的愿景尚未实现:他希望有“软件工业革命”,其中标准化的组件(软件-集成电路,类似于电子设计中的集成电路)可以根据其外部可见的行为进行指定,并组合成与当前任务相关的系统。相反,我们仍然有一个手工艺行业,但现在我们每次构建的应用特定组件被称为“对象”。

这种方法——将整个系统设计为一个单一的软件产品,但将各个部分称为“对象”——被称为面向对象分析与设计。通常,它被表达为根据解决问题的数据分解大问题的方法,因此面向对象编程成为功能编程的“替代品”,在功能编程中,大问题是根据其解决方案中使用的操作来分解的。尼尔·萨维奇在 2018 年的“使用函数简化编程”中未标注标题的表格——dl.acm.org/citation.cfm?id=3193776——描述了面向对象的术语:

抽象的核心模式是数据本身,因此一个术语的价值并不总是由输入(有状态方法)预先决定的。

函数式编程这个术语被描述为:

抽象的中心模式是函数,而不是数据结构,因此一个术语的值总是由输入(无状态方法)预先确定的。

不要理会像 Haskell 这样的“功能性”语言有处理状态的机制,或者我们可能想要解决的世界上的许多问题既有有状态和无状态方面!

这种对象作为数据的思想确实有其根源在面向对象编程运动中。在他的 2009 年教科书“A Touch of Class”的第 2.3 节“什么是对象?”中,Bertrand Meyer 使用了以下定义:

对象是一个软件机器,允许程序访问和修改数据集合。

这与我们所听说的“封装”或“数据隐藏”的通常目标正好相反,我们试图禁止程序访问和修改我们的数据!在这个观点中,我们将对象视为“软件机器”,这很好,因为它暗示了一种独立、自主的功能,但不幸的是,我们得到了这样一个观点:这个机器的目的是照顾程序中使用的整体数据集合中的一小部分数据。

正是这种心态导致了对象作为“活跃结构”,如下面的典型 C# 示例:

class SomeClass
 {
   private int field;
   public int Field => field;
 }

这满足了我们对封装的要求(字段是私有的),以及我们需要对象允许程序访问和修改数据集合的要求。我们最终得到的结果与一个普通的数据结构没有区别:

struct SomeClass
 {
   int Field;
 }

例外的是,C# 示例要求在每次访问字段时进行函数调用。这并不是真正的封装;拥有自己字段的物体无法猜测这些字段的状态,而包含此类物体的系统只能通过考虑整个系统来理解。我们原本希望将大问题分解为小问题的优势已经丧失。

对象作为数据方法的贡献者似乎是对将面向对象编程与软件工程相结合的尝试,这是一个始于 1968 年的研究领域,旨在通过让非常聪明的计算机科学家思考产品设计与构建可能是什么样子,而不去询问任何人,将产品设计与构建技能带给计算机科学家。过程密集型和设计工件密集型的系统、方法和“方法论”(这个词曾经意味着“方法的研究”,直到自负的软件工程师将其用作“方法,但更长的词”)推荐决定对象、它们的方法和属性;涉及的数据;以及数据的展示和存储的细节,所有这些都在满足用例的名义下进行,这是软件工程术语,意为“某人可能想要做的事情”。

Craig Larman(1997 年)的“Applying UML and Patterns”的内部封面有 22 个详细的步骤,在构建产品之前需要遵循。

我们可以将对象视为我们试图解决的某个问题的模拟,而与模拟互动是学习的好方法。如果我们的对象仅仅是代表程序持有一些数据的活跃结构,那么我们就无法获得这种好处:我们无法与模拟互动,除非构建出整个程序的其他部分。实际上,这正是许多使用对象的“工程”过程背后的目标:虽然他们可能口头上支持迭代和增量开发,但他们仍然谈论一次性构建一个系统,每个对象都是一个拼图碎片,能够满意地填补拼图中的特定空隙。

因此,让我们回到 Bertrand Meyer 的定义,并移除让程序访问对象数据的那个有问题的部分:

对象是一个软件机器

机器是一个有用的类比。它是一个设备(所以是人们建造的东西),它使用能量产生某种效果。注意,没有任何关于机器如何产生这种效果、如何消耗其材料或如何提供机器输出的陈述。我们有一个执行某种操作的东西,但如果我们想要将这些事物组合起来执行其他操作,我们就需要知道如何进行这种组合。添加一个约束使我们从“它是一个机器”转变为“我们可以这样使用这个机器”。

对象是一个可以通过发送和接收消息与其他软件机器协作的软件机器

现在我们有了可以执行操作并且可以一起使用的东西。我们不限制每个机器执行的操作的复杂程度(所以预订航班和表示数字都是我们可以构建机器来执行的操作);只是我们如何将它们结合起来。这也与 Brad Cox 的软件 IC 类比有相似之处。一个“集成电路”可以是任何东西,从NAND 门UltraSPARC T2。如果我们知道如何处理它们的输入和输出:每个引脚上应该出现什么电压以及它代表什么,我们就可以将任何 IC 组合在一起,无论大小。

这个类比告诉我们,我们的软件系统就像一个大型机器,通过组合、供电和使用较小的组件机器来完成有用的操作。它告诉我们关注一个机器输出的东西是否可以作为另一个机器的输入是有用的,但不需要担心每个机器内部发生的事情,除非是在维护这些机器的受限环境中。它告诉我们,在每个点上考虑我们拥有的机器是否比没有这个机器更有用,而不是追踪构建一个全能超级机器的进度。

它甚至告诉我们,建立一个将某种类型的输入转换为某种类型输出的装配线是我们可能想要做的事情;否则,我们可能认为这是功能程序员的专属领域。

绘制一个对象

我看到一扇红门,我想把它漆成黑色。不再需要任何颜色,我想要它们变成黑色。

滚石乐队,漆它黑色

如果面向对象编程是建模软件中问题的活动,那么软件团队用来传达这些对象特性和行为的图表(和口头描述)就是元建模——模型的建模。例如,规则,使用元元模型时隐含的约束:描述模型模型的模型如何工作的模型。

统一建模语言

在过去的时间里,许多这样的系统(从现在起我将避免使用“元元模型”这个词)被用来描述对象系统。UML(统一建模语言)是结合三种先前技术的结果:三位精灵国王,Grady Booch,Ivar Jacobson 和 James Rumbaugh 弯曲他们的力量戒指(分别代表Booch 方法面向对象软件工程对象建模技术——后者今天主要因为著名的设计模式书中大多数图表都是按照其规则绘制的)到一个理性的戒指,由 Mike Devlin 掌握。

顺便说一下,Rational 最初是一家制造更好的 Ada 应用程序和其他 Ada 程序员工具的公司,以便他们制造更好的 Ada 应用程序,包括 R1000 工作站,优化运行 Ada 程序,并具有集成开发环境。R1000 没有起飞,但 IDE 的想法确实如此,通过他们 Rose 产品的几代迭代(以及 UML 和 Rational 统一流程),在改变组织规划、设计和构建软件的方式上取得了重大进展。

UML 及其前身建模技术,代表了一种全面的方法来处理对象建模,其中所有实现方面都可以用图表表示。实际上,存在将 UML 转换为兼容语言(如 Java)以及反过来将 UML 表示转换回 UML 的工具。

你创建的模型,既要包含足够多的系统“业务”方面的内容以展示你已经解决了问题,又要包含足够多的实现方面的内容以生成可执行程序,这实际上不是一个模型,它就是程序源代码。在追求完整性的过程中,UML 建模工具系列完全忽略了“建模”,而只是引入了另一种实现语言。

如果消息传递的目标是通过众多小型、独立计算机程序的协同操作来解决我们的重大问题,这些程序通过通信协议松散耦合,那么我们应该能够通过两种透镜之一来看待每个对象:内部或外部。实际上,边界本身也值得特别考虑,因此有三个视角:

  1. “外部”透镜:我能向这个对象发送什么消息?我需要安排什么才能发送它们?我能期待什么结果?

  2. “内部”透镜:这个对象对其消息有何反应?

  3. “边界”透镜:这个对象的行为是否满足外部期望?

这最后两点紧密相连。确实,一些流行的实施学科,如测试驱动开发,引导你只通过边界透镜实现对象内部,通过说“当收到这个消息时,我需要这样做”,然后安排对象的内部结构,使其确实发生。

第一部分与其他部分是分开的。从对象的外部来看,我只需要知道我可以要求它做什么;如果我还需要知道它是如何做到的或者内部发生了什么,那么我就没有将我的大问题分解成独立的小问题。

UML 类图包括所有级别的可见性:公共、包、受保护的和私有的类特征;同时显示。要么它们显示了大量的冗余信息(这对图表没有好处),要么它们期望模型者采取全面的方法,一次解决整个大问题,使用“对象”这个词给他们的解决方案增添一些 1980 年代高科技的感觉。这是从波奇早期方法中的下降发展,其中对象和类被表示为蓬松的云形状的东西,支持这样的想法:里面可能有一些动态和复杂性,但当前并不相关。

有趣的是,正如在“分析和设计”部分中探讨的,贝特朗·梅耶的声明“一个对象是一个软件机器,允许程序访问和修改一组数据”,我们可以在格雷迪·波奇 1991 年出版的《面向对象设计与应用》的第一章中找到他一句话就超越了建模工具世界的点。

注意

或许有一个普遍原则,即关于制作软件的句子左半部分总是比右半部分更有价值。如果是这样,那么(敏捷宣言agilemanifesto.org/)就是我们的历史上最具洞察力的设计文件。

这句话是这样的:

面向对象设计的根本概念是应该将软件系统建模为协作对象的集合...

到目前为止,一切顺利。

...将单个对象视为类的实例...

我建议这并不必要,并且类,尤其是继承,在这个书的这部分应该有自己的一节(参见找到运行方法部分)。

...在类的层次结构中。

而在这里,我们完全分道扬镳。通过将他的对象置于“类的层次结构”中,波奇确实鼓励我们思考整个系统,按分类法关联对象并定义共享特征。这源于一个良好的意图——继承长期以来被视为面向对象实现重用的方式——但促进了关于重用的思考,而不是关于使用的思考。

类-责任-协作者

正如 UML 代表了一种描述对象的方式的发展快照一样,CRC 卡片也是由 Kent Beck 和 Ward Cunningham 在 1989 年引入,并由 Rebecca Wirfs-Brock、Brian Wilkerson 和 Lauren Wiener 在他们合著的《面向对象软件设计》一书中传播的。

CRC 卡片描述了对象的三个方面,其中没有一个是对称冗余检查:

  • 名称

  • 对象的责任

  • 对象将需要与之协作的协作者

这种设计方法不仅关注对象的通信方面(责任将是我可以要求它做的事情,而协作者将是它要求做事的其他对象),而且还引入了一种有趣的拟人化元素。你和我可以各自拿一张卡片,扮演“对象”,通过对话解决问题,并以此推动我们对将要交换的消息的理解。

David West 在他的 2004 年著作《面向对象思考》中提出了对象立方体,通过增加五个面将 CRC 卡片扩展到三维:

  • 类实例的文本描述

  • 命名契约的列表(这些应该表明“类创建者的意图,即谁应该能够发送特定的消息”,在他的例子中都是“公共”或“私有”)

  • 对象所需的“知识”以及它将获得这些知识的指示

  • 消息协议是对象将响应的消息列表

  • 对象生成的事件

一些坏消息:你不能用 3x5 索引卡片做一个立方体;你也买不到 5x5 索引卡片。但这只是个插曲。同样,就像使用 UML 一样,我们必须在同一个地方记录我们对象的内情和外情,而现在我们需要用大架子而不是索引盒来存储它们。

使用这两种技术,其演变似乎是一种累加的复杂性。是的,你可以绘制出对象和消息的网络,哦,而且当你在这里的时候,你还可以做……

从理性上讲,这些元模型中的每一部分似乎都有意义。当然,在某个时候,我需要考虑这个对象的内情;在某个时候,我需要考虑它的实例变量;在某个时候,我需要规划对象发出的事件。是的,但不是在同一个时候,所以它们不需要在同一模型上同时可见。

果冻甜甜圈和足球

具有讽刺意味的是,确实有一种对象图的形式可以清楚地区分外部和内部,尽管我只在一个地方见过:NeXT(以及随后的苹果)的果冻甜甜圈模型www.cilinder.be/docs/next/NeXTStep/3.3/nd/Concepts/ObjectiveC/1_OOP/OOP.htmld/index.html 这不是一个程序员用来设计对象的工具:它是一些文档中使用的类比。

这是一种一些作者不认同的类比。在《面向对象思考》一书中,大卫·韦斯特说,松露甜甜圈模型(他称之为足球模型,以肯·奥厄的名字命名)是“传统开发者”的选择模型,而“面向对象思考者”则会将对象拟人化,使用人作为代表。

韦斯特可能会争辩说,松露甜甜圈/足球模型代表了传统思维,因为它反映了梅耶式的观点,即你的系统是通过确定它需要哪些数据,然后在不同对象之间划分这些数据来设计的。具有讽刺意味的是,伯特兰·梅耶可能会因为一个无关的原因而拒绝足球模型:Eiffel 遵循统一引用原则,其中对象字段或成员函数(方法)使用相同的符号访问。对于 Eiffel 程序员来说,数据“被方法包围”的想法是多余的;甜甜圈表明了使用一种允许甜味果冻逃逸并使其他一切变得粘稠的破损语言。

反对函数式编程

[功能编程的一个]重要方面是函数不会改变它们工作的数据 [...] 面向对象的命令式语言,如 C、Java 或 Python,在运行时会改变它们的状态。

尼尔·萨维奇,(使用函数进行更简单的编程—— dl.acm.org/citation.cfm?id=3193776)

许多程序员通过他们的工具来定义自己,因此将自己定义为反对某些其他工具。如果你是.NET 程序员,那么你不会使用 Java。如果你是原生移动程序员,那么你不会使用 JavaScript。如果你是 React 程序员,那么你不会使用 Angular。与一个工具的关联自动意味着与其他工具的疏远。

这种党派性是 Sayre 定律的一个确认性例子:因为赌注很低,所以争论如此激烈。对于那些据说在理性和科学领域工作的人来说,当有人想要使用与我们选择的不同的库、语言、文本编辑器或空白符号时,我们真的很擅长变得情绪脆弱。

这种关于强烈捍卫的相似性的激烈分歧也扩展到了编程范式。如果你是一个面向对象的程序员,那么你的宿命之敌就是函数式程序员——www.sicpers.info/2015/03/inspired-by-swift/,反之亦然。

消息只是请求

别急!回想一下我在对立面中使用的对象的工作定义:对象是一个孤立的、独立的计算机程序,通过传递消息与其他程序进行通信。这并没有告诉我们如何构建那些孤立的、独立的计算机程序。特别是,没有强制在任何地方都有可变状态。以下接口作为一个时间变化的列表的消息接口工作:

public interface MutableList<T> {
  void setElements(T[] elements);
  void appendObject(T element);
  void removeObject(int index) throws OutOfBoundsException;
  void replaceObject(int index, T element) throws OutOfBoundsException;
  int count();
  T at(int index);
 };

以及这个:

public interface TemporalList<T> {
  void setInitialState(T[] elements);
  void appendObject(T element, Time when);
  void removeObject(int index, Time when) throws   InconsistentHistoryException;
  void replaceObject(int index, T element, Time when) throws   InconsistentHistoryException;
  void revertMostRecentChange(Time beforeNow);
  int count(Time when);
  T at(int index, Time when);
 };

在列表生命周期的第一个阶段,使用计算机内存的连续状态来模拟时间。在第二个阶段,列表生命周期的模拟是显式的,并且保留了列表的历史记录。另一个选项是使用不同的对象来模拟演化,将时间转化为空间:

public interface ImmutableList<T> {
  ImmutableList<T> addObject(T element);
  ImmutableList<T> removeObject(int index) throws OutOfBoundsException;
  ImmutableList<T> replaceObject(int index, T element) throws OutOfBoundsException;
  int count();
  T at(int index);
 }

现在列表看起来很像一种函数式编程列表。但它仍然是一个对象。在每种情况下,我们都定义了对象响应的消息,但记住告诉对象做什么的部分,我们没有说任何关于该对象上存在哪些方法的事情,当然也没有说它们是如何实现的。《MutableList》和《TemporalList》接口使用 Bertrand Meyer 的Datalog程序原则,或者 SQL 程序,或者存储为一系列事件链,当接收到查询消息时重新播放。

ImmutableList接口中,命令被转换操作取代,这些操作要求提供一个新列表,该列表反映了将更改应用于现有列表的结果。再次强调,没有对如何实现这些转换的限制(我可以想象通过创建一个新的列表,该列表将每个调用委托给原始列表,将count()的结果加 1,并为at(originalCount)提供自己的值来实现addObject();或者我可以简单地创建一个新的列表,包含所有现有元素和新元素),但在这个例子中,很明显每个方法都可以基于对象内容和消息参数的纯函数。

通过用 Python 语法重写接口(跳过实现),我们可以更清楚地看到“基于对象内容和消息参数的纯函数”与“纯函数”是相同的:

class ImmutableList:
  def addObject(this, element):
     pass
  def removeObject(this, index):
     pass
  def replaceObject(this, index, element):
     pass
  def count(this):
     pass
  def at(this, index):
     pass

现在更容易看出,这些方法中的每一个在其参数上都是一个纯函数,其中this/self是其他语言中自动准备好的参数(或在其他语言中自动封闭的方法环境的一部分)。

消息传递机制并没有说,“请不要使用函数式编程技术。”

一个对象的边界只是一个函数

下面的子部分深受文章《Objects as Closures: abstract semantics of object-oriented languages》的启发——dl.acm.org/citation.cfm?id=62721,该文章更严格地构建了这种对象观。

对象的接口是它响应的消息集合。在许多情况下,这背后是一个方法集合,每个方法的名字与将调用它的消息选择器相同。这不仅是最容易做到的事情,而且在许多编程语言中也是一个实现约束。前述ImmutableList的 Python 实现可以在这个表中可视化:

图片

图 3.1:实现后的 ImmutableList 的可视化

这个表可以等价地替换为一个类型为Message Selector->Method to Invoke的纯函数。该函数的平凡实现会在表的左侧列中查找其输入,并返回它在右侧列相同行中找到的值。ImmutableList的实现不需要有任何方法,而是根据消息选择器选择函数:

	class ImmutableList:
		def __init__(this, elements):
			this.elements = elements
		def __getattr__(this, name):
			if name == "count":
				return lambda: len(this.elements)
			elif name == "at":
				return lambda index: this.elements[index]
			# ...

使用这个对象的方式与使用在常规方式下定义方法的对象相同:

    >>> il = ImmutableList([1,2,3])
	>>> il.count()
	3
	>>> il.at(0)
	1
	>>>

因此,无论你如何编写对象,其方法都是可以访问(闭包)对象内部结构的函数,其消息接口是这样的一个函数,它使用消息选择器来选择要调用的方法。

释放了语言关于方法所在位置的想法的束缚,我们发现用于从选择器中查找实现的函数可以使用任何可用的信息。如果对象了解另一个对象,它可以向另一个对象发送消息,发送一个不同的方法,或者它可以编译一个新的函数并使用它。重要的思想是一个对象是查找其他函数的函数

那个类似函数的边界?实际上,是构造函数参数的闭包

我们的ImmutableList有一个名为__init__的构造方法,它使用其参数设置对象的初始状态,然后是消息查找的__getattr__函数,它选择响应发送给对象的那些消息的函数。

安排这种方式的等效方法是让构造函数返回一个消息查找函数,作为构造函数参数的闭包(并且“设置对象初始状态”中暗示的任何转换也可以通过在闭包中捕获的局部变量来安排)。因此,总的来说,一个对象是一个单一的高阶函数:一个捕获其参数并返回一个闭包的函数,该闭包接受消息并选择一个方法来执行代码:

    (constructor arguments) -> message -> (method arguments) -> method return type

坚持使用 Python,并利用这个洞察,ImmutableList可以简化为一个单一的表达式:

	def ImmutableList(elements):
		return type('ImmutableList',
					(object,),
					{'__getattr__':
					 (lambda this, name:
					 (lambda: len(elements)) if name=="count"
					 else (lambda index: elements[index]) if name=="at"
					 else False)
					})()

顺便说一句,这说明了为什么许多面向对象的语言似乎没有类型系统。如果“一切皆对象”,那么即使在最严格的类型系统中,一切也是一个message->method函数,所以一切都有相同的类型,一切都会通过类型检查。

ImmutableList的前一个定义通过以else False结束来避开“一切皆对象”的类型方案,这意味着“如果没有找到方法,返回一个不可调用的东西,这样用户就会得到一个TypeError。”一个更完整的对象系统会在这里让对象发送一个doesNotRespond消息,并且不会跳出 Python 的常规计算世界。

捕获可重用设计的元素

一种针对离职内部人员知识产权盗窃增加监控的模式

《第 18 届模式语言程序会议论文集》中的一篇文章的标题—— dl.acm.org/citation.cfm?id=2579157,PLoP'11*

Christopher Alexander 虽然在建筑领域具有开创性,但与其他建筑师相比似乎相当懒惰。为什么?因为他不是自己设计建筑或甚至城镇,而是期望将居住、工作、购物和娱乐在那里的人们为他做这件事,甚至建造其原型。

实际上,这与懒惰无关;这是因为他认为他们是做设计最好的,因为他们是最了解结构将用于何种用途以及将解决何种问题的人。他对此了解多少?不多;他所了解的是,建筑师在设计和建造城镇和建筑时遇到的问题的解决经验。

在《模式语言:城镇、建筑与建造》中,Alexander 及其合作作者和审稿人试图将那种专业知识封装在一种语法中,使用户能够通过利用专家建筑师已知有效的解决方案来解决他们自己的建筑问题。每个模式都描述了它解决的问题、解决该问题的上下文以及解决方案的优点和局限性。有些代表需要即时做出的决定——建筑中柱子的位置;而另一些则代表需要逐渐培养的经验——开设街头咖啡馆以促进人们与其环境之间的轻松互动。《模式语言》中开发的语法是累加的,因此每个模式都发展了之前引入的思想,而不依赖于之后将看到的模式,并且没有循环引用。每个模式都通过超链接(老式使用页码)与前一个构建在其上的模式相连接。

我们可以预期,在从《模式语言》中汲取灵感时,软件设计师和构建者将创建一种模式语言,允许计算机用户通过阐明用户面临的问题和表达解决这些问题的已知方法来设计和构建自己的软件。确实,当 Kent Beck 和 Ward Cunningham 发表《使用模式语言进行面向对象程序设计》—— c2.com/doc/oopsla87.html 时,就是这样发生的。报告中列出的五个 Smalltalk UI 模式就像一份人机界面指南文档的缩影,是为将使用该界面的人编写的。

然而,当我们寻找软件构建模式语言的示例时,我们大多数人会发现的是 1994 年“四人帮”的书籍《设计模式:可复用设计元素》中提到的 23 个模式,作者为 Gamma、Helm、Johnson 和 Vlissides。与亚历山大等人记录的 253 个架构设计模式相比,软件模式语言似乎显得非常瘦弱。与实际应用相比,情况看起来更糟。以下是现代开发中经常使用的三个模式:

  • 自己实现迭代器模式;这是编程语言设计者已经想出如何为你提供的,通过for (element in collection)结构。

  • 单例:你之所以构建单例,只是为了写那篇关于为什么单例是“被认为有害的”博客文章。

  • 抽象工厂:所有关于 Java 框架的笑话的靶子,这些笑话是由没有使用过 Java 框架的人说的。

问题是:四人帮的书实际上非常好,这些模式确实是可重复的模式,可以在软件设计中识别出来,并解决常见问题。但正如布赖恩·马里克在《模式失败。为什么?我们应该关心吗》一书中所争论的——www.deconstructconf.com/2017/brian-marick-patterns-failed-why-should-we-care,其中讨论的 23 个模式是实现模式,软件实现者(也就是我们)不想要可重复的模式;我们想要抽象。不要告诉我“哦,我以前见过这个,你做的是...”;告诉我“哦,我以前见过这个,这是我的npm模块。”

软件重用的最大赢家不是可以从一个程序员传递给另一个程序员的那些信息,而是可以从一个律师传递给另一个律师的信息,这允许其他信息可以从一个程序员传递给另一个程序员的程序。免费软件许可证(尤其是由于商业技术人员的保守性质,非 copyleft 的免费软件许可证,如 MIT 或 BSD)允许一些程序员将库发布到 CTAN 及其精神继承者,并允许大量其他程序员将这些库纳入他们的作品中。

在这个意义上,软件重用的最终情况与布拉德·考克斯在《面向对象编程:一种进化方法》中描述的“软件集成电路”极其相似。他提出,我们将浏览目录(即npm仓库)以寻找看起来能完成我们想要的功能的软件集成电路,比较它们的数据表(README.md或 Swagger 文档),然后选择一个并下载以集成到我们的应用程序中(npm install)。

无论如何,回到设计模式。马里克建议,我们工作的方式意味着我们不能从实现模式中受益,因为我们不依赖于实现中的重复实践。一些程序员确实参与了代码 kata——codekata.com/,这是一种在编程中培养重复实践的技术,但总的来说,我们试图要么采用现有的解决方案,要么尝试新的方法,而不是寻找现有的解决方案并以类似的方式解决问题。

事实上,我们可以通过引入策略(315)并以其术语描述所有其他问题来大大缩减《设计模式》这本书。抽象工厂?创建对象的策略(315)。工厂方法?同样。适配器?选择集成技术的策略(315)。状态?处理时间的策略(315)。但我们没有这样做,因为我们认为这些问题是不同的,所以用不同的术语描述它们,并寻找不同的解决方案。

因此,抽象必须在某个地方停止。特别是,当我们与产品所有者或赞助者交谈时,抽象必须停止,因为我们通常在构建特定的软件工具来支持特定的任务。建筑架构有设计住宅、办公室、商店和酒店的技术,而不是“建筑”。一个年轻单身工人的房子与一个退休寡妇的房子不同,尽管两者都是只有一个居住者的住宅。所以,正如布赖恩·马里克总结的那样,这表明我们的软件问题域中需要有设计模式,告诉我们领域专家如何解决他们遇到的问题。我们可能对有状态的软件、桌面应用程序小部件或基于微服务的服务架构有很好的抽象,但我们必须将它们用于特定的目的,而了解该领域的人知道他们试图解决的问题。

事实上,这正是编程模式语言系列会议和软件模式社区的现代目标之一。我原本以为,在第一次阅读时,本节所选的引用(“一种用于增加离职内部人员知识产权盗窃监控的模式”)会引发一些讽刺的笑声:“哇,模式专家们已经深入兔子洞,他们甚至为编写模式?”是的,他们确实如此,因为这是一个多次被多人遇到的问题,而且对解决方案的共同方面的了解可以帮助设计师。任何企业 IT 架构师、CISO 或小型公司的人力资源人员都知道,离职者,尤其是那些因与管理层意见不合或被竞争对手挖角而离职的人,代表了知识产权盗窃风险的增加,并希望有一种方法来解决这个问题。在这里,模式语言显示了问题的关键维度、解决方案的方面以及解决方案的利弊。

模式描述中的一句话揭示了真相:

作者们不知道有任何在生产环境中实现该模式的情况。

这意味着,虽然这个解决方案(可能和希望)捕捉到了关于问题及其解决方法的专业知识,但它尚未经过测试。Beck 和 Cunningham 论文中的设计模式(以及 Beck 后来的Smalltalk 最佳实践模式),以及《四人帮》的书,都是基于观察问题通常是如何解决的。并不是有很多 C++或 Smalltalk 程序都有名为AbstractFactory的类,但确实有很多 C++或 Smalltalk 程序解决了“我们需要创建一系列相关或依赖的对象,而不指定它们的具体类”的问题。

另一方面,除了 SEI 实验室之外,没有人使用“通过离职内部人员增加知识产权盗窃监控”作为解决那个问题的方案。所以,也许模式已经失控了。

找到运行方法

不要为了证明明显很酷的东西而费尽心机。不要仅仅因为它们不是最新和最棒的而嘲笑想法。选择你自己的时尚。不要让别人告诉你你应该喜欢什么。

拉里·沃恩,(Perl,第一种后现代计算机语言——www.perl.com/pub/1999/03/pm.html/)

Perl 社区有一个格言:“TIMTOWTDI”(发音为“Tim Toady”)。它代表“有不止一种方法可以做到”(There Is More Than One Way to Do It),反映了设计原则,即语言应该允许用户以他们思考的方式编写程序,而不是语言设计者思考的方式。当然,TIMTOWTDI 不是唯一的方法,Python 的禅宗——wiki.c2.com/?PythonPhilosophy采取了不同的(尽管不是不兼容的)方法:

应该只有一个——最好是只有一个——明显的方法来做这件事。

那么,如何找到方法呢?有多种方法可以做到。第一种,也是最容易理解的,是一个对象有一个与消息选择器同名的方法,语言假设当你发送那个消息时,是因为你想调用那个方法。这就是 JavaScript 中的样子:

    const foo = {
	  doAThing: () => { console.log("I'm doing a thing!"); }
	}

	foo.doAThing();

下一种方法是最通用的,并不是所有语言都有,而且在某些语言中很难使用。想法是让对象本身决定对消息做出什么反应。在 JavaScript 中,它看起来像这样:

    const foo = new Proxy({}, {
	  get: (target, prop, receiver) => (() => {
	    console.log("I'm doing my own thing!");
      }),
	});

	foo.doAThing();

虽然有许多语言没有以这种方式查找方法的语法,但实际上自己编写它非常简单。我们在功能编程的部分中看到,对象只是一个将消息转换为方法的函数,因此任何允许你编写返回函数的函数的语言都将允许你编写按你想要的方式工作的对象。这个论点在 Swift 中的面向对象编程谈话中也得到了追求——www.dotconferences.com/2018/01/graham-lee-object-oriented-programming-in-functional-programming-in-swift

几乎所有具有对象的编程语言都有一个回退机制,即如果一个对象没有与消息选择器匹配的方法,它将默认查看另一个对象以找到方法。在 JavaScript 中,完全接受了 Tim Toady 的世界观,有两种方式来做这件事(记住,这已经是 JavaScript 中找到方法的第三种方式了)。第一种,经典、原始的 JavaScript 方法,是查看对象的原型:

    function Foo() {};
	Foo.prototype.doAThing = () => { console.log("Doing my prototype's thing!"); };

	new Foo().doAThing();

第二种方式,在有些其他语言中是定义方法的唯一方式,是让对象查看其类:

    class Foo {
	  doAThing() { console.log("Doing my class's thing!"); }
	}

	new Foo().doAThing();

在这里,我们牺牲了一些清晰度以换取一点诚实:这最后两点实际上只是同一件事的不同语法;方法最终是在对象的原型上定义的,并在那里找到。心智模型是不同的,这才是重要的。

但我们不能止步于此。如果那个对象找不到方法怎么办?在原型案例中,答案很明确:它可以查看其原型,以此类推,直到找到方法,或者原型用尽。对于一个对象的外部用户来说,看起来这个对象拥有其原型及其定义的所有行为(这可能是一些其他、不同的特征,或者它们可能是原型的替代品)。我们可以说,对象继承了其原型的行为。

当涉及到类时,继承的情况更为复杂。如果我的对象类没有实现响应消息的方法,我们接下来该看哪里?一个常见的方法,在早期的对象环境(如 Simula 和 Smalltalk)以及 Objective-C、Java、C# 等语言中使用,是说一个类是对单个其他类的细化,通常称为超类,并且类实例将继承为超类实例及其超类定义的行为,直到我们用完superclasses

但这相当有限。如果有一个对象可以被视为两种不同类别的对象的一种改进,或者有两个不同的类描述了它应该继承的不同行为,那会怎样呢?Python、C++和其他语言允许一个类从多个其他类中继承。当一个消息发送给一个对象时,它将在其类中寻找方法实现,然后在其...

...现在我们感到困惑。它可能从树的广度优先搜索开始,考虑每个父类,然后是每个父类的父类,依此类推。或者它可能从深度优先搜索开始,考虑其第一个超类,然后是其第一个超类的超类,依此类推。如果有多个方法与单个选择器匹配,那么找到的是哪一个将取决于搜索策略。当然,如果有两个匹配的方法但具有不同的行为,那么一个方法的存在可能会破坏依赖于另一个方法行为的特性。

人们已经尝试在不引起混淆的情况下获得多重继承的好处。混入(Mixins)——dl.acm.org/citation.cfm?id=97982代表了“抽象子类”,可以被附加到任何超类上。这把单超类继承系统转变为能够支持有限形式的多重继承的系统,通过将消息委派给超类和任何混入。

然而,这并没有解决如果多个混入(mixins)或一个超类和一个混入提供了相同的方法时,将会出现的冲突问题。对混入(mixins)这一想法的改进,称为特质(traits),引入了额外的规则来避免冲突。每个特质都会在它被混入的类中暴露其提供的功能和所需的功能。如果两个特质提供了相同的功能,那么必须要么在其中一个中重命名该功能,要么从两个特质中移除并将其转换为需求。换句话说,程序员可以选择通过构建一个方法来解决这个问题,这个方法能够完成两个特质都需要做的事情。

因此,继承是代码重用的一个伟大工具,允许一个对象从另一个对象那里借用特性来完成其任务。在"Smalltalk-80: The Language and its Implementation"中,这是对继承的辩护:

在面向对象系统中,类成员资格的缺乏是对设计的限制,因为它不允许类描述之间的任何共享。我们可能希望两个对象在实质上相似,但在某些特定方面有所不同。

随着时间的推移,继承对设计者的意图产生了更强的暗示。虽然实例与其类之间始终存在“是”的关系(例如,OrderedCollection 类的实例 一个 OrderedCollection),但类与其子类之间出现了子集关系(例如,SmallIntegerNumber 的子类,因此 SmallInteger 的任何实例也是 Number 的实例)。这进而演变为子类型关系(例如,只有当任何期望类实例的程序在给定该类任何子类的实例时也能正确运行时,你才正确地使用了继承),这导致了将面向对象开发者搞得团团转的限制,并导致了“优先使用组合而非继承”:只有当你也符合这些其他无关的要求时,你才能通过继承获得复用。关于子类型的规则非常明确,并且在数学上是合理的,但一个子类必须是子类型的假设并不需要得到坚持。

事实上,还有一个常见的假设,它隐含了大量的设计意图:类的存在。我们已经看到,JavaScript 没有类也能正常运行,而当类被添加到语言中时,它们是以一种方式实现的,以至于实际上根本不存在“类性”,类在幕后被转换成了原型。但是,在系统的设计中存在类,意味着,类的存在:存在一些具有共同特征的对象,并且以特定方式定义。

但是,如果你的对象确实是一个手工制作的、独特的单例呢?嗯,类设计社区为此有一个解决方案:单例——这个设计模式说,“一个类的存在。”但是为什么要有一个呢?在这个时候,这只是额外的劳动,当你只想有一个对象时。你的类现在负责系统的三个方面的行为:对象的工作、制作对象的工作,以及确保只有一个这样的对象的工作。这比仅仅制作一个执行工作的对象的设计要松散。

如果可能的话(就像在 JavaScript 中那样),首先创建一个对象,然后创建另一个类似的对象,然后更多,然后注意到相似之处和不同之处,并在包含所有这些对象的类的设计中封装这种知识,那么这个一次性对象就不需要是任何比一次设计并多次使用更多的东西。就没有必要创建一个包含所有与该对象相似的对象的类,只是为了再次约束类成员,以确保单例实例不能被任何同伴加入。

但正如你可能已经体验到的,大多数编程语言只提供一种继承方式,而且通常是“单一继承,我们假设它也意味着子类型”。很容易构造出多继承有意义的场景(一本书既是可以被编目和上架的出版物,也是可以被定价和销售的产品);单一继承有意义的场景(一个具有集合的所有操作,但添加同一个对象两次意味着它在中出现了两次);以及定制原型有意义的场景(我们的假设是,通过应用固定的运费而不是让客户从一系列选项中选择,简化结账交互将增加尝试结账的客户完成率)。很容易考虑所有这三种情况同时适用的情况(一个在线书店可以轻松地在单一系统中表示书籍、包和结账);那么为什么在同一个对象系统中建模所有这些情况这么困难呢?

到头来,继承只是引入代理的一种特定方式——一个对象找到另一个对象来转发消息。继承被限制在特定形式的事实并不阻止我们将消息委托给任何我们喜欢的对象,但它确实阻止我们在设计中使这样做的原因明显。

构建对象

那么什么是个人电脑呢?我们希望它能同时作为包含和表达任意符号概念的中介,以及一组用于操作这些结构的实用工具,并且有方法向工具库中添加新工具。

艾伦·C·凯,《面向所有年龄段儿童的电脑》

Smalltalk 既是一个非常个性化的系统,也是一个非常活跃的系统。这影响了使用、构建和共享系统中构建的对象的体验,这些都是在与 COBOL 和后来的语言相关的编辑-编译-汇编-链接-运行工作流程非常不同的方式下完成的。

顺便说一下,我这里主要用“Smalltalk”来指代“Smalltalk-80 及其后来没有改变体验太多的衍生版本”。任何看起来和感觉“非常像”Smalltalk 环境的,如 Pharo 或 Squeak,都被包括在内。任何涉及明显更传统工作流程的,如 Java 或 Objective-C,都被排除在外。如何划线是故意模糊的:尝试使用GNU Smalltalksmalltalk.gnu.org/)并决定你是否认为它“是 Smalltalk”

Smalltalk 环境由两部分组成:虚拟机可以执行 Smalltalk 字节码,而映像包含 Smalltalk 源代码、字节码以及类和对象的定义。

因此,这个图像既是个人化的,也是普遍性的。个人化在于它对我而言是独一无二的,包含了我自己创造的或从他人那里获得的物体;普遍性在于它包含了整个系统:没有私人框架,没有只包含目录服务对象而不包含 GUI 对象的可执行文件,也没有在使用网络之前需要链接的库。

这使得构建事物变得非常容易:我创建我需要的对象,我找到并使用我可以利用的对象。另一方面,它使得共享变得相当复杂:如果出于某种原因我需要更改系统对象,你不能接受我的更改而不考虑这个更改将对你的图像中的其他一切产生的影响。如果你想将我的类添加到你的图像中,你必须确保你还没有一个具有该名称的类。我们不能在Smalltalk字典中为不同的目的使用相同的键。

它也是动态的,因为你修改图像的方式是通过与之交互。方法通过将方法写入文本字段并发送消息给编译器对象,请求它编译该方法,以 Smalltalk 字节码的形式实现(尽管这种字节码可能只是一个请求在虚拟机上执行存储的“原始方法”)。类是通过向现有类发送消息,请求它创建一个子类来添加的。对象是通过向类发送new消息来创建的。

虽然有编辑、编译和调试,但这些都是在图像内进行的。这使得原型和反馈体验非常快速(毫不奇怪,Smalltalk 背后的一个愿景是让孩子们同时探索世界和计算机——mprove.de/diplom/gui/kay72.html。你做的任何更改都会影响你正在使用的系统,并且其影响可以在不重新构建或退出应用程序重新启动的情况下看到。同样,你正在使用的系统也会影响你正在做的更改:如果一个对象遇到它没有响应的消息或者断言没有得到满足,那么调试器就会被调出,这样你就可以纠正你的代码并继续。

使用代表 UI 小部件的对象构建 UI 所提供的快速反馈被许多快速应用开发工具所使用,例如 NeXT 的 Interface Builder、Borland 的 Delphi 和 Microsoft 的 Visual Basic。这些工具在之前描述的权衡方面采取了非常不同的立场。

虽然像 Eclipse 这样的 IDE 可能是由 Java 编写的,但使用 Eclipse 的 Java 开发者并不是在编写修改 Eclipse 环境的 Java 代码,即使他们正在编写的 Java 包是 Eclipse 插件。相反,他们使用 IDE 来托管生成另一个程序的工具,该程序包含他们的代码,以及代码运行所需的其他包和库的引用。

这种方法更偏向于通用而非个人化(任何拥有相同集合的包和库的人都可以使独立代码工作,而无需任何步骤将它们集成到他们的镜像中)并且更具体而非通用(结果程序——不考虑错误——只包含该程序所需的所有内容)。

这一个关键的区别——有一个“构建阶段”将你正在制作的东西与你制作它的东西分开——是两种构建对象方式之间的主要区别,也是两种方式中思想传递不完美的一种方式。

这些带有 GUI 构建器的快速应用程序开发工具让你可以从供应商框架中设置 UI 小部件并配置它们的属性,通过操作实时对象而不是编写静态代码来构建 UI。在实践中,能够这样做的能力受到以下限制:

  • 要了解 UI 的质量,你需要与界面暴露的真实信息和工作流程一起工作,所有这些都在编辑器窗格和代码浏览器中等待编译和与 UI 布局集成到(目前处于休眠状态的)应用程序中。

  • UI 编辑器工具之外的变化无法反映在其中。更改标签上的字体很容易测试;编写应用于标签内容的新的文本转换则不然。

  • 你可以在 UI 构建器中测试的 UI 部分通常由平台的接口指南明确定义,所以你永远不会想要更改标签上的字体。

在实践中,即使有 UI 构建器,你仍然有一个编辑-构建-调试的工作流程。

类似的思想部分传递也可以在测试驱动开发中看到。简要总结(显然,如果你想看详细版本,你总是可以购买我的书——qualitycoding.org/test-driven-ios-development-book/)是,你通过考虑你想发送给对象的哪些消息,然后它应该如何响应,然后发送这些消息并记录是否得到预期的响应来增量地创建一个对象。你可能不会得到预期的响应,因为你还没有告诉对象如何行为,所以你添加一段产生正确响应的行为,然后继续到下一个消息,之后进行一些整理。

在 Smalltalk 的世界里,我们已经看到,一些意外事件会让你陷入调试器中,在那里你可以修复出错的组件。因此,前面的整个过程可以总结为“想一个消息,输入它,点击执行,编辑源代码直到调试器停止显示”,现在你已经在你的镜像中增加了一个工作软件的增量。

在 Java 的世界里,即使同一个人编写了 SUnit 和 JUnit 测试工具,过程也是(假设你已经有一个包含相关工件的项目):

  1. 编写发送消息的代码

  2. 满足编译器

  3. 构建并运行测试目标

  4. 使用输出结果来指导编辑器中的更改

  5. 重复 3 和 4,直到测试通过

因此,存在一个更长的反馈循环。这适用于任何类型的反馈,从验收测试到正确性测试。你不能从你正在构建的东西内部构建它,所以当你和你的电脑切换上下文时,总是会有一个暂停。

这种上下文切换的原因只有部分是由于技术:在 2003 年,当苹果公司推出 Xcode 时,他们大肆宣传“修复并继续”,这个功能在 Java 环境等其他环境中也可用:当源代码更改时,在一定的范围内,相关的目标文件可以被重新构建并注入到正在运行的应用程序中,而无需终止和重新链接它。然而,这通常不是程序员思考他们活动的方式。赋予我们“工具链”和“流水线”等词汇的世界观是一种顺序活动,其中程序可能最终“在生产中”,但肯定不是开始在那里。使用程序的人发生在乐趣结束之后。

第一部分结论

我们已经看到,面向对象编程确实,正如许多批评者所建议的,是一个具有许多移动部件的复杂范例。我们还看到,这种复杂性不是必要的:其核心是一个单一的想法,即问题可以被建模为许多不同的、相互作用的代理,并且每个代理都可以被建模为一个小的、孤立的计算机程序。原始问题的解决方案在于这些代理之间的交互,这种交互是通过消息传递来介导的。

部分偶然的复杂性似乎是由于人们想要留下自己的印记而添加的:设计模式的激增似乎是因为添加新模式总是比巩固现有模式更容易;尽管有些人可能喜欢从历史上抹去 Singleton。对象不仅仅是分解和消息传递,它们是并且提供对程序数据的访问,或者并且一个类层次的体系结构。

与对象相关的大部分复杂性都源于另一个原因:试图将面向对象编程与之前出现的结构化、过程式、命令式流程相类似,并将它的术语映射到既定软件编写方式的思维结构和工作流程中。这就是本节引言中提到的“结构化入门”,其中面向对象编程被视为现有思想的扩展,通过添加对象使程序“更好”,就像在食物上撒上辣椒粉使其“更好”一样。因此,Ann Weintz 在《编写 NeXT 应用程序》一书中可以说:“NeXT 对象仅仅是 C 代码的一部分”。因此,面向对象软件工程是通过仔细的、自上而下的分析程序(或自下而上的分析数据和其操作)来构建复杂的软件系统,同时作为一个附带活动创建具有特定关系的类层次结构。

如果将对象视为与编写软件一样需要去做的事情,那么难怪这比不使用对象要难!面向对象编程似乎失败了,但也许它甚至都没有真正尝试过。

第三章:第二章

论文

对象是独立的程序

许多不同演示中贯穿的线索是对象是相互独立的计算机程序,它们通过发送和接收消息进行通信。通常,有一个“和”,但第二个子句差异很大。让我们忽略它,专注于第一个子句。

例如,在 Smalltalk-80 及其大多数后继者中,对象可以被描述为相互独立的计算机程序,它们通过发送和接收消息进行通信,并且是组织在树结构中的类的实例。这里第二部分,关于类的内容,通过减少隔离的范围削弱了第一部分。为什么要求消息的发送者和接收者都是类的实例,并且这两个类都是同一树结构的成员?这并不是必须的,所以让我们通过移除继承的约束来加强独立程序的概念。

一个具有这种隔离形式的面向对象编程环境的现有例子是 COM(是的,就是微软的组件对象模型,也就是COM)。当你收到一个对象时,你对其一无所知,只知道它响应在 IUnknowndocs.microsoft.com/en-us/windows/desktop/api/unknwn/nn-unknwn-iunknown 接口中定义的消息,该接口允许你保持对对象的引用,放弃该引用,或找出它支持的其他接口。它没有告诉你该对象来自何处,是否从另一个对象继承,或者它是否对其每个方法都有新鲜、手工制作的、工匠般的实现。

你可以对 COM 对象和 Smalltalk 对象做出的一个推论是,它们存在于同一个进程中,也就是说,与发送消息的同一块内存和执行上下文。也许它们在内部通过某些 Smalltalk 对象将消息转发,其他对象则无法运行。

因此,虽然 Smalltalk 对象近似了独立计算机程序的概念,但这种近似并不精确。同时,在 Mach 上,发送者对对象唯一知道的是“端口”,这是一个内核可以使用它来确定正在被消息的对象的数字。对象可能位于不同的线程上,位于同一线程上,位于不同的进程中,或者(至少在理论上)位于不同的计算机上,向其发送消息的方式是相同的。接收者和发送者可能共享他们所有的代码,从共同的祖先继承,或者用不同的编程语言编写,在以不同方式存储数字的 CPU 上运行,但他们仍然可以向对方发送消息。

Smalltalk(所有对象都是同一种对象,并且相互关联)和 Mach 之间,存在MetaObject的概念——wiki.c2.com/?MetaObjectProtocol。由于软件系统中的对象定义了系统如何建模某些问题,元对象定义了软件系统如何表达其对象的行为。MetaObject协议暴露了改变系统内部对象模型含义的消息。

换句话说,一个MetaObject协议让程序员能够为程序的不同部分选择不同的编程环境规则。以方法查找为例:在第一部分,我们看到了原型继承、单继承和多继承各自的优点和缺点,以及它们对对象系统设计的不同约束。为什么不同时拥有所有这些继承工具——以及任何其他工具,以及其他形式的委托——呢?有了MetaObject协议,这是可能的。

独立对象的开放封闭特性

在他的书籍《面向对象软件构造》中,伯特兰·迈耶介绍了开放封闭原则。这个原则可能是所有计算中最容易混淆的观点之一,并导致了一个完整的子行业,包括文章和播客解释如何一个ShapeRenderer可以绘制正方形圆形(当然,我也有参与其中,并将继续在这里讨论)。

开放封闭原则指出,一个模块(在我们的情况下是一个对象)应该对扩展开放——应该能够扩展其行为以适应新的目的——同时对其修改封闭——你不应该需要改变它。这个设计原则是有代价的,因为你需要设计你的对象以支持沿着尚未知的线路进行扩展(或者至少,要清楚地说明哪些线路是有益的,哪些不是),以换取维护者和对象使用者知道它们将是稳定的,并且不会通过意外的变化将破坏引入系统的好处。

上文探讨的对象特性,将它们视为完全独立的程序,通过保持每个对象与其他对象保持距离来支持开放封闭原则。它们唯一的接触点是它们的消息接口,甚至是对它们的父类(当然,记住它们可能没有任何父类)。

因此,为了保持开放和封闭,一个对象也需要无知:它应该尽可能少地了解其上下文。它知道在接收到消息时应该做什么,也知道何时发送消息,但应该在其他方面对周围发生的事情保持不知情。由于无法区分这些上下文,无知对象可以在多个上下文中使用——对其使用的扩展是开放的——这是因为它无法区分这些上下文。它不需要进行上下文更改,因此对修改是封闭的。

独立对象的正确性

当每个对象都是其自己的独立程序时,我们将“这个大系统是否工作”的问题转化为两个独立的问题:

  • 这些独立的对象是否工作?

  • 这些独立的对象是否正确地进行了通信?

这些问题在软件工程中,尤其是在面向对象编程(OOP)中,已经被反复解决。一个对象的消息接口在定义单元测试时,自然地构成了“这个单元”和“其他一切”之间的边界。Kent Beck 的测试驱动开发(TDD)方法通过询问自己希望向对象发送什么消息以及期望什么结果,从消息边界向内设计对象。这种方法通过将每个对象视为一个单独的系统来测试,回答了“这些独立的对象是否工作?”的问题。

以《通过测试引导的面向对象软件增长》一书为例的伦敦 TDD 学校,对消息边界作为系统边界的规则进行了极端解释,通过使用模拟对象——xunitpatterns.com/Mock%20Object.html作为被测试对象的全部协作者的替代品。这个对象(被测试的对象)需要向那个对象(某个协作者)发送消息,但没有理由了解关于那个对象的其他信息,除了它将响应这个消息。通过这种方式,伦敦学校推崇的上述无知被用来支持开放-封闭原则

在 Eiffel 编程语言中,Bertrand Meyer 通过允许开发者将一个契约与每个类关联起来,也解决了每个对象是否工作的问题。这个契约基于 Edsger Dijkstra 和其他人使用数学归纳法来证明关于程序陈述的工作,使用对象的消息接口作为程序的自然外部边界。契约解释了在处理给定消息之前对象需要什么条件为真(前置条件),对象在其方法执行后将安排什么条件为真(后置条件),以及当对象不执行方法时始终为真的条件(不变性)。然后,当对象被使用时,这些契约作为检查运行,与仅使用测试作者最初考虑的输入和输出的单元测试不同。

契约在传统的软件开发方法中以有限的方式出现,形式为基于属性的测试——blog.jessitron.com/2013/04/property-based-testing-what-is-it.html,体现在 Haskell 的 QuickCheck、Scala 的 ScalaCheck 和其他工具中。在 Eiffel 中,契约是正在构建的系统的一部分,并描述了对象与其他对象结合时应该如何使用。基于属性的测试通过将契约作为测试占卜师,从而作为测试对象的外部验证器,使用契约来构建任何数量的自动化测试。一个契约可能说“如果你提供一个包含具有唯一标识符的电子邮件消息的列表,这个方法将返回一个包含相同消息的列表,按发送日期排序,如果日期相同,则按标识符排序”。一个基于属性的测试可能说“对于所有具有唯一标识符的电子邮件消息列表,调用此方法的输出结果...”。开发者可能生成一百或一千个这样的测试,作为他们发布流程的一部分,检查是否存在反例。

问题的第二部分——这些独立对象是否正确地进行了通信?——也可以用多种方式来处理。在像 Eiffel 这样的契约世界中,这是通过确保在每个对象向协作者发送消息的点,满足该协作者的先决条件来解决的。对于其他人,则有集成测试。

如果单元测试报告了一个单一对象的行为,那么集成测试就是任何包含多个对象的组件的测试。借用布拉德·科克斯的软件集成电路隐喻,单元测试告诉你芯片是否工作,集成测试告诉你电路是否工作。集成测试的一个特殊情况是系统测试,它集成了解决某些特定问题所需的所有对象:它告诉你整个板是否按预期工作。

独立对象的 设计

在这里讨论设计是合适的,因为测试和设计活动是紧密相关的。Eric Evans 的书籍领域驱动设计讨论了一种之前被称为面向对象分析的形式:通过解释问题的描述来找到解决问题所需的对象。这个过程很简单。取一个问题的描述,执行动作的“事物”是对象,它们执行的“动作”是方法,它们告诉或询问其他事物的“事物”是消息。Evans 建议在整个开发团队中有一个单一的“普遍”语言,这样拥有问题的目标提供者wiki.c2.com/?GoalDonor所使用的词汇与构建解决方案的人所使用的词汇相同。借鉴 Christopher Alexander 的想法,这是问题域和解决方案域的普遍语言,在其中可以期望找到一种模式语言,因为问题的常见方面以类似的方式得到解决。

行为驱动开发测试驱动开发的技术流程与普遍语言的构思设计相结合,通过鼓励开发者与其他团队成员协作,用普遍语言定义期望的行为陈述,并使用这些陈述来驱动解决方案领域中对象的开发和实现。这样,目标提供者所需的目标陈述也是充分性和正确性的陈述——也就是说,需要解决的问题的描述也是有效解决方案的描述。这种方式最终看起来足够自洽,以至于不会令人感到惊讶。

构建独立对象

这篇文章贯穿的主题是充分性足够。当一个对象被识别为解决问题的一部分,并且对解决方案的贡献达到所需程度(即使现在这个程度是“证明解决方案是可行的”),那么它就准备好使用了。没有必要将对象定位在继承类别的分类中,但如果这样做有助于解决问题,那么就尽一切可能去做。没有必要证明各种对象表现出严格的子类型关系并且可以互换使用,除非解决问题需要它们可以互换使用。没有必要让对象将其数据提供给程序的其他部分,除非这样做可以更好地解决问题(或者更便宜地解决问题,或者其他可取的性质)。

我之前对开放封闭原则做了很多讨论,它建议我们构建的对象应该是“开放于修改”。这难道不是意味着预测系统将如何变化,并使对象能够以这种方式灵活变化吗?

在某种程度上,是的,而且确实这种考虑是有价值的。如果你的问题是确定在你的当地台球馆中台球手在台上的时间应该如何收费,那么你的解决方案确实可能在同一个馆的台球桌上或在不同的台球馆中使用。但哪个会先发生?会很快发生吗?这些问题需要与目标捐赠者和黄金所有者wiki.c2.com/?GoldOwner,为解决方案付费的人)一起回答。现在解决这个相关问题值得付费吗?

无论答案如何,事实是,一旦它们解决了“你现在遇到的问题”,这些对象就可以随时开始工作。而且,无论如何,还有其他方法来解决相关的问题,这些方法不需要“未来证明”对象设计以预测它们可能被用于哪些用途。也许你的SnookerTable并不适用于扩展表示台球桌,但你的解决方案中的其他对象可以代替向PoolPlayer发送消息。正如开放-封闭原则的变体所显示的,这些其他对象可能对在桌面上进行的游戏一无所知。

无论计划是否最终实现,一定的规划总是有帮助的。在每个转折点,我们的目标应该是理解我们如何从“我们现在拥有的”达到“我们现在想要的”,而不是“已经拥有”我们可能将来想要的东西。也许最容易的事情就是从头开始:所以,就这样做。

与独立对象协同工作

传统的编写和更改软件的方式导致了持续部署,这是一种自动化从编写源代码到在实时环境中部署生产实体的管道的原则,目标是减少更改通过管道所需的时间,同时保持高质量。

Pharopharo.org/)、SqueakJSsqueak.js.org/run/#url=https://freudenbergs.de/bert/squeakjs&zip=[Squeak5.0-15113.zip,SqueakV50.sources.zip)或甚至以它们有限的方式Swift Playgroundswww.apple.com/swift/playgrounds/)和Project Jupyterjupyter.org/)都表明,这个管道可以是零长度的,并且软件可以直接在预期的环境中编写。测试失败的结果不需要是 Jenkins 提供的日志文件,必须仔细阅读以在“本地开发”中假设一个修复方案,它可以是纠正运行在实时环境中的程序的机会,并继续(或,在最坏的情况下,重新启动)失败的操作。

这种活跃性属性不仅限于类似 Smalltalk 的环境或 REPLs。考虑 Mach 微内核操作系统;任何注册到名称服务器(或者在 HURD 的情况下,作为文件系统上的翻译器)的服务器都是一个“活对象”,它可以接收来自系统其余部分的消息并参与其行为。它们也是可以检查、调试、更改、重新启动或替换的任务。

由微服务组成的服务器应用程序具有类似属性。“对象”(服务的运行实例)通过 URL 找到彼此:任何配置为在给定路由接收 HTTP 请求的服务“响应”消息。这些服务中的每一个都可以独立检查、调试、编辑或替换。

第二部分结论

当移除了额外的复杂性和试图采用传统的软件开发技术时,面向对象编程(Object-Oriented Programming)试图通过一组小型、独立的程序来表示复杂问题,每个程序都模拟问题的一个(更简单)方面。这些程序可以独立编写、验证、部署、更改和使用。理想情况下,它们应尽可能地对彼此一无所知,仅依赖于它们应该响应某些消息并且可以向其他对象发送其他消息的知识。

第四章:第三章

综合考虑

第二章,论点中,我们看到了使用面向对象编程(OOP)的核心好处可以通过少数几个考虑来实现:

  • 对象是独立的程序,尽可能无知于上下文

  • 对象通过发送消息进行通信

  • 对象的行为由合约描述,这些合约表达了它们对消息的响应

  • 对象可以在上下文中编写、更改、检查和适应

目前还没有一个系统能够同时支持所有这些要求。讽刺的是,尽管面向对象编程(OOP)已经变得过于复杂,正如第一章“对立”中所示,它也仍然不完整。在这本书的最后部分,让我们考虑一下这样一个系统会是什么样子。

对象是独立的程序

最容易解决的问题是可以让开发者独立设计对象,而不必表达限制开发者设计自由的约束。一种方法是为开发者提供一个MetaObject协议,允许他们根据特定上下文调整语言的规则。一个更简单的方法(无论是创建还是使用)是将消息系统的原始部分提供给开发者,按需组合以满足他们的设计目标。

这更容易创建,因为任何更复杂的系统都需要这些原始操作。使用起来也更简单,因为它允许开发者根据遇到的问题构建解决方案,而不是试图将现有规则适应他们为解决方案构建的模型。这种适应是我们第一章“对立”中探讨使用面向对象编程(OOP)的困难之一:如果你有 Java 继承,你需要使用 Java 继承来解决你的问题,即使你的问题看起来并不适合 Java 继承。

需要的原始操作数量很少。以下是一个基于第一章“对立”中探讨的对象函数式编程视图的 Python 示例。

选择器类型。这是一种可以用来命名消息的类型,因此它必须是可比较的:接收者需要知道消息中命名了哪个选择器,以便它可以决定要做什么。Python 的字符串类型足以作为选择器类型,尽管许多面向对象的语言使用内部字符串类型(例如 Ruby 的符号)来降低比较的成本。

使用__getattr__()函数来完成这项工作,无论是为了实现object.attribute语法还是为了实现getattr(object, attribute)函数,而且方便的是,它期望属性名是一个字符串,因此这与消息选择器兼容。

发送消息的方式。这将允许对象使用其自己的查找函数找到适当的方法实现,然后使用消息中提供的参数执行该方法。它看起来像这样:

	def msg_send(obj, name, *args):
		message_arguments = [obj]
		message_arguments.extend(args)
		return getattr(obj,name)(*message_arguments)

注意到任何消息的第一个参数都是接收对象。这允许对象递归地给自己发送消息,即使被调用的方法不是在接收器上找到的,而是在一个被委托的对象上,否则这个对象可能对接收器一无所知。

消息查找的递归情况。如果一个对象不知道如何实现给定的消息,它可以请求另一个对象。这是 委托。它看起来是这样的:

    def delegate(other, name):
        return getattr(other, name)

doesNotUnderstand 函数提供了这种行为(在我们的情况下,引发错误),我们还将提供一个使用 doesNotUnderstand 并可以终止任何委托链的 Base 类型:

    def doesNotUnderstand(obj, name):
        raise ValueError("object {} does not respond to selector {}".format(obj, name))
	Base = type('Base', (), {
        '__getattr__': (lambda this, name:
            (lambda myself: myself) if name=="this"
            else (lambda myself: doesNotUnderstand(myself, name)))
        })

由于消息发送约定,myself 是接收消息的对象,而 this 是代表其处理消息的对象:这些可以是,但不一定必须是同一个对象。

现在这 13 行 Python 代码(在 objective-pygitlab.labrary.online/leeg/objective-py 找到)足以构建任何形式的面向对象委托,包括常见的继承形式。

一个 对象 可以通过将所有未知消息委托给原型来继承原型。

是一个代表其实例实现方法的对象。一个类的创建实例包含它自己的所有数据,但将所有消息委托给类对象。

这个类可以没有父类(它不委托未知消息),有一个父类(它将所有未知消息委托给单个父类对象)或多个父类(它将未知消息委托给父类对象列表中的任何一个)。它还可以支持特性或混入,通过将它们添加到搜索方法实现的对象列表中来实现。

一个类甚至可以有一个 metaclass:一个类对象,它将接收到的消息委托给它。如果需要,metaclass 还可以有一个 metametaclass

这些方案中的任何一个或多个都可以在同一个系统中使用,因为对象之间互不知晓,也不知道它们的构造方式。它们只知道它们可以使用 msg_send() 发送彼此的消息,并且可以使用 delegate 让另一个对象代表它们响应消息。

但是,Python 作为 Python,这些对象都在同一个线程、同一个进程中同步运行。它们还不是真正独立的程序。

继续使用 Python,通过为每个对象使用不同的 Python 解释器,我们可以很容易地将我们的对象分离到不同的进程中,通过使用 execnet 模块——codespeak.net/execnet/index.html

一个简短但重要的补充

此处的示例(以及可在 gitlab.labrary.online/leeg/objactive-py 获取)专注于展示运行隔离对象的可能性,实际上并不适合用于实际的应用或系统。本书中描述的简单面向对象原则缺乏生产系统,正是最初写这本书的动机!

每个对象可以存在于自己的模块中。创建对象涉及创建一个新的 Python 解释器并告诉它运行此模块:

    def create_object():
        my_module = inspect.getmodule(create_object)
        gw = execnet.makegateway()
        channel = gw.remote_exec(my_module)
        return channel

execnet 运行一个模块时,它有一个特殊名称,我们可以用它来存储接收通道并安装消息处理程序。在此代码中,接收者存储在一个全局变量中;由于它在自己的 Python 解释器中运行,与我们的系统其他部分是分开的进程,因此这个 全局 实际上是接收对象的唯一标识:

    if __name__ == '__channelexec__':
        global receiver
        receiver = channel
        channel.setcallback(handler)
        channel.waitclose()

handler 函数是我们对象的信使分发函数:它检查信使选择器并决定运行什么代码。这可以与之前的示例完全相同地工作——换句话说,它可以按照我们的意愿工作。一旦对象收到一个消息,它应该由 该对象 决定如何处理它,以及如何响应。

对象的行为可以用契约来描述

虽然任何对象都有权决定如何响应消息,但我们需要知道该对象是否代表对我们系统的有用补充。换句话说,我们想知道 对象 将如何响应 什么 消息。

第二章,论文 所见,Eiffel 语言将关于对象的知识封装成 契约 的形式,描述了每个方法的先决条件和后置条件,以及对象创建时和未执行方法时保持的不变量。

正如 面向对象软件构造 中的语言所暗示的,这个契约是一个有用的 设计 工具:用对象接收到的消息、接收这些消息时的期望以及发送者可以期望得到什么来描述你的对象。

Eiffel 还表明,契约是一个有效的 正确性测试 工具,因为对象契约中包含的断言可以在适当的时候进行检查,无论对象是在测试还是生产系统中使用。原则上,契约甚至可以用来 生成 基于属性的测试;"对于所有(预期输入结构)->(断言结果中某些属性成立)" 除了是先决条件和后置条件的陈述之外,还有什么?在实践中,这种集成尚不存在。

根据合同描述一个对象可以做什么,对象执行该操作必须满足什么条件,以及对象执行后会发生什么,它也是每个对象 标准文档结构 的绝佳候选人。我们已经在 HTTP API 的世界中看到,Open API 规范(以前称为 Swagger,swagger.io/specification)是 API 支持的操作、其参数和响应的机器和人类可读描述。这种方法可以很容易地应用于单个对象;毕竟,一个对象代表了一个小型、隔离的计算机程序模型,因此它的消息边界 就是 支持特定操作的一个 API。

对象可以在上下文中编写、检查和更改

大卫·韦斯特将对象描述为计算机舞台上的 演员,甚至程序员拿起代表对象的 CRC 卡并扮演其在系统中的角色,解释他们使用的数据以及他们向其他对象发送的消息。对象本质上是一种实时、交互式的 思考软件 的方式,因此它们最好通过一种实时、交互式的方式 将思想转化为软件

Smalltalk 环境,包括现代的 PharoAmber——www.amber-lang.net/,证明了这样的工具是可能的。特别是 Pharo,它为开发者体验增添了新颖的功能,项目“关于”页面上的一个要点(pharo.org/about)告诉我们,“是的,我们在调试器中编码。”

目前,使用这种环境制作的软件的分发可能不是最优的。使用 Pharo,你可以将特定的类导出到一个包中,其他人可以使用已经设置好 Pharo 的包,或者你可以将整个 Pharo 环境的状态写入一个 镜像文件,并将 Pharo 虚拟机和镜像文件提供给将使用你的软件的人。Amber 也以这种方式工作,但在后台使用流行的 Bower 包管理器来管理 JavaScript,其 镜像 只包含实现 JavaScript 函数的几个类。此外,许多 JavaScript 开发者并不以传统方式 分发 他们的软件,因为它们要么被作为浏览器所需的服务提供,要么由开发者自己在 Node.js 服务中运行。

这种实时交互不仅限于 Smalltalk 世界。我正在使用 GNU Emacs——www.gnu.org/software/emacs/ 文本编辑器编写这本书的这一部分,它实际上是一个带有动态文本中心用户界面的 Emacs Lisp 解释器。在任何时候,我都可以输入一些 Emacs Lisp 并对其进行评估,包括定义新函数或重新定义现有函数。例如,给定一个包含以下内容的段落:

    (defun words () (interactive) (shell-command (concat "wc -w " buffer-file-name)))

我可以将我的光标移至段落的末尾,运行 Emacs Lisp 的 eval-last-sexp 函数,然后得到一个新的 words 函数,它返回 1909(写作时的数字)这个部分手稿中的单词数。如果它没有这样做,如果我意外地计算了字符而不是单词,我可以编辑这个函数,重新评估它,并继续使用修正后的版本。在我重新构建它的时候,没有必要退出 Emacs,因为我正在编辑它在其中运行的相同环境中的代码。

将所有这些放在一起

在这里探索的所有部分都存在,但不在同一个地方。将这些部分组合起来是一项重大任务;在分离的进程中构建对象之间的消息传递和委派可能只需要几行源代码,通过合同设计是 assert() 语句的明智应用,但提供一个整个交互式环境以允许对这样的系统进行实时开发和调试则是一项更大的任务。那么为什么考虑它呢?

速度

当开发环境和部署环境相同时,开发者可以获得更高的保真度体验,这通过减少由于环境差异而导致更改“在 CI 中中断”(甚至在生产中)的可能性,从而降低了开发周期。

使用该软件的人也可以更有信心,因为他们知道开发者是在将要使用的相同环境中构建了这个东西。此外,在这个提议的开发系统中使用合同增加了信心,因为软件被声明(并证明)对所有满意的输入都有效,而不仅仅是开发者想到的几个测试用例。

这种保真度通常是以牺牲速度为代价提供给开发者的。程序员通过网络连接到类似生产的服务器,或者在他们的本地系统上构建虚拟机或容器镜像。这段时间被添加到典型的步骤中,例如编译或链接,这些步骤来自分离开发和部署,这给了我们时间在验证到目前为止的工作时分心并失去专注。

然而,最终的速度来自于实验。当开发接近部署时,更容易提出诸如“如果我将其改为那样会怎样?”等问题,并回答它们。当系统被分解为小型、隔离、独立的对象时,更容易更改或甚至丢弃和替换需要改进或适应的对象。

虽然通过合同进行设计有其价值,但随着对被模拟系统更多属性的了解和对象形状的信心增加,逐步向对象的合同中添加细节也有其价值。合同对于文档和对象行为的信心非常有用,但这些好处不必以强迫开发者的思维过程在规定的顺序中停靠在特定站点为代价。正如我们在第一章,对立面中看到的,迄今为止面向对象编程中的许多复杂性都源于要求软件团队在面向对象软件工程过程的特定点上考虑他们的用例、类层次结构、数据共享或其他系统属性。

最好这样说,“这里有一些工具,当它们有意义时使用它们”,这样开发者的体验就不会受到限制。如果这意味着花时间设计开发者系统,以便开发、构建、文档、测试和配置正在开发的东西可以按任何顺序进行,那么就这样吧。

定制

这样的实验也适合于适应。对软件工业化的频繁呼吁包括组件的标准化和最终用户根据需要将这些组件连接在一起的能力。布拉德·科克斯(Brad Cox)的软件集成电路、萨尔·索戈扬(Sal Soghoian)的 AppleScript 字典,甚至 NPM 存储库都代表了通过定义“可重用的事物”和“它们被重用的上下文”之间的边界来设计重用方法的方法。

在所有这些情况下,尽管如此,这种区别是任意的:一个软件集成电路(IC)可以实现一个完整的应用程序,或者 Mac 应用程序的内部可能用 AppleScript 编写。在实时开发环境中,这种区别被抹去,任何部分都可以用于扩展、修改或替换。有一个著名的故事是关于丹·英加尔斯(Dan Ingalls)在为包括史蒂夫·乔布斯(Steve Jobs)在内的苹果电脑团队进行演示时,向运行中的 Smalltalk 系统添加了平滑滚动功能(www.righto.com/2017/10/the-xerox-alto-smalltalk-and-rewriting.html)。在那个时刻,丹·英加尔斯的 Alto 电脑有了平滑滚动,而其他人的电脑没有。他不需要重新编译他的 Smalltalk 机器并将电脑关闭以重新部署,它只是开始那样工作。

我的观点是,将合同添加到实时编程环境使实验、定制和适应成为可能,因为它增加了对替换部件的信心。许多面向对象程序员已经设计他们的对象以符合 Liskov 替换原则,该原则(大致上)说,如果一个对象的前置条件至多与另一个对象相同,并且它的后置条件至少与另一个对象相同,那么它可以作为另一个对象的替代。

然而,在当前环境中,这种可替换性的想法不必要地与类型系统和继承耦合在一起。在所提出的系统中,一个对象是否具有继承性是其自身的事务,所以我们问一个更简单的问题:这个对象的合约是否与该对象的使用兼容?如果是,那么它们可以被交换,我们知道事情将会工作(至少在合约足够的情况下)。如果不是,那么我们知道什么将不会工作,以及需要什么适应来连接事物。

专有性

“但我们如何赚钱?”几十年来一直是那些不愿意使用新工具或技术的开发者的口号。我们说过,当免费和开源软件使我们的源代码对用户可用,然后开始运行用户连接的 GNU/Linux 服务器,以便他们可以下载我们的 JavaScript 源代码时,我们无法赚钱。

在这里描述的系统涉及将开发和部署环境相结合,那么我们怎么可能赚钱呢?用户难道不能免费提取我们的代码并自行运行,或者将其提供给朋友,或者卖给朋友吗?

系统上的每个对象都是一个在其自身进程中运行的独立程序,其接口是松散耦合的消息发送抽象。任何特定的对象都可能是一个基于专有算法的编译可执行文件,没有源代码的分布式。或者它可能运行在开发者的自有服务器上,远程处理消息,或者它可能被部署为dApp以太坊NEO。在每种情况下,开发者都避免了将源代码部署给最终用户,虽然这意味着用户无法检查或修改该对象,但这并不阻止他们替换它。

考虑在这样的系统下软件交付的经济可能如何改变是很有趣的。目前,一次性付费的应用程序、定期订阅费、免费应用程序带有付费内容或组件,以及免费(零成本)应用程序和组件都很常见。其他模型也存在:一些 API 提供商按使用次数收费,区块链 dApps 也通过代币间接收费来执行分布式功能。一个应用程序或网络服务有一个清晰的标志,通过用户定义的入口点(他们的网址或主屏幕图标)可见。软件企业如何为履行程序性合约收费,或者为应用程序中由其他对象增强的部分收费,甚至部署后替换的部分收费呢?

安全性

在讨论对象的专有性时提到,每个对象都隐藏在松散耦合的消息发送抽象之后。这种系统对安全性的影响如下:

  • 为了使对象信任消息的内容,它必须拥有足够的信息来做出信任决定,并且有信心它收到的消息是按照预期发送的,没有修改。使用操作系统 IPC,对象之间发送的消息由内核介导,可以强制执行任何访问限制。

  • “足够的信息”可能包括消息代理提供的元数据,例如,关于发送者背景或导致发送此消息的事件链的信息。

  • 对象接收消息的形式不必是传输时的形式;例如,消息层可以在发送时加密消息并添加一个在接收时检查的认证代码,在允许对象处理消息之前检查。从事 Web 应用程序开发的开发者对此已经很熟悉了,因为他们的请求涉及 HTTP 动词,如GETPOST,以及可读数据,如JSON,但随后以压缩格式通过加密、认证的 TLS 通道发送。没有理由这样的措施需要限制在应用程序的网络边缘,也没有理由(如微服务架构所示)网络边缘和系统的物理边缘在同一个地方。

多进程

计算机在每秒单任务指令方面已经很久没有变快了。尽管如此,它们仍然从其中加载其代码和数据的内存的显著更快。

这个假设需要验证,但我的预测是,通过消息传递进行通信的小型、独立对象更适合今天的多核硬件架构,因为每个对象都是一个小的自包含程序,它应该比一个单一的应用程序进程更好地适应靠近 CPU 核心的缓存。

现代高性能计算架构已经是大规模并行系统,它们运行独立的工作负载实例,通过消息发送同步、共享数据和通信结果,通常基于 MPI 标准。在 HPC 中使用的许多处理器设计在指令频率方面甚至比桌面或服务器应用程序使用的处理器更,但单个包中具有更多的核心和更高的内存带宽。

将应用程序分解为独立的、分离的对象的想法与观察结果相一致,即我们不需要一个快速程序,而是一个由多个程序组成的快速系统。正如云计算架构一样,这样的系统可以通过扩展来提高速度。如果我们能够运行同一部件的数十个副本并在它们之间共享工作,那么我们就不必一定要使部件更快。

可用性

所有这些讨论都集中在本书中开发出的编写软件方法的益处(观察到的或假设的)。然而,我们需要现实一些,承认这里描述的工作方式尚未经过测试,并且与程序员目前的工作方式有重大差异。

Smalltalk 程序员已经深爱着他们的 Smalltalk,但 C++ 程序员也深爱着他们的 C++,因此没有一种适合所有程序员的解决方案,即使可以证明在某些所谓的软件构建过程或结果的客观属性上,一种工具或技术比其他工具或技术有优势。

有些人可能会采取“宁为鸡头,不为凤尾”的视角,而其他人可能会尝试这种方法(假设这样的系统甚至被构建出来!)并决定它不适合他们。还有一些人甚至可能会爱上这种方式想法,尽管我们发现这可能会减慢他们的速度或比他们目前的工作方式产生更低质量的结果!实验和研究将有助于找出什么有效,对谁有效,以及如何改进。

这可能是整个系统中最大的创新领域。开发者的体验通常非常保守。"现代"项目使用几十年前为了满足技术而非经验约束而出现的编辑-编译-链接-运行-调试工作流程。奇怪的是,这从来不是由拥有设计师和用户体验专家的团队提供的消费产品的首选界面。

第三部分结论

本书的故事一直是解构与重建。三十年 OOP 的巨大复杂性被解构,以找到简单的核心,并在该核心周围重建面向对象编程体验。重建包含了该范式的所有独特和重要元素,同时摒弃了由附加咨询和对现有流程的屈服带来的复杂性。

重要的是,这种新的重建仍然从计算机的两个思想流派中吸取教训,我将它们称为实验室学校图书馆学校

实验室学校

实验室学校是实验性的方法。走出去,做一件事,并根据你对它表现出的观察来调整、改进或拒绝它。不要担心做对的事情,或者把事情做对,只需确保它被完成。你可以稍后调整它。

极限编程XP)和精益创业运动都体现了实验室学校的影响。这两种方案都倡导实验和快速反馈。它们都建议从小和简单开始——XP 通过其 Ya Ain't Gonna Need It 原则,精益创业通过其 最小可行产品——然后根据反馈快速迭代。

Smalltalk 风格的面向对象编程也体现了实验室思维方式。通过消息发送的松散耦合让程序员能够轻松快速地替换系统中的协作对象。集成开发和部署环境使得一种称为调试驱动设计(Debugger-Driven Design)的风格成为可能——medium.com/concerning-pharo/pharo-50c66685913c:找出因为尚未构建而破坏系统的东西,构建它,然后让系统继续其新的行为。

图书馆学校

图书馆学校是研究驱动的方法。理解你的问题,发现一个解决方案的属性,这个解决方案适当地解决了问题,用这些属性实现解决方案。

面向对象软件工程相关的学科显示出与图书馆学校的关联。以理性统一过程(Rational Unified Process)为例,它确实促进了迭代和增量开发,但这些增量往往是累加的而不是探索性的:先构建行走骨架,然后设计、实现和测试这个用例,然后是那个用例。添加更多用例,直到资金耗尽。确保在每一步你都能保留你仔细考虑过的类关系层次。

编程中的如果类型检查通过,它就工作原则似乎来自图书馆学校。类型系统是一个构建关于使用该系统类型的软件的证明的机器。通过一致地应用这些类型来设计你的软件,你将免费获得关于软件行为的定理(ecee.colorado.edu/ecen5533/fall11/reading/free.pdf)。

通过合同进行设计展示了将图书馆学校的思想应用于面向对象编程。一个对象的主要特征不是它的命名类型,而是它的形状:它响应的消息以及它对这些消息的响应。从形式方法中采用数学证明工具并将其应用于对象的形状,你最终得到合同:一个关于对象响应的消息及其接收这些消息产生的行为的数学陈述。

图书馆

从这些思想流派中我们可以学到很多经验教训,而不是站在任何一方,这里描述的系统采用了两者的细节。不是以累加的方式让我们做这些人做的所有事情,并添加这些人做的所有事情,而是以综合的方式让我们看看这些人推广了哪些想法,以及它们如何可以结合在一起。我们有来自图书馆的合同,但不需要通过合同进行设计:它们是实验室中一个活生生的、实验性的系统的一部分,可以随时添加或删除。

当然,这个环境存在一个很大的问题,这是由合成“图书馆”学派思想产生的。这个问题就是环境还不存在。去图书馆吧!

第二部分 – 适当的举止

促使我写这部分的关键之一是拿起我的《完整的程序员》第二版——www.cc2e.com。在我的开发者生涯中,我大部分时间都有一本这本书的副本,要么是这一版,要么是第一版。尽管如此,我有一段时间没有读过它,所以我翻阅了目录,寻找一个有趣的章节来重新阅读。

唯一引起我注意的部分是关于开发者个性和自我提升的章节。我觉得这很奇怪;“完整的程序员”被广泛推荐为关于编写软件技艺的全面书籍。这是正确的;它帮助了许多程序员(包括我自己)反思他们的工作方式,理解并改进它。

《完整的程序员》当然足够厚,可以被认为是全面的。那么,为什么它有那么多关于代码应该如何编写的内容,却如此少地谈论编写代码的人呢?

我现在可以回答这个部分标题所提出的问题;这部分是关于成为程序员所需的事情,而这些事情并不特指编程。即“完整的程序员”。它从离我们很近的地方开始,包括与其他程序员合作、支持自己的编程需求以及程序员应该理解和利用的其他“软件工程”实践(我目前不确定我认为软件是否是一门工程学科,或者对于对这个运动感兴趣的人来说,是否是一门手艺——这个术语很常见,所以我将使用它)。但随着我们阅读这本书的这一部分,我们将讨论心理学和元认知——关于理解你,程序员,如何运作以及如何改进这种运作。我的希望是思考这些事情将有助于形成制作软件的哲学;一个连贯的论点,描述了制作软件的好处、值得和期望之处,什么不是,以及本部分讨论的内容如何融入这一哲学。

这本书的这一部分只有一小部分内容在我的博客上出现过*——sicpers.info。更多内容原本打算放在我的博客上,但最终被收录在这里。还有更多内容,如果没有规划我脑海中空白部分的目录,可能永远都不会被写出来。

第五章:第四章

支持软件开发的工具

引言

是的,有大量的不同工具。是的,每个人都有他们偏爱的工具。不,没有理由看不起使用与你不同工具的人。是的,喜欢vi的人很奇怪。在这一章中,我不会推荐特定的工具,但可能会介绍某些工具类别以及我找到的一些使用方法,这些方法对我有所帮助。

如果你刚开始学习编程——也许你只是上过几节课或者读过几本书——这一章应该会告诉你程序员除了在文本编辑器中输入public static void之外还做些什么。如果你更有经验,你也许还能在这里找到一些有用的信息。

版本控制/源代码管理

我想象许多读者现在可能认为关于版本控制的争论肯定已经结束了,所有开发者都在使用某种系统。遗憾的是,这显然是不真实的。让我从一个轶事开始。那是 2004 年,我刚刚开始在一家大学计算机实验室担任系统管理员。我的工作部分是维护实验室的电脑,部分是教授物理本科生编程和数值计算,部分是编写辅助教学的软件。作为这项工作的一部分,我开始使用版本控制,既用于我的源代码,也用于服务器上/etc目录中的一些配置文件。一位经验更丰富的同事看到我在这样做,告诉我我只是在给自己找麻烦;说这些小事情没有必要进行版本控制。

现在转到 2010 年,我在英国的一个大型科学设施工作。使用软件和大量的电脑,我们将原本需要整个博士研究生才能完成的工作缩短到了 1 到 8 小时之间。我是软件团队的一员,是的,我们使用版本控制来跟踪软件的变更以及了解哪个版本被发布。好吧,至少在某种程度上是这样的。文件/源代码的核心部分在版本控制中,但它的一个主要功能是提供一个脚本环境和领域特定语言(DSL),让“实验室台面”上的科学家可以编写自动化实验的脚本。这些脚本(不一定)在版本控制中。更糟糕的是,源代码被部署到实验站,如果有人发现核心中的错误,可以本地修复它,而无需在版本控制中跟踪更改。

因此,一个团队在这个设施进行了一次实验,并产生了一些有趣的结果。你后来尝试复制这个实验,却得到了不同的结果。这可能是软件相关的问题,对吧?你所需要做的只是使用与原始团队相同的软件……不幸的是,你做不到。它已经消失了。

这就是科学家未能使用软件开发工具可能会损害他们科学的一个例子。软件领域有很多“蛇油”,既有想要你使用他们的工具/方法论的人,因为他们会为此向你收费,也有决定“他们的”工作方式是正确的,而任何其他方式都是错误的人。你需要能够穿越所有这些废话,找出特定工具和技术如何影响你试图做的实际工作。科学哲学目前高度重视可重复性和审计。版本控制支持这一点,因此对于在科学领域工作的程序员使用版本控制是有益的。但他们并没有;至少不是始终如一。

在其最简单的形式——我在 2004 年使用的那种形式——版本控制是一个大的撤销栈。只是,与一系列撤销和重做命令不同,你可以留下消息解释每个变更是由谁做出的以及为什么。即使你是在独自工作,这也是一个非常好的功能——如果你尝试的事情变得混乱或者没有成功,你可以轻松地回滚到一个可工作的版本,并从那里继续。

一旦你更熟悉版本控制系统的功能,它就可以成为配置管理的强大工具。对同一产品的不同功能和错误修复工作可以并行进行,当工作准备好时,可以集成到产品的一个或多个版本中。详细讨论这个工作流程超出了我在这里愿意涵盖的范围:我推荐 Travis Swicegood 的关于版本控制的实用程序员书籍,例如使用 Git 进行实用版本控制——pragprog.com/book/tsgit/pragmatic-version-control-using-git

关于版本控制和协作

版本控制并不比其他文档管理系统,如 SharePoint,更是一种协作工具。整合(或合并)不同人之间的相关工作很困难,需要了解代码的含义以及变更如何交互。版本控制系统没有这种知识,因此除了最简单的情况外,无法简化合并过程。它确实让你可以推迟问题直到你想要面对它,但仅此而已。

一些工具——例如,GitHub —— www.github.com – 在核心版本控制系统周围提供社交功能。然而,知道从谁那里整合什么,何时整合,以及解决冲突的问题仍然存在。社交功能为你提供了一个讨论这些问题的场所。

分布式版本控制

这些年来,我使用过许多版本控制系统,从与本地文件系统工作的简单工具到极其昂贵的商业产品。我现在最喜欢的工作方式是使用darcs,它们都以非常相似的方式工作)。

使用分布式版本控制系统(DVCS),将本地项目纳入版本控制非常简单,因此即使是玩具项目和原型也可以进行版本控制。这使得它们比早期的系统(如RCS(反应控制系统)和SCCS(源代码控制系统))更适合版本控制本地文件,因为这些系统将整个仓库(即构成版本化项目的所有文件)视为原子单元。换句话说,仓库可以处于一个版本或另一个版本,但永远不会处于一个中间状态,其中某些文件比其他文件的版本更早。

早期的系统,如 RCS,没有这种限制。在 RCS 中,每个文件都是独立版本化的,因此每个文件都可以在不同的版本上检出。虽然这更灵活,但也引入了某些问题。例如,考虑以下图中的文件。其中一个文件包含一个在其他文件中使用的函数。您需要更改函数的签名,以添加一个新参数。这意味着需要更改所有三个文件。

图 4.1:跨越多个文件的依赖关系

图 4.1:跨越多个文件的依赖关系

在原子版本控制系统中,文件可以同时使用一个参数检出特定版本的文件,或者同时使用两个参数检出特定版本的文件。一个按文件版本化的系统将允许检出任何版本的组合,尽管其中一半的组合没有意义。

一旦在 DVCS 仓库中本地版本化了项目,与他人共享就变得简单,可以通过多种方式完成。如果您想在像BitBucket这样的托管服务上备份或共享仓库——www.bitbucket.org,您将其设置为远程仓库并推送内容。然后,合作者可以从远程版本克隆仓库并开始工作。如果他们与您在同一网络中,那么您只需共享包含仓库的文件夹,而无需设置远程服务。

个人经验

在某些情况下,需要这些方法的组合。我使用的所有 DVCS 工具都支持这一点。在一个最近的项目中,所有内容都托管在远程服务上,但仓库中存储了数百兆字节的资产。对于办公室的计算机来说,不仅克隆远程仓库,而且相互对等以减少资产更改时的时间和带宽使用是有意义的。这种情况看起来如下图所示。

图 4.2:分布式版本控制系统的配置可以摆脱集中式系统所需的“星形”拓扑结构

图 4.2:分布式版本控制系统的配置可以摆脱集中式系统所需的“星形”拓扑结构

使用集中式版本控制系统来做这件事是可能的,但会很丑陋。其中一个开发者需要完全同步他们的工作副本与服务器,然后将整个仓库及其元数据完全复制到所有其他开发者的系统上。这比仅仅复制仓库之间的差异要低效。一些集中式版本控制系统甚至不支持这种方式,因为它们跟踪他们认为你在服务器上签出的文件。

DVCS 带来的另一个好处——部分归功于改进的算法,也归功于其分布式特性——是创建和销毁分支的便捷性。当我主要使用集中式版本控制(主要是 Subversion 和 Perforce)时,分支是为特定任务创建的,比如新版本发布,而我所在的团队发明了工作流程来决定何时将代码从一条分支迁移到另一条分支。

在 DVCS 中,我经常每小时创建一个分支。如果我想开始一些新的工作,我会在本地仓库版本中创建一个分支。过了一段时间,我可能完成了,分支被合并并删除;或者我确信这个想法是错误的——在这种情况下,它只是被删除;或者我想让别人看看,我就不合并这个分支而将其推送到远程。所有这些在集中式 VCS 中都是可能的,尽管速度慢得多——而且你需要网络访问才能创建分支。

持续集成与持续部署

在刚刚讨论了版本控制之后,是时候宣布我看到的比其他任何错误都更频繁的 VCS 错误了——这个错误是每个人(包括我自己)都会犯的,无论他们的经验或专业知识如何。获胜者是…

在项目中添加新文件,但忘记将其添加到仓库中。

我并不经常这样做——可能每月不到一次。但每次我这样做,当团队中的其他开发者同步他们的仓库时,我们就会陷入一种情况:对我而言一切正常,但他们无法构建。

如果我们很幸运,错误报告将指出文件未找到,我们可以快速解决问题。如果不这样,可能会有一些其他错误,比如缺失符号或其他需要花费时间追踪的问题,在发现根本原因之前。

如果我们有一种形式的机器人,它会看到每一次的提交,获取源代码,并尝试构建产品。如果它做不到,如果它出现并抱怨导致构建失败的人,那就太好了。

结果表明,我们已经生活在未来一段时间了,那个机器人已经存在了。它被称为持续集成,或 CI。

为什么使用持续集成?

找到那些缺失的文件并不是 CI 好处的唯一之处。如果你有自动化测试(见第五章,编码实践),CI 系统可以在每次更改时运行测试,并报告任何问题。我团队的 CI 服务器配置为运行分析工具(本章讨论),如果该工具发现任何问题,则认为构建失败。一些项目会自动生成 API 文档并将其发布到 web 服务器上。

一旦通过了所有测试,甚至可以让构建对人们安装可用。这与持续部署的概念相关:如果软件的某个版本看起来足够好可以使用(也就是说,它没有失败你给它做的任何测试),那么就开始使用它。你仍然可能会发现自动化测试没有暴露的问题,但你会比没有立即部署时更早地发现这些问题。

CI 的一个最终好处——虽然相当微妙但非常有用——是它迫使你设置你的项目,以便可以从版本控制中检出并自动构建。这意味着即使当人类程序员在处理项目时,他们也很容易设置源代码并开始变得高效。那个人可能是你,当你得到一台新笔记本电脑时。也可能是承包商或加入团队的新的员工。无论如何,如果只有一个步骤可以获取项目并构建它,那么他们可以快速上手,而不是询问你如何获取某个库或配置某个插件。

CI 在真实团队中的应用

我参与过的某些团队对使用 CI 投入了极大的热情,以至于他们雇佣了专人维护 CI 基础设施(这不是一份全职工作,所以他们通常还会负责其他支持工具的维护并对其使用提供咨询)。在其他团队中,维护 CI 的责任则落在了开发者身上。

在第二种情况下,困难在于知道何时照顾 CI 而不是做项目工作。例如,在本节编写前的一个月,我不得不将我团队的 CI 系统迁移到不同的硬件上。尽管我试图确保系统配置在两个环境之间没有变化,但其中一个项目中的测试将不再运行。

问题是,测试在所有开发者的 IDE 上都运行得很好。我们真的需要花费更多的时间从增加客户支付的产品价值中抽离出来,去手把手地指导一些困惑的机器人吗?

我认为现在不使用 CI 进行开发是有风险的。是的,没有它,我可以避免所有问题。是的,可能不会出任何问题。但为什么要冒险呢?为什么不花一点额外的时间来确保我尽早发现问题呢?这是现在花一点时间,以备将来可能节省很多。因此,我试图在必要时找到时间维护 CI 服务。

构建管理

在上一节中,我提到采用 CI 的好处之一是它迫使你简化项目的构建(我的意思是编译源代码、转换资源、创建包以及任何其他将项目团队创建的输入转换为将被客户使用的产品的操作)。确实,为了使用 CI,你必须将构建过程压缩到自动化流程可以在任何源代码修订后完成它的程度。

没有必要编写脚本或其他程序来完成这项工作,因为已经存在大量的构建管理工具。从高层次来看,它们都做同样的事情:它们接受一组输入文件、一组输出文件,以及一些关于如何从一组到另一组进行转换的信息。当然,它们如何做到这一点因产品而异。

习惯或配置

一些构建系统,如 makeant,需要在它们能够做任何事情之前,让开发者几乎告诉它们关于项目的所有信息。例如,虽然 make 有一个将 C 源文件转换为目标文件的隐式规则,但它实际上不会执行该规则,直到你告诉它你需要目标文件用于某个目的。

相反,其他工具(包括 Maven)对项目有一些假设。Maven 假设名为 src/main/java 的文件夹中的每个 .java 文件都必须编译成一个将成为产品一部分的类。

配置方法的优点是,即使对系统了解不多的人也能发现它。有人手持一组源文件、grep 和一点耐心,可以从 MakefileXcode 项目中找出哪些文件被构建成了哪些目标,以及如何构建。因为有一个完整的(或几乎是完整的)关于如何构建一切的规范,你可以找到需要更改的内容,以便使其行为不同。

这种可发现性的缺点是,你必须指定所有这些内容。你不能只是告诉 Xcode,名为 Classes 的文件夹中的任何 .m 文件都应该传递给 Objective-C 编译器;你必须给它一个包含所有这些文件的庞大列表。添加一个新文件,你必须更改这个列表。

在基于习惯的构建系统中,这种状况正好相反。如果你遵循习惯,一切都会自动完成。然而,如果你不知道这些习惯,它们可能很难发现。我曾经在一个 Rails 项目中遇到过这样的情况,静态资源(如图像)保存的文件夹在两个版本之间发生了变化。在启动应用程序时,我的所有图像都没有被使用,而且不清楚原因。当然,对于知道这些习惯的人来说,在不同项目之间转移时没有学习曲线。

总体来说,我更喜欢一种由惯例引导的方法,前提是这些惯例在某处有很好的文档记录,这样就可以轻松地了解发生了什么,以及当你需要时如何覆盖它。对我来说,减少努力和增加一致性的好处超过了偶尔遇到的惊喜。

生成其他构建系统的构建系统

一些构建过程变得如此复杂,以至于它们产生了另一个构建系统,在构建之前为目标系统配置构建环境。一个典型的例子是 GNU Autotools,——它实际上有一个三级构建系统。通常,开发者会运行autoconf,这是一个检查项目以确定后续步骤应该提出什么问题的工具,并生成一个名为configure的脚本。用户下载源代码包并运行configure,它会检查编译环境并使用一系列宏来创建一个 Makefile。然后 Makefile 可以编译源代码(最终!)创建产品。

Poul-Henning Kamp所论证——queue.acm.org/detail.cfm?id=2349257),这是一个糟糕的架构,它增加了不必要的层来处理那些没有编写成可移植到将使用其环境的代码。用这些工具编写的软件难以阅读,因为你必须阅读多种语言才能理解一行代码是如何工作的。

考虑你在项目中报告的一个特定 C 函数中的缺陷。你打开该函数,发现有两个不同的实现,由一个#ifdef/#else/#endif预处理器块选择。你搜索该块使用的宏,并在config.h中找到它,因此你必须阅读configure脚本以了解它是如何设置的。为了发现那个测试是否做得正确,你需要查看configure.ac文件以了解测试是如何生成的。

使用这种复杂过程的唯一理由可能是它被认为是一种传统,并且你的目标用户期望这样做,但即使如此,我也会质疑这种期望是由技术需求还是由斯德哥尔摩综合症——en.wikipedia.org/wiki/Stockholm_syndrome驱动的。如果你的产品不需要可移植性,那么就没有必要增加所有这些复杂性——即使它确实需要,也可能有更好的方法来解决你的产品的问题。一个明显的方法是针对可移植平台,如 Mono 或 Python。

缺陷和工作跟踪

在它们的大部分历史中,计算机擅长一次只做一件事。即使是一个单独的客户或用户也能比这更好地并行处理,在你还在处理一件事的时候,他们可能会想出(并制作)多个请求。

将所有这些请求都写下来并跟踪你和你的同事在每个请求上的进度,这样你们就不会都试图解决相同的问题,并且可以通知客户哪些问题已经修复。bug 跟踪器(有时更通称为问题跟踪器或工作跟踪器)旨在解决这个问题。

何时以及什么内容进入?

我参与过一些项目,其中 bug 跟踪器在项目开始时就被所有项目功能请求填满(这个讨论与第十三章“团队合作”中软件项目管理模式的处理略有重叠)。这引入了一些问题。一个是大列表需要大量的梳理和编辑,以保持相关性,因为功能被添加和删除,分散在多个开发者之间,或者被发现依赖于其他工作。第二个是心理上的:在很长的一段时间里,项目团队成员将看到令人沮丧的尚未完成的事项列表,就像西西弗斯站在山脚下仰望他的石头一样。项目从一开始就会像死亡行军一样。

我的偏好是采用迭代方法来攻击工作跟踪器。当决定下一个构建将包含哪些内容时,将这些任务添加到工作跟踪器中。完成它们后,标记为已关闭。唯一从一次迭代保留到下一次迭代的东西是那些在计划中未能完成的任务。现在,跟踪器中的大列表始终是我们已经完成的大列表,而不是仍然剩下的大列表。这与看板系统类似,团队将有一个固定的“容量”的待办工作。当他们从待办桶中提取工作开始工作时,他们可以要求桶得到补充——但永远不要超过其容量。

我报告 bug 的方法不同。除非是我在现在工作的代码中的小事,这样我可以在几分钟内解决问题并继续前进,否则我总会立即报告。这意味着我不会忘记问题;修复是隐式地计划在下一个迭代中进行的,遵循乔尔测试规则,即在添加新代码之前修复 bug,我们可以看到每个产品构建中发现了多少 bug。(现在回想起来,我意识到这一章涵盖了测试中包含的许多要点。也许你应该只测量你团队相对于乔尔测试的 12 个点的表现,并修复你回答“否”的任何问题——www.joelonsoftware.com/articles/fog0000000043.html)。

如何精确跟踪?

所以,你设法在 2 小时内修复了那个 bug。但是,它实际上是 2 小时吗,还是 125 分钟?你花了那 2 小时仅仅修复 bug,还是在这段时间内还回答了关于工程师与销售之间的口哨赛跑的邮件?

能够比较估计时间与实际时间可能是有用的。我不确定“速度”——估计时间与实际花费在任务上的时间的比率——特别有帮助,因为在我的经验中,估计并不总是以一个常数因子错误。知道你什么工作估计得不好是有帮助的。你是否没有意识到添加新功能的风险,或者你是否倾向于认为所有错误修复都是微不足道的简单?

因此,精确的测量并不特别有帮助,了解这一点是有用的,因为准确性可能并不存在来支持这种精确度。我通常只在开始工作和结束工作的时候看我的手表,并四舍五入到最近的十五分钟或半小时。这意味着我的时间记录包括了我在修复错误时所做的所有那些中断和小的任务——这是可以接受的,因为它们减慢了我的速度,这需要记录。

估计甚至并不那么准确。我和我的团队玩的游戏是这样的:团队中的每个开发者(只有开发者)独立写下我们计划的任务预计需要多长时间。他们可以从中选择以下之一:1 小时、2 小时、4 小时、8 小时,或者不知道。如果我们认为一个任务将需要超过 8 小时,我们会将其分解并估计更小的任务部分。

对于每个任务,每个人都提出他们的估计。如果他们的估计大致相同,那么我们就选择最大的数字并继续。如果有意见分歧——也许一个开发者认为某件事需要一个小时,而另一个人认为需要一天——我们会讨论这个问题。很可能,团队中的一个人(或更多人)依赖于需要公开的隐性知识。通常,可以快速解决这些差异并继续下一件事。

集成开发环境

嗯,实际上,我想你的环境并不需要完全集成。长期以来,我的工具集是项目构建器、界面构建器、WebObjects 构建器、EOModeler 和编辑器的组合。它确实需要比简单的“文本编辑器和make”组合更高效。

什么大问题?为什么对文本编辑器这么苛刻?每次你必须停止制作软件来处理你的工具时,你都有可能失去注意力,忘记你在做什么,然后需要花几分钟重新熟悉问题。失去几分钟听起来并不像什么大问题,但如果你每天工作的时候每小时都这样做几次,它很快就会变成一个令人沮丧的生产力下降。

在接下来的几年里,你将大部分工作时间都在使用你的 IDE,每一天都在使用。你应该在它上面进行大量投资。这意味着为一个好的 IDE 花一些钱,它比免费的替代品更好。这意味着训练自己掌握技巧和快捷方式,这样你就可以不用思考就能完成它们,节省偶尔的几秒钟,更重要的是,保持你专注于工作。如果你的环境支持,甚至可能意味着编写插件,这样你就可以在不切换上下文的情况下做更多的事情。

在一些插件丰富的环境中,你可能会整天都待在集成开发环境(IDE)里,从未离开过。例如,Eclipse 现在包括了Mylyn (eclipse.org/mylyn/start/)这样一个以任务为中心的插件,因此你可以在 IDE 内部与你的缺陷跟踪器进行交互。它还会让你只关注与你当前正在工作的任务相关的文件。

你不仅需要深入掌握你选择的 IDE,还需要广泛了解替代方案。你最喜欢的工具的未来版本可能会带来很大的变化,以至于你切换到不同的应用程序会更加高效。或者,你可能会开始在一个你首选的 IDE 不可用的项目中工作;例如,你无法(轻易地)在 Xcode 中编写 Mono 应用程序,或者在 Visual Studio 中编写 Eclipse RCP 应用程序。

这种将开发环境限制在特定平台上的限制,无论出于技术还是商业原因,都是不幸的。这正是“只需使用一个文本编辑器”的人群有道理的地方:你可以一次性学习 emacs,无论你最终使用哪种编程语言,你都不需要再次学习如何使用编辑器来编写代码。既然你将在这些环境中度过你整个职业生涯,那么你对已知功能的任何更改都代表着巨大的低效。

注意,上述所有 IDE 都遵循相同的通用模式。当人们争论“哪个 IDE 是最好的?”时,他们实际上讨论的是“你更喜欢使用哪个带有构建按钮的略微增强的等宽字体文本编辑器?”Eclipse、Xcode、IntelliJ、Visual Studio……所有这些工具都基于相同的设计——让你能够查看源代码并更改源代码。作为次要效果,你还可以执行诸如构建源代码、运行构建的产品以及调试它等操作。

我认为世界上最成功的 IDE 是一个根本不是那样设计的 IDE。它是那些比已经提到的任何 IDE 都被更多非软件专业人士使用的 IDE。它是那些不需要你练习多年才能成为一个优秀的 IDE 用户之前就能获得好处的 IDE。它是当商业分析师、办公室助手、会计和项目经理需要他们的电脑运行一些自定义算法时都会转向的 IDE。世界上最成功的 IDE 是 Excel。

在电子表格中,最显眼的是输入和结果,而不是那些将你从一个带到另一个的中间过程。你可以通过输入不同的输入并观察结果在你面前变化来测试你的“代码”。你可以看到中间结果,不是通过中断和逐步执行,或者放入日志语句然后切换到日志视图,而是通过将算法分解成更小的步骤(或者如果你愿意可以称之为函数或过程)。然后你可以可视化这些中间结果是如何随着输入和输出的变化而变化的。这比 REPLs 提供的反馈更快。

许多电子表格用户自然采用“测试优先”的方法;他们为已知结果应该是什么的输入创建输入,并逐步尝试构建一个能够实现这些结果的公式。当然,还有有趣的可视化,如图表等(尽管产品的质量各不相同)。在 Xcode 中绘制图表是……具有挑战性的。确实,你根本无法做到这一点,但你可以让 Xcode 创建一个可以自己生成图表的应用程序。结果与工具相去甚远。

静态分析

第五章,编码实践中,有一个关于代码审查的部分。考虑到审阅者会找到并专注于他们能找到的最简单的问题,如果能够消除所有这些琐碎的问题,迫使他们寻找更实质性的问题,那岂不是很好?

这就是静态分析所做的事情。它可以在不运行产品的情况下自动发现代码中的问题,但这些问题的离题性质使得编译器警告不适用,或者发现这些问题的速度太慢,以至于编译器不适合作为搜索它们的工具。

什么是离题问题?通常,那些需要了解你使用的函数或方法的语义的问题——这种知识超出了编译器的范围。例如,考虑一个 C++的destroyObject<T>(T t)函数,它会删除其参数。用相同的参数调用该函数两次将是一个错误——但是编译器并不知道这一点,如果它只是检查函数签名的话。其他问题则是关于风格的。例如,苹果的 C 语言 API 有一个与其内存管理规则相关的命名约定:当调用者拥有返回的对象时,函数名包含Create;当callee拥有时,包含Get。使用 C 语言混合这些名称并不是错误,所以编译器不会告诉你,但分析器可以。

基本上没有理由避免使用静态分析器(如果你的理由是还没有为你使用的语言/框架/等等提供静态分析器,你可能选择了一个尚未准备好的语言/框架/等等。关于这一点,在第十二章“商业”中有一个部分)。它将为你发现容易修复的错误,并迅速训练你从一开始就不犯这些错误。

代码生成

在许多应用中,有很多功能实现起来很简单,但必须反复执行。也许是将模型对象的数组转换为列表视图,从数据库模式创建类,或者从文本文件创建编译时常量列表。

这些情况通常可以通过生成代码来自动化。想法是将问题表达在一个简洁的表示中,然后将其翻译成可以集成到你的程序中的东西。这正是编译器所做的事情;尽管许多编程语言远非简洁,但它们仍然比机器的本地指令代码要容易操作得多。

编写自己的生成器不应是首选方案

正如代码生成器使创建产品变得更容易一样,它也使得调试变得更困难。以本章前面讨论的autotools构建系统为例。想象一下,一个开发者正在调查一个报告的问题,其中一个测试失败了(今天我不得不处理的问题)。日志文件告诉他们哪个 C 程序封装了测试,但开发者不能直接修改那个程序。他们必须发现configure脚本在哪里生成那个程序,以及它是通过什么方式尝试达到这个目的的。然后他们必须找出在configure.ac中那个 shell 脚本的部分是如何生成的,并找出对m4宏的修改,以便在两步之后实现 C 程序的期望变化。

简而言之,如果你的目标环境提供了原生解决你问题的工具,那么在诊断后续问题时,这种解决方案将需要更少的逆向工程。只有当这种解决方案过于昂贵或容易出错时,代码生成才是一个合理的替代方案。

本节开头给出的许多案例都是数据驱动的,例如从数据库模式推导出某些对象关系映射(ORM)系统的类描述的情况。这是一个某些编程语言允许你解决此问题而不在它们的语言中生成代码的情况。如果你可以在运行时解析发送给对象的短信,那么你可以告诉该对象其对象所在的表,并且它可以决定任何消息是否对应于该表中的列。如果你可以在运行时添加类和方法,那么你可以在应用程序连接到数据库时生成所有的 ORM 类。

这种功能的存在和适用性在很大程度上取决于你针对的环境,但在开始编写生成器之前,寻找并考虑这些功能。

当生成器不会被程序员使用时

如果这个设施的目标“客户”不是另一个开发者,那么尽管实现复杂度增加,生成器通常比功能齐全的编程语言是一个更好的选择。

在这个背景下,经常探索的解决方案是领域特定语言(DSL),这是一种非常有限的编程语言,它暴露的语法和功能与客户理解的问题比计算机科学概念更接近。我参与过的许多项目都使用了 DSL,因为它们在让客户根据需要修改系统与避免复杂的配置机制之间提供了一个很好的权衡。

案例研究

使用该应用程序的“客户”不一定是最终产品的终端用户。在我参与的一个项目中,我创建了一个领域特定语言(DSL)供客户使用,以便他们能够定义项目中用于游戏化功能的成绩。一个解析应用程序会告诉他们定义中的任何不一致性,例如缺失或重复的属性,并且还生成了一组对象,这些对象将在应用程序中实现这些成绩的规则。它还可以生成一个脚本,连接到应用商店,告诉它这些成绩是什么。

第六章:第五章

编码实践

引言

如果你通过阅读书籍或在线课程来学习编程,你可能坐在电脑前,使用文本编辑器或 IDE,完全解决每个问题。大多数软件团队都有两个额外的问题需要应对——他们正在编写的应用程序更大,并且有不止一个人同时在工作产品上。在本章中,我将探讨一些在更大项目上进行编程的常见方法(尽管,团队合作在这个问题中扮演了如此重要的角色,以至于在本书后面的章节中有一个专门的章节来讨论它)。

本章的大部分内容将作为快速参考,包括一个内联阅读列表和一些额外的观点。原因是这些概念太大,无法在这本小说长度的一章中详细说明。

测试驱动开发

TDD测试驱动开发)是一个很大的话题,关于它的书籍已经有很多了。确实,其中一本书是我写的:《测试驱动 iOS 开发》(www.pearsoned.co.uk/bookshop/detail.asp?item=100000000444373)。所以,这里不会过多地深入细节。如果你之前从未遇到过测试驱动开发,或者“红-绿-重构”这个短语,我推荐阅读《通过测试引导面向对象软件开发》(www.growing-object-oriented-software.com/)(当然,除非你专注于 iOS)。

TDD 的目的

人们谈论测试驱动开发作为一种确保高测试覆盖率的方法。当然,它确实做到了这一点。但它的主要用途是一个 设计工具。你可以根据你在产品中需要如何使用该类来构建模块或类的可执行规范。通常,我会在设计类的时候创建一些测试,但随着代码的变化,它们变得过时,我会移除它们。

我之前通过编写一系列测试并将实现工作委托给其他开发者来分配课程。我在周五晚上留下了一个失败的测试,这样我知道周一早上应该做什么(#error C 预处理器命令,它通过插入带有自定义消息的编译器错误来使用,对此也很有用)。TDD 在生成自动化回归测试之外还有很多实用工具。

注意,TDD 只在你将自己限制在可以通过 TDD(轻松地)实现的设计时,才能帮助你进行设计。这并不是坏事,因为它意味着所有内容都将以相似、可理解的方式进行设计。就像一本杂志有一个语气和风格指南,这样读者对任何文章都有一个基本的期望水平。

从允许 TDD 引发设计选择中得出的特定约束,或者至少是建议,包括你的设计可能会是松散耦合的(也就是说,每个模块不会在很大程度上依赖于系统中的其他模块)以及从外部注入的可互换依赖项。如果你的反应是“太好了——这正是我想要的”,那么你不会有任何问题。

我正在编写的软件无法进行测试

实际上,可能可以。除了前面提到的书中提供的示例代码外,我在写的每个项目中都有未测试的代码。在大多数情况下,它可能确实可以测试。让我们看看一些没有编写测试的真正原因。

我已经编写了没有测试的代码,无法想出如何进行回溯测试

这是一个常见的抱怨。不要让 TDD 的支持者自鸣得意地说“好吧,你一开始就应该编写测试”,这是教条且无用的。此外,已经太晚了。相反,你应该决定你是否愿意(并且能够)花时间更改代码,使其易于测试。

当然,不仅仅是时间;任何软件的任何更改都存在风险。 – 正如本书其他地方提到的,你编写的任何代码都是一种负债,而不是资产。关于是否将代码适应以支持测试的适应性的决定,应考虑的不仅仅是工作的成本,还有执行工作的潜在风险。(我故意将这项工作称为“适应”而不是“重构”。重构意味着在不影响行为的情况下更改模块的结构。在你设置测试之前,你不能保证行为没有改变。)这些需要与代码在测试下的潜在好处以及错过将代码整理好的机会成本相平衡。

如果你决定确实想要继续进行这些更改,你应该规划你的方法,以确保为测试所做的支持工作不会过于侵入性。在你看到这些更改是否反映了你的期望之前,你不想改变软件的行为。在这方面,迈克尔·费瑟斯的《与遗留代码有效工作》(c2.com/cgi/wiki?WorkingEffectivelyWithLegacyCode)是一本很好的资源。

我不知道如何测试那个 API/设计/等等

通常,“这个无法测试”归结为“我不知道如何测试它。”有时,确实存在某些特定的 API 不适合独立使用的情况。一个很好的例子是低级图形代码,它通常期望存在一个你正在绘制的上下文。在一种方式下重现这个上下文,以便测试框架捕获和检查绘图命令,这可能非常困难(如果确实可能的话)。

你可以通过将问题 API 包装在你自己设计的接口中来提供这种检查能力。然后,你可以用可测试的实现来替换它——或者如果需要,用替代 API。好吧,你写的适配器类可能仍然无法进行测试,但它应该足够薄,以至于风险很低。

在其他情况下,通过一点思考,可以找到测试代码的方法。我经常被告知,带有大量 GUI 代码的应用程序无法进行测试。为什么不能呢?

GUI 应用程序中有什么?首先,有大量的数据模型和“业务”逻辑,这些在其他任何上下文中都是相同的,并且可以轻松地进行测试。然后,是与 UI 的交互:MVC 世界中的“控制器”层。这是通过触发模型中的变化来响应来自 UI 的事件,并通过更新视图来响应来自模型的事件的代码。这也很容易测试,通过模拟事件并确保控制器正确地响应它们;模拟交互的“另一端”。

这就只剩下视图层中的任何自定义绘图代码了。这确实可能既困难(见上文)又无关紧要——有时,图形的重要性不在于它们的“正确性”,而在于它们的美学品质。你无法真正为这一点推导出自动测试。

如果你的应用程序确实主要是自定义绘图代码,那么:(i)我可能愿意承认其中大部分无法进行测试;(ii)你可能需要重新思考你的架构。

我现在没有时间

好吧!这就是“为什么没有对这个进行测试?”的真正答案。实际上,编写没有测试的代码可能比创建测试更快、更便宜,尤其是如果需要考虑如何测试该功能的话。然而,正如我之前所说的,对测试努力的全面成本分析应该包括没有测试可能带来的潜在成本。而且,正如我们所知,预测未测试代码中可能存在的错误数量是困难的。

那么,测试驱动开发是不是一个银弹?

正如你将在本章后面看到的那样,人们并不相信存在一个制作软件的银弹。很多人对 TDD 得到的结果感到满意。其他人则对其他实践得到的结果感到满意。我的观点是,如果你正在制作解决问题的东西,并且可以非常有信心地证明你所做的是在解决问题,那么你就在做出有价值的贡献。就我个人而言,我目前对 TDD 作为一种展示我如何用软件解决问题各个部分的方法感到满意。

领域驱动设计

领域驱动设计DDD)这个术语是在 2004 年出版的同名书中提出的——domaindrivendesign.org/books/evans_2003,尽管其大部分原则在面向对象分析和设计实践者中已经存在了相当长的时间。实际上,DDD 的核心可以被认为是源自 Simula 67 中使用的模拟技术——这是一种影响了 C++设计的语言。

简而言之,许多软件(尤其是“企业”软件)都是作为特定问题的解决方案而创建的。因此,软件应由软件专家与领域专家共同设计。他们应该使用一个共享的问题域模型,这样整个团队都在努力解决同一个问题。

为了减少沟通问题,定义了一个“通用语言”——一个在文档和软件中使用的通用术语表。这包括源代码——类和方法使用通用语言命名,以反映它们解决的问题部分。

我认为是学习领域驱动设计的一些原则最终让我对面向对象编程OOP)有了“点击”的感觉(本章后面还有更多关于 OOP 的内容)。当我开始接触 C++和 Java 等语言时,我已经做了很长时间的 C 和 Pascal 编程。虽然我能看到方法属于类,就像模块一样工作,但决定什么应该是一个对象,它的边界在哪里,以及它如何与其他对象交互,这让我花了很长时间才掌握。

在某个时候,我参加了一个关于领域建模的培训课程——它使这个过程变得非常简单。其核心内容大致如下:倾听领域专家描述一个问题。每当他们用名词描述问题域中的概念时,那就是一个候选类,或者可能是类的属性。每当某事被描述为动词时,那就是一个方法候选。

将问题规范转换为对象和动作的简短描述对我来说是一个巨大的启发;我无法用其他方式思考面向对象编程。

行为驱动开发

我发现很难决定是把 BDD(行为驱动开发)放在这一章,还是与团队合作一起讨论,因为它实际上是一种伪装成编码实践的沟通练习。但无论如何,它就在这里。确实,本章中的许多部分都会在编码和沟通之间徘徊,因为编程是一种协作活动。

BDD 实际上是一种其他技术的融合。它严重依赖于 DDD(领域驱动设计)的理念,如通用语言,并将它们与测试驱动开发相结合。主要创新是在功能级别应用测试优先原则。使用通用语言作为领域特定语言martinfowler.com/books/dsl.html),团队与客户合作,以可执行的形式表达功能规格,作为自动验收测试。然后,开发者努力满足验收测试中表达的条件。

我自己的经验是,BDD 往往停留在对话层面,而不是实施层面。敏捷团队(包括其客户)很容易就故事的可接受标准进行合作,然后技术团队成员根据这些标准实施评估系统的测试。对于团队来说,合作编写自动化测试,其结果让客户确信用户故事已被正确构建,这是很难的——我从未见过这种情况发生。

xDD

似乎每次开发者大会都会出现一个新的(某种)驱动开发的流行词汇。TDD;BDD;验收测试驱动开发www.methodsandtools.com/archive/archive.php?id=72);模型驱动开发social.msdn.microsoft.com/Forums/azure/en-US/d9fa0158-d9c7-4a88-8ba6-a36a242e2542/model-driven-development-net?forum=dslvsarchx)。有些人认为这太多了——scottberkun.com/2007/asshole-driven-development/

许多新术语都是由那些希望在他们刚刚定义的领域内开辟自己的一片天地的人引入的。许多只是吸引人的名字,涵盖了现有的实践。——的确,TDD 实际上是极限编程中流行的测试优先实践的编码化。但这并不意味着它们没有价值;有时,真正原创的部分确实是新颖的,值得了解。而且,围绕这些技术聚集的社区有自己的习俗和工作方式,值得探索。

设计由合同

关于我最近的一个软件项目,我有点儿坦白:里面有很多单元测试。但是,对于每一个测试断言,代码本身中都有超过三个断言。

这些对于记录我对代码如何组合的假设非常有价值。虽然单元测试表明每个方法或类在独立工作时都按预期工作,但这些断言是确保边界尊重方法内部所做的假设——也就是说,它们充当了一种集成测试的形式。

我所写的断言主要分为三类——测试当方法进入时期望是否得到满足,其结果是否如我在返回之前所期望,以及方法执行期间进行的某些转换是否产生符合特定条件的结果。在开发者构建中,每当这些假设中有一个未得到满足时,应用程序会在失败的断言处崩溃。然后我可以决定是否需要更改该方法,或者它被使用的这种方式是否错误。

在 1988 年设计Eiffel 语言(docs.eiffel.com/book/method/object-oriented-software-construction-2nd-edition)时,Bertrand Meyer 将一个包含三种不同类型测试的“合同”形式化,这些测试与上述断言类似:

  • 前置条件应在函数进入时为真

  • 后置条件应在函数退出时为真

  • 不变性在所有“稳定”时刻都保持为真——在构造函数退出后,以及在任何对象的方法没有被执行的时刻。

与将条件编码为断言不同,在 Eiffel 中,它们实际上是方法定义的一部分。合同形式化了方法调用者与其实现者之间的关系:调用者需要在调用方法之前确保满足前置条件。作为回报,被调用者承诺满足后置条件。这些条件可以通过编译器插入到代码中作为断言,以验证类在运行时是否表现正确。你也可以想象将一个自动检查器,如Klee(klee.github.io/getting-started/),指向一个类;它可以检查方法的所有代码路径,并报告那些即使它们从满足前置条件和不变性开始,但最终没有满足后置条件或不变性的路径。

Meyer 创造了设计合同这个术语来指代在 Eiffel 方法定义中包含前置条件、后置条件和不变性的做法。实际上,这个术语是他的公司拥有的商标;其他语言的实现被称为合同编程或合同编码(幸运的是,不是合同驱动开发…)。

正如我们所看到的,即使没有语言支持这种功能,我也倾向于使用合同编程的糟糕替代品。我发现这些合同式断言在开发中失败得比单元测试失败得多;对我来说,合同编程是比 TDD 更好的早期警告系统。

按规格开发

据我所知,这目前不是一种常见的开发实践。但作为测试驱动开发的自然发展,我认为它值得提及和考虑。

单元测试,即使作为 TDD 的一部分使用,也是以定制的方式使用的——作为我们独特类别的定制规范。我们可以从更多地使用这些测试中受益,用动态规范测试来替代许多 API 中使用的静态、易出错的类型测试。

例如,表视图不需要仅仅响应数据源选择器的某些东西;它需要的是像数据源一样行为的东西。所以,让我们创建一些任何数据源都应该满足的测试,并将它们捆绑成一个可以在运行时测试的规范。请注意,这些并不是真正的单元测试,因为我们不是在测试我们的数据源——我们在测试任何数据源。

表视图需要知道有多少行,以及每行的内容。因此,你可以看到,对表视图数据源的动态测试并不仅仅是单独测试这些方法;它还会测试数据源能否提供它所说的行数那么多值。你可以想象,在支持设计-by-contract 的语言中,例如 Eiffel,合作者的规范可以是类合同的一部分。

这些规范将在协作者提供时由对象进行测试,而不是等待在执行过程中出现失败。是的,这比通常在方法的前置条件中发生的易出错的类型层次或一致性测试要慢。不,这不是问题:我们希望在让它变得快速之前先让它变得正确。

将测试用例视为对象之间协作的规范,而不是(或除了)为特定类进行的单一测试,为开发者之间的协作开辟了新的途径。框架供应商可以提供作为增强文档的规范。框架消费者可以提供他们如何使用框架的规范作为错误报告或支持问题;供应商可以将这些规范添加到回归测试工具中。应用程序作者可以创建规范发送给承包商或供应商作为验收测试。供应商可以通过证明他们的代码是某些其他代码的“即插即用”替代品来展示他们的代码。

结对编程

在我的职业生涯中,我进行了很多结对编程,尽管它只占我时间的少数部分。我也观察过其他人进行结对编程;合作伙伴之间的互动可以是非常有趣的观看。

在深入探讨我认为的良好结对编程之前,我将描述一下不良结对编程的特点。

后排驾驶并非结对编程

因为我已经做了很长时间的 TDD,我习惯于故意让我的代码在足够好以集成之前经历一段无价值的阶段。也许我会等到看到它失败后再处理失败条件,或者最后再添加它。也许我想不出一个方法名,所以会先命名为DoTheThing(),直到我有一个更清晰的图像。

我必须记住的是,我的搭档可能不会以同样的方式工作。是的,看到未处理的条件或未按照我偏好的约定命名的变量很烦人,但那是不是现在最紧迫的问题?可能不是;司机目前正在处理的问题吸引了他们的注意力,谈论其他事情只是分心。我应该帮助他们工作,并在适当的时候提出其他问题。

这种问题的更极端形式:偷走键盘并不是结对编程。

作为一名沉默的搭档并不是结对编程

我最常看到这种情况发生的情况是,领航员(由于没有更好的、更通用的词来描述那个不驾驶的人——尽管不是“乘客”,很明显的原因)感到自己不如司机,或者被司机吓到了。他们害怕贡献,或者觉得自己不够资格贡献,因为他们不想显得愚蠢或害怕对他们的贡献的反应。

这个部分不是为在这种情况下开车的司机准备的——我稍后会提到;这是为领航员准备的。如果你看不出代码是如何做到它应该做到的,也许它根本就没有做到。如果你向你的搭档询问,可能会发生以下两种情况:

  • 你会发现一个错误,它将被修复

  • 那个错误根本不存在,但你将了解到代码是如何处理那个问题的

(技术上,还有第三种选择,即司机可能会告诉你闭嘴。到那时,你想要一本关于人力资源的书,而不是成为一名开发者。)

如果你没有询问,可能会发生以下两种情况:

  • 那个错误将存在,并且不会被修复

  • 那个错误根本不存在,你也不会了解到代码是如何工作的

简而言之,如果你不询问,错误更有可能留在代码中,所以你应该考虑这是你的职业责任去询问。

那么,结对编程仅仅是那些事物之间的平衡吗?

那是对事情的一种过于简化的看法,但是的。结对编程在两个人都参与时效果最好;否则,其中一个人可能是多余的,即使他们碰巧在充当打字员。如何良好地进行结对编程取决于你试图用它来做什么。

结对编程作为编程

我很少使用结对编程作为将代码放入应用程序的手段,我发现确保两个人都参与的简单规则是:直到两个人都同意,任何事情都不会发生。这允许司机调节副驾驶的行为:“那是个好点子,但让我们把它放在一边,直到我们完成这部分。”这也要求司机让沉默的搭档参与。

在进行配对编程时,我常常会犯的错误是掌舵:“让我给你展示一下我的意思。”双方的规则对我来说和其他人一样重要,因为它要求我找到比破坏合作关系更好的方式来描述我的想法。有一个白板真的很有帮助。

如果目标是编写生产代码,配对在两个技能水平大致相同的人之间进行时效果最好,他们可以轮流驾驶。当他们的能力之间存在不平衡时,它就会变成……

配对作为一种辅导实践

配对作为教学练习非常出色。关于在两人都同意之前不写代码的规则仍然适用,这确保了学生能够与导师讨论任何问题,导师有机会引导这个过程。

我认为当教练坐在导航员的位置上时,配对辅导效果最好。他们的角色是鼓励学生提问,然后成为那个用另一个问题回答每个问题的任性的幼儿。

严肃地说。我发现帮助某人通过问题的最好方法是识别并提出他们应该问自己的问题。这揭示了隐藏的假设,让人们提出口头论据来支持(有时引导他们改变)他们的立场,最终他们会尝试猜测接下来会问哪些问题,这意味着他们在被问之前就有了答案。这种技巧在你对辅导的主题一无所知时也非常有用——但到目前为止,我们假设教练程序员是更有经验的程序员。

当学生盯着 IDE 中的空白文件时,问题可以非常高级。我们即将编写的代码将接口于何处,它又强加什么约束?我们是否有选择我们使用的 API,如果有,我们应该选择哪一个?偶尔的“为什么?”有助于揭示学生的思维过程。

行动在学习过程中有它的位置,因此有时适当的回应不是一个问题,而是“好吧,让我们试试看。”即使你的学生还没有找到你认为最好的解决方案,开始尝试也是快速找出哪一方对将要发生的事情判断错误的好方法。

但它有效吗?

配对编程实际上有益吗?它在编程课程的环境中显然是有益的(dl.acm.org/citation.cfm?id=563353),其中配对程序员生产的软件比单独程序员更好,并且更有可能获得更高的分数(portal.acm.org/citation.cfm?doid=611892.612006)。这些结果是否可以推广到所有程序员是有疑问的;了解为什么这些主题在配对时表现更好,以及这些条件是否适用于更熟练的程序员将很有趣。

代码审查

另一件可以肯定的是,结对编程不是代码审查练习;它们有不同的目标。代码审查应该进行以讨论和改进现有代码。结对编程是关于两个人从头开始构建一些代码。如果你的结对编程是关于一个人编写代码,另一个人说他们做错了,那么你需要重新思考你的实践(或者你的合作关系)。

请注意,当代码审查进行得不顺利时,这也是真的。代码审查的一个问题是,发现满足“我不会那样写”的代码比发现满足“应该考虑到这些因素”的代码要容易得多。这往往阻碍了从代码审查中获得有用信息,因为审查者会对代码的制表符/空格/变量名/其他退化特性感到沮丧。

正是这类问题让我更倾向于异步、分布式的代码审查而不是面对面审查。我们经常看到,人们(programmers.stackexchange.com/questions/80469/how-to-stand-ground-when-colleagues-are-neglecting-the-process)不理解他们同事的动机(thedailywtf.com)。让审查者自己解决最初的挫折和愤怒——最好是,没有作者在场作为出气筒。审查者有机会冷静下来,熟悉要求,并详细研究代码……这在面对面的审查中并不成立,因为房间里还有其他人,等着得到第一块智慧之石。

关于面对面审查的话题,要警惕那些在这个领域引用“经典”的人。鼓吹代码审查好处的人经常会引用Fagan 关于代码检查的论文(ieeexplore.ieee.org/xpls/abs_all.jsp?arnumber=5388086),声称它表明在引入代码审查后,软件开发成本有所降低。好吧,确实如此。但并不是以任何你从现代软件开发中认识到的任何方式。

Fagan 小组进行的代码检查在很大程度上会揭露今天在编译代码之前现代 IDE 就会报告的问题。确实,Fagan 特别描述了在产品编写后、提交给编译器之前进行代码检查。回想一下你上一次在尝试构建之前完全编写应用程序的时候。对于今天的大多数开发者来说,这种情况从未发生过。

Fagan 的审查会在将一叠穿孔卡片提交到批量编译器之前发现诸如缺少分号或拼写错误等问题。这确实是在失去计算机时间和返工方面的一项宝贵节省。然而,对于现代代码审查来说,要具有价值,它必须在其他地方节省时间。应该鼓励审阅者关注更高层次的实际问题。代码是否代表了一个良好的抽象?是否有在其他地方重用其组件的机会?它是否准确地解决了当前的问题?

我发现对实现这一目标最有用的工具是清单。一组简短的、审阅者应关注的事项可以将审阅引导远离关于风格和命名实践的琐细问题。此外,它还指导作者在编写代码时思考这些问题,这应该会使实际的审阅过程本身相当简短。使用相同的清单几次之后,其有效性会降低,因为团队中的每个人都会对清单上出现的问题有一个共享的处理方法。因此,当旧项目变得不相关而其他问题的重要性增加时,清单上的项目应该进行交换。

通常,我正在工作的团队在将某个工作集成到发布版本时会进行代码审查。这比计划中的审查(代码很少经过充分准备,导致审阅者专注于已知的粗糙边缘)或请求审查(开发者根本不会请求)要好。GitHub 等工具通过“拉取请求”支持这一点——当作者想要将一些代码合并到上游分支或存储库时,他们会发送一个请求,这是一个进行审查的机会。其他工具,如gerrit (code.google.com/p/gerrit/),也提供了类似的功能。

理想情况下,代码审查应被视为学习活动。作者应该学习为什么审阅者建议特定的更改,存在什么问题,以及为什么提出的更改以代码提交到审查的方式解决了这些问题。审阅者也应该在学习:从提交的代码中学习的机会,通过提出令人信服的论据来支持为什么你的更改应该被接受,这些论据不是“因为我最懂”。为了使这起作用,代码审查的结果必须是一个讨论,即使它是一个审查工具中的评论线程。做一些额外的修复并接受没有讨论的修复更改会失去审查的大部分好处。

编程范式及其适用性

在一个(理论上正确,但实际并不令人愉快)层面上,所有软件都是由加载、存储、数学和跳转组成的,所以任何应用程序都可以使用任何允许正确排序这些基本操作的工具来编写。然而,贯穿这本书的一个关键主题是软件的人际性质,在这里,我们有一个具体的例子:应用程序源代码是程序员之间相互理解的来源。

在探索这个话题之前,我们先稍微回顾一下历史,以便明确这个想法,这样我们就可以将其放下。这是关于连续层抽象的想法,它允许人们在前人的基础上构建。是的,所有软件都是由上述基本操作构建的,但用计算机操作来思考你的问题是困难的。在存储程序计算机被发明后的几年里,EDSAC 程序员创建了一个汇编器,它将助记操作名称(如表示为A)转换成计算机使用的操作代码。然后程序员只需关注他们正在添加事物的事实,而不必关注处理器在这个特定的寻址模式中用哪个数字来表示加法(在有多于一种寻址模式的计算机上)。

其他工作,包括对宏汇编器的研究以及格蕾丝·霍珀在 A-1 和其他编译器上的工作,使得程序员能够从计算机操作(即使是“友好”的名称)中提升一个层次,并以一种可以被翻译成低级指令的方式表达他们想要发生的事情。例如,一个循环遍历某些代码,使用索引变量从 2 到 20 取偶数值,可以表示为FOR I=2 TO 20 STEP 2:…:NEXT I,而不是计算机实际需要执行的初始化、测试、分支和更新步骤。

因此,当某人第一次在软件中解决问题时,其他人(在合法性、兼容性和可用性允许的情况下)可以在该解决方案之上构建其他软件。这适用于以下讨论:对象可以由其他对象构建,函数可以由其他函数构建。函数也可以由对象构建,对象也可以由函数构建。这不是那个故事。这是关于故事由函数和对象构建的故事;关于将编程范式作为思考软件和向其他程序员描述关于软件思考的方式。

面向对象编程

当它在 20 世纪 80 年代中期成为一种流行的技术时,有些人试图将 OOP 定位为解决软件行业所有问题的方案(这些问题是否以描述的形式存在可能是一个需要另一次讨论的话题)。IBM 臭名昭著的 System/360 项目的管理者弗雷德·布鲁克斯曾告诉程序员,没有银弹——www.cs.nott.ac.uk/~cah/G51ISS/Documents/NoSilverBullet.html;软件行业面临的问题是困难的,没有任何技术解决方案会使它变得更容易。布拉德·考克斯反问,如果真的有银弹——dl.acm.org/citation.cfm?id=132388(即面向对象技术),而你的竞争对手已经在使用它?

就考克斯所看到的(或者至少在推广他的公司时定位它),面向对象编程是文化转变,它将软件构建从分离的独立手工艺品行业转变为真正的工程学科,通过引入对象作为具有标准接口的可互换组件,就像 pozidrive 螺丝或四乘二的板条。(软件-集成电路:考克斯使用的另一个隐喻,尤其是在他的书《面向对象编程:一种进化方法》——books.google.co.uk/books/about/Object_Oriented_Programming.html?id=deZQAAAAMAAJ&redir_esc=y中,是软件集成电路。正如计算机硬件的发展通过从组装离散组件的计算机到连接标准集成电路而加速一样,他设想了一种软件摩尔定律,这种定律源于从标准对象或软件集成电路组装的应用程序的连续发展。)

软件制造公司可以构建这些标准组件,并将它们提供给对象市场。这将相当于软件行业的贸易商店,蓝领工匠和 DIY 计算机用户可以从货架上购买对象,并将它们组装成他们需要的应用程序。

事实上,布鲁克斯已经指出,与软件开发相关的问题分为两类:一类是源于其作为复杂活动的本质问题,另一类是与当前流程或技术及其缺陷相关的偶然问题。面向对象编程没有解决本质问题,而是用其他偶然问题取代了一些问题。

总之,所有这些历史可能有些兴趣,但什么是面向对象编程呢?我们需要关注的问题不是操纵数据或指导计算机,而是如何组织这些数据和指令以帮助(人类)理解。

面向对象软件区别于其他技术的特性是其代码及其作用的数据以相互关联的组织形式构成自主单元(即所谓的对象),通过发送消息相互交互。支持这种方法的论点是,一个在这样一个单元上工作的程序员只需要理解其协作单元的接口——他们理解的消息以及这些消息的前提条件和结果;而不需要理解实现——这些单元是如何工作的。因此,一个由许多指令组成的大型程序被分割成多个独立的实体,这些实体可以独立开发。

许多在面向对象编程之前就已经存在的编程语言已经允许将代码组织成模块,每个模块都有自己的功能和数据。这些模块可以限制只通过特定的接口函数相互通信。面向对象编程在此基础上引入了自动机的概念,即代码和数据自包含的包,它既独立于软件系统中的无关部分,也独立于其他类似实例。因此,虽然用 Modula-2 编写的多人游戏可能有一个控制玩家角色并隐藏其细节的模块,但如果用像 Oberon-2 这样的面向对象语言编写,它可能有一个代表每个玩家角色的对象,该对象隐藏其内部细节,既隐藏于游戏的其他部分,也隐藏于其他玩家对象。

考虑到这种希望构建一个通过消息(cpp-messages)进行通信的自主代理系统,一些读者可能会对“面向对象编程涉及消息发送”的说法表示异议,他们认为使用 C++等语言(其成员函数作为反例)并不涉及消息发送。但可以简单地说,对象之间发送消息的心智模型仍然是有用的,尽管实际的语言实现可能不同。现在,一些其他读者可能会引用艾伦·凯的言论来断言,只有具有消息发送功能的语言才能被认为是面向对象的。(如果你挖掘得足够深,你会发现,在 Smalltalk 中,“面向对象”有时被用来指代内存管理范式;换句话说,就是垃圾回收器。编程模型被称为“消息传递”。因此,也许带有 Boehm-Demers-Weiser 垃圾回收器的 C++在纯粹主义者看来确实面向对象的。不管怎样。如果你对此有异议,请找其他人发邮件。)最大的问题(如果不是唯一的问题;采用面向对象编程引入的唯一问题是选择哪些对象负责哪些动作。这是一个难以解决的问题;我记得我在创建第一个面向对象系统时犯了很多错误,直到近十年后才想要改进。所有领域的程序员都写过关于将系统分解为组件对象的经验法则,有些人还开发了评估软件与这些经验法则相关性的工具,以及自动改变组成的工具。

这些启发式方法从模糊的概念(如开闭原则、单一职责原则等)到精确定义的数学规则(如 Liskov 替换原则、迪米特法则等)不等。大多数(或许全部)这些方法都有一个高级目标,那就是提高系统中对象的自主性,减少它们对系统其他部分的依赖程度。这样做的好处是:提高了对象在不同系统中的可重用性,以及减少了某个特定对象因系统其他部分的变动而需要修改的可能性。

研究人员还发现,面向对象软件比结构化软件更难进行审查——dl.acm.org/citation.cfm?id=337343。导致紧密耦合对象连接系统的理想设计属性也产生了一个难以发现执行流程的系统;你不能轻易地看到任何特定消息的结果控制流向何方。确实存在一些工具旨在通过提供面向对象系统的多个相关视图来解决这个问题,例如 Code Bubbles 和 Eclipse Mylyn。这些工具目前还不是主流。当然,还有那些以 UML 等符号图示化方式描述面向对象软件的文档,这些文档的价值在第八章,文档中有所描述。

我发现,关于面向对象编程最有趣的阅读材料是它在新兴时期所写的;至少对于商业程序员来说是新的。这些材料试图说服你面向对象编程的好处,并解释这种范式的推理。在接下来的几十年中,具体的实践发生了显著变化,但现代书籍假设你知道为什么想要做面向对象编程,甚至常常假设你知道它是什么。

我建议即使是自认为经验丰富的面向对象程序员也应该阅读《面向对象编程:一种进化方法》(books.google.co.uk/books/about/Object_oriented_programming.html?id=U8AgAQAAIAAJ&redir_esc=y)和《面向对象软件构造》(books.google.co.uk/books?id=v1YZAQAAIAAJ&source=gbs_similarbooks)。这些书籍不仅告诉你关于特定语言(分别是 Objective-C 和 Eiffel)的信息,还介绍了这些语言旨在解决的问题。

从这些以及其他领域的基石性文本中,你可能学到的就是,面向对象编程(OOP)没有成功的原因并不是它失败了,而是它从未被尝试过。为了使 OOP 易于理解,面向对象技术公司明确指出,你已经在做的事情其实已经是面向对象编程了。如果你知道如何在 C 语言中编写顺序语句,你会喜欢在 Java 中编写顺序语句,然后你就在做面向对象编程了。

面向方面编程

面向对象编程的一个扩展,到目前为止还没有达到相同的应用和货币水平,即面向方面编程,旨在解决面向对象系统构建中的特定问题。更具体地说,这个问题存在于具有单一继承的单继承类面向对象系统中。

上一节描述了许多启发式方法的存在,这些方法是为了指导基于对象系统的代码组织。其中一种启发式方法是单一职责原则,它表示一个类中的代码应该只负责一件事。想象一下,然后是一个人力资源部门的数据库应用程序(如果忽略食谱管理器,几乎是面向对象示例的典范)。一个类可能代表一个员工,具有姓名、薪水、经理等属性。不应让每个人都能够更改员工的薪水,因此需要一些访问控制。为了审计和调试目的,能够记录员工薪水的任何更改也可能很有用。

因此有三个职责:更新数据库、访问控制和审计。单一职责原则意味着我们应该避免将所有职责放入Employee类中。实际上,这会导致大量重复,因为访问控制和审计功能也需要在应用程序的其他地方使用。它们是横切关注点,许多不同类必须提供相同的设施。

虽然有其他方法可以将这些横切关注点构建到应用程序中,但面向方面编程在面向对象系统中打开了可配置的连接点。这些连接点包括方法进入或退出、执行转移到异常处理器以及字段被读取或更改。方面定义了连接点必须满足的谓词,以便此方面相关(称为切入点)以及在该连接点运行的代码(有时称为建议)。

面向方面编程扩展适用于流行的面向对象环境(AspectJ (www.eclipse.org/aspectj/) 用于 Java 和 Aspect# (sourceforge.net/projects/aspectsharp/) 用于.NET)),但如前所述,这种风格并不广泛使用。它进一步加剧了面向对象已经存在的问题,即很难确定在给定事件响应中确切执行了哪些代码。其他系统,如 Ruby 和 Self(以及 C++),有“特性”或“混入”,它们占据着方面的位置,但不是名称。

函数式编程

比面向对象编程还要不那么新颖——尽管需要一点重新发现——的是函数式编程。正如其名所示,函数式编程完全是关于函数的;在这种情况下,是在数学意义上可以应用于某个输入域并产生相应范围输出的操作。而面向对象系统描述了计算机必须执行的命令式命令,函数式程序描述了应用于给定输入的函数。

这种区别导致了一些从命令式系统(尽管这些偏离可以在面向对象代码中建模,但在函数式编程中很普遍)中出现的有趣差异。函数式系统的一部分可以惰性评估;换句话说,计算机看到需要 x 的平方结果,可以推迟计算该结果,直到它实际上被使用,或者 CPU 处于空闲状态。这在对计算平方来说并不那么有趣,但可以导致像处理所有整数的列表这样的技巧。在命令式代码中,所有整数的列表在创建时就需要计算,这是不可能做到的。函数式软件可以定义一个评估为所有整数列表的东西,然后只对实际访问的条目进行惰性评估。

类似地,结果可以被记忆化:当 x 等于 2 时,x 乘以 x 的结果总是 4;我们知道它不依赖于任何其他东西,例如数据库的状态或用户在键盘上按下的键,所以一旦计算出 2 乘以 2 等于 4,我们就可以始终记住它并再次使用答案 4。

递归是函数程序中经常使用的武器。我们如何构建所有整数的列表?让我们限制自己为正整数的列表。定义 f(x) 函数,使得:

  • 如果 x 是列表 l 的头部,f(x)=1

  • 否则,f(x)=1+f(previous entry)

那么,对于一个只有一个条目的列表,应用 f 的结果是 1。对于两个条目的列表,它变成 1+f(single-entry)=2,依此类推。

递归和惰性评估都是有用的特性,但它们都不是函数式编程风格的固有特性;它们只是经常被用于这些领域。程序作为函数模型的一个更本质的部分是副作用的缺失。

因为数学函数没有副作用,函数的输出只取决于其输入。函数式编程的倡导者说,这使得软件更容易理解(不会发生任何“魔法”),并且它为构建多线程软件提供了一个良好的方法,因为不可能出现竞态条件;如果函数的输入可以准备,函数就可以产生其输出。如果一个函数与一个数字作为输入工作得很好,它将以相同的方式与另一个函数的(数值)输出作为输入工作;它的执行只取决于它接收到的。

当然,许多软件系统都有产生副作用的要求,例如在显示屏上绘制图像或修改数据库的保存状态。不同的函数式编程语言随后提供了不同的技术来封装——而不是完全删除——可变状态。例如,用 Haskell 编写的软件系统的有状态组件将被表示为函数的结果,并且可以自身执行以产生所需副作用;这样,有状态的部分可以充当函数程序的汇或源。

函数式编程在过去的几年里在商业领域获得了极大的流行,这主要得益于功能语言与现有(面向对象)代码的接口;例如,Clojure (clojure.org) 是在 Java 虚拟机 (JVM) 上运行的,而 F# (fsharp.org/learn.html) 是在 .Net 虚拟机上的。尽管如此,其原则却要古老得多——LISP 是在 1958 年首次被描述的——www-formal.stanford.edu/jmc/recursive.html,但这些原则是基于早于可编程计算机的数学概念——www.jstor.org/stable/1968337。关于函数式编程的如何和为什么,一本很好的参考书是 《计算机程序的构造和解释》 (web.mit.edu/alexmv/6.037/sicp.pdf),尽管这本书的最近版本使用了乍一看不是功能语言的 Python。

第七章:第六章

测试

简介

我在 IT 行业的第一份工作是在软件测试领域。我发现开发人员和测试人员有各自独立的社区,拥有各自的技术和知识体系。我还发现,在一些公司中,开发人员与测试人员之间存在对抗性的关系:开发人员对测试人员因吹毛求疵而感到不满,因为测试人员喜欢挑剔他们的辛勤工作。作为回报,测试人员对开发人员草率和不一致的编写和发布软件的方式感到不满。当然,这两种极端立场实际上并没有建立在现实基础上。

本章阐述了一种思考制作软件的方法,将开发人员和测试人员置于相同的位置:都希望制作一个有价值的产品。然后介绍了软件测试领域的系统化软件测试,这是软件测试人员所理解的,而开发人员似乎很少关注。

测试哲学

想象一下在多维图表上绘制你软件的各个维度:功能、性能、用户界面等等(在本节的图表中,我将坚持使用两个维度;即使你是在一些疯狂的未来阅读器上查看,我的图形工具也不支持超过这个数量)。

首先,要注意的是,你无法在图 6.1上绘制一个代表“目标”产品的点。最重要的原因是目标可能不存在。根据你对软件的哲学方法,可能没有一个真正的需求集合被普遍理解为要构建的正确事物。考虑使用软件作为支持系统的组成部分的人,因此“正确的事情”取决于这些人以及他们之间的互动。你应该构建的东西取决于上下文,并随时间变化。(Manny Lehman 对这种哲学有更完整的描述,其中他将嵌入现实世界互动和过程中的软件系统描述为“E 型”系统(E 代表演变)。在探索 E 型系统的属性时,他提出了八个软件演变定律——en.wikipedia.org/wiki/Lehman's_laws_of_software_evolution。我发现这些定律被描述为自然固有的法则,这是讽刺的,因为教训是,在软件方面没有普遍的真理。)

你可以绘制许多模糊的块来表示对软件的各种看法:客户认为它做什么,客户认为它应该做什么,以及项目团队各个成员认为它做什么。然后还有一个块,代表软件实际上做什么。

图片

图 6.1:软件行为维恩图

软件系统的行为以及不同人对该行为或其应该是什么的看法是可能行为空间中的不同区域。软件测试就是识别这些差异,以便它们可以被调和。

构成软件测试的各种实践可以看作是记录这些认知和它们之间差距的一部分。然后,调和这些不同认知和关闭差距的努力不仅仅是调试工作,这意味着测试人员会发现开发者遗漏的问题。这是一个团队的努力,其中调试只是调和活动之一。营销(改变客户的认知以匹配软件的能力)、额外的销售工程(改变部署环境以匹配软件预期的环境)和其他技术都是关闭这些差距的方法。

以这种心态,测试人员并不是在努力“展示”开发者;每个人都致力于创建一个有价值的软件系统,以及对该系统功能的共同理解。测试的目标是识别项目团队可以利用的机会

黑盒与白盒

我发现,让开发者感到愤怒的一件事是,当问题报告是从黑盒视角编写的时——测试人员报告了一个没有其他信息的错误,除了通过用户界面可以发现的: “我试了,但不起作用。”我知道这很令人愤怒,因为我曾经是这类报告的接收者。

然而,从上一节概述的视角来看,黑盒测试报告是最有价值的报告。(在这里,“黑盒”指的是测试报告的格式,其中描述了软件的输入以及预期输出和实际输出之间的差异。在测试规划中,测试人员使用“黑盒”和“白盒”这些短语来指代是否在测试设计中使用了软件的源代码;这类测试仍然很可能是通过软件的接口来执行的。)任何通过用户界面无法按预期工作的情况都代表了一个被描述的差距:客户对软件功能的认知与软件实际展示的能力之间的差距。

收到这类报告常常令人沮丧的原因在于,重现报告中的问题以及隔离原因可能极其困难且耗时。通常,这个过程比找到问题后修复问题所需的时间更长;为什么测试人员不使用白盒技术,利用对软件的内部知识来单独测试组件,直接找到错误所在,而要给你增加这么多额外的工作?

这是那些感知差距的另一个例子。因为我们把所有的时间都花在与将指令组合成大约 10 个序列的方法和函数打交道,程序员对系统的自然看法就是那些指令和方法。黑盒问题报告与古老的黑盒谜题游戏非常相似,你在一边照光,可以看到光被吸收或反射。你想要思考镜子和盒子内部的其它特征,但你被迫从光束发生的情况中推断它们。

同时,测试人员是在代表客户行事,因此他们对系统的核心部分没有情感上的依恋。客户会想“我遇到这个问题,我相信如果我能做这样,软件就能帮助我解决这个问题”——这是一种自然的黑盒视角,只与软件的外部接口进行交互。换句话说,他们(以及代表他们的测试人员)对特定方法在参数为3时返回truefalse没有意见;他们关心的是软件的输出是否是作为其输入表达的问题的有用解决方案。记住,测试人员试图找出预期行为和实际行为之间的差异;发现这些差异的原因只有在团队决定代码修复是合适的时候才需要做。

阐明黑盒之谜

因此,如果定位和诊断代码问题的努力只有在决定代码必须修复时才需要,那么从黑盒问题定义到根本原因的转换是由程序员而不是测试人员来完成的。不管你喜不喜欢,隔离错误是开发者的责任——无论测试人员是否能够提供帮助。

显然,通过在问题报告中重现步骤,逐步在调试器中从开始到结束执行代码,直到问题出现,是有可能隔离错误的。但这既不快,也不愉快。如果你能够假设可能的原因并迅速证明这个假设是否有效,那么诊断问题会快得多。

这就是组件和集成测试变得有用的地方,但它是更大图景的一部分:了解(或能够找出)构成整个系统的各个模块成功工作的条件,以及这些条件是否满足每个参与有缺陷行为的模块。

构建这些假设的帮助可以来自软件的行为。在问题诊断中常用的一个工具是可配置的日志输出级别:消息被标记为不同严重程度,用户选择记录在日志中的级别。在重现一个错误时,日志被设置为显示所有内容,从而更清晰地查看代码的流程。这种方法的缺点取决于具体的应用程序,但可能包括来自软件无关部分的噪声,以及如果问题是与时间相关的话,对整体行为的改变。

问题诊断也受益于拥有对应用程序的可脚本化接口;例如,命令行或 AppleScript 接口。第一个好处是它为你提供了第二个 UI 来访问相同的功能,这使得快速确定问题是在 UI 还是应用程序逻辑中成为可能。其次,它为你提供了一个可重复和可存储的测试,可以添加到回归测试套件中。最后,这样的接口通常比 GUI 简单得多,因此只有与问题相关的代码被测试,这使得隔离任务更快。

否则,从可观察的行为到可能的原因在很大程度上仍然是一个直觉和系统特定知识的问题。了解哪些模块负责应用程序外部行为的哪些部分(或能够找出——见第八章,文档)以及推理哪个最有可能导致问题,可以大大减少调试时间。因此,我更喜欢按照这些线路组织我的项目,使得所有进入一个功能的代码都在一个组或文件夹中,并且只有在与其他功能共享时才会拆分到另一个文件夹中。Eclipse 的 Mylyn 任务管理器——eclipse.org/mylyn/start/提供了一种更丰富的方式来提供项目的问题特定视图。

测试用例设计

随机、无向测试(也称为与用户界面玩耍)是测试软件的低效方法。一种长期确立的技术(在迈耶的《软件测试的艺术》中有所记载——books.google.co.uk/books/about/The_art_of_software_testing.html?id=86rz6UExDEEC&redir_esc=y)试图用最少的测试覆盖所有可能的情况。对于每个输入变量或状态,测试人员会发现代表软件中不同条件的值域。例如,一个年龄字段可能具有以下值域:

  • [0,18[ : 儿童

  • [18, 150[ : 成人

  • 0[ : 太小

  • [150 : 太大

  • NaN : 非数字

然后,测试员将这些各种范围的所有输入进行表格化,并创建出执行所有这些输入所需的最小测试数量。这被称为等价类划分:36 岁和 38 岁的行为可能相同,因此可以合理地预期,如果你测试其中一个,不测试另一个的残余风险很小——具体来说,小于进行那个测试的成本。

事实上,测试员不会完全产生最小数量的测试;他们可能会选择额外关注边界值(也许会编写使用 17 岁、18 岁和 19 岁的测试)。边界很可能是含糊的丰富来源:是否每个人都理解“不超过 18 岁”和“超过 18 岁”意味着相同的事情?软件是否使用适合年龄的舍入方案?

这种技术最初是基于这样的假设,即软件系统的“真实”行为可以在其功能规范中找到;所有测试都可以通过将上述分析应用于功能规范来推导出来;并且观察到的行为与规范之间的任何差异都是一个错误。根据本章开头描述的测试哲学,这些假设是不成立的:即使存在功能规范,它也和其他任何对软件系统的描述一样,是不完整和含糊的。这里描述的技术仍然是有用的,因为挖掘这些含糊和误解是测试员为项目带来的价值的一部分。这只意味着他们的角色已经从验证扩展到包括成为一个(口头)语言律师。

代码导向测试

记住“白盒测试”这个短语具有上下文意义,我选择将其称为代码导向测试。这意味着测试是设计时参考了应用程序的源代码,无论它们是如何运行的。

当测试员设计这些测试时,他们通常有两个目标之一:要么确保 100%的语句覆盖率,要么确保 100%的分支覆盖率。最大化分支覆盖率会产生更多的测试。考虑这个函数:

    void f(int x)
    {
        if (x>3)
        {
            // do some work...
        }
    }

一个想要执行每个语句的测试员只需要测试x大于 3 的情况;一个想要执行每个分支的测试员还需要考虑另一种情况(而且一个勤奋的测试员会试图发现当x等于3 时人们认为会发生什么)。

因为测试是从源代码派生出来的,而源代码按定义是一种适合软件工具操作的格式,所以工具支持非常适合代码导向的测试设计。许多平台都有用于测量和报告代码覆盖率的工具。甚至还有自动测试用例生成器,可以确保 100%的分支覆盖率;一个很好的例子是Klee——klee.llvm.org/,符号虚拟机。

非功能需求测试

原则上,测试系统的非功能性属性应该和测试其功能性行为一样。你发现系统做了什么,各方认为它应该做什么,然后进行比较。在实践中,非功能性需求可能是隐含的(有人可能希望系统以某种特定方式工作,但他们不知道如何表达,或者认为这太明显而不需要明确说明)或者用含糊不清的术语定义(“系统必须快速”)。

解决这些问题的第一步是让它们进入讨论,因此测试软件的这些方面并报告结果是一个好主意。例如,客户可能没有表达任何系统需求,因为他们不知道这很重要;一份报告说“应用程序在 32 位系统上无法正常运行,需要至少服务包 2”将揭示这是否是一个问题,从而更好地相互理解系统。

自动化一切

测试软件和编写软件有一个共同的特性:不是“做”它们是有益的,而是“完成”它们。能够访问到完成并工作的软件是有用的,因此一个正在进行的项目只和尚未开始的项目一样有价值(尽管正在进行的项目已经花费了更多)。因此,应该尽可能地自动化测试过程本身,以便测试人员能够专注于定义测试和发现/报告问题的更具创造性的任务。

这种自动化从设置测试环境到一个已知的初始状态开始。虚拟机越来越多地被用于这项任务(至少在服务器和桌面环境中),因为它们提供了一种快速创建已知配置环境的方法,测试工具和被测试的软件可以部署到这个环境中。测试运行结束后,虚拟机的状态会被重置,并准备好再次启动。

自动测试软件的驾驶可以通过专门的脚本接口来完成,正如之前所描述的,但这些接口并不能测试 UI 按钮和组件的行为。开发者通常不喜欢自动 GUI 驾驶测试,因为测试失败的可能性有限,这可能是由于 GUI 的一些不重要的属性改变,例如控制的位置或设计。这里有两个需要注意的地方:

  • 控制的位置和设计是重要的;如果测试驱动程序在软件的两个版本之间找不到相同的控制,那么客户可能也无法找到。

  • 尽管这种测试因无关紧要的更改而失败的风险存在,但如果完全取消测试,那么你可能会在发布 GUI 时遗漏未检测到的问题。这些相互冲突的风险必须得到解决。测试失败场景的影响是,在这些情况下,当 GUI 更新时,测试套件将出现一段时间的假阴性结果,直到有人意识到发生了什么并花时间更新测试。GUI 损坏场景的影响是,你的软件肯定不会按照客户期望的方式运行,这会导致客户不满、低评分,可能还会造成收入损失,并且有人将不得不花时间发布软件的修复版本。第二个场景似乎比第一个场景更不可取,因此接受保持测试更新的成本是更好的选择。

自动化特别有助于你有一个“烟雾测试”程序来确定构建是否足够稳定,可以接受进一步的测试或被视为发布候选。通过烟雾测试套件几乎可以定义为重复的苦差事,所以把它交给计算机来做。然后,开发者可以回到规划和工作于下一个构建,测试人员可以专注于提供有价值的测试。此外,自动烟雾测试套件将比手动烟雾测试更快,因此构建可以接受更大的严格性。你甚至可以将所有自动测试添加到烟雾测试电池中,这样每个构建都包含相对于先前构建没有已知的回归。

一些团队允许构建在通过自动测试后自动部署——github.com/blog/1241-deploying-at-github

引入他人

大多数关于测试的文献都提到,外部测试人员对被测试软件的情感依恋不如开发者,在评估该软件时将更加冷静,因此会发现更多问题。事实上,开发者可以系统地测试自己的软件,但往往缺乏这种倾向(尤其是我们倾向于将编写代码视为我们做的有价值的事情,而其他一切都是开销)。获取某种形式的外部输入,无论是第三方测试人员还是顾问来检查我们的测试是否覆盖了相关案例,是对我们工作的宝贵检查。

注意,beta 测试者不太可能提供这样的系统审查。通常,beta 测试者是一个感兴趣的用戶,在软件仍在开发过程中,可以免费获得软件的访问权限。他们可能会以随机的方式接近测试,并且只使用对他们感兴趣的软件部分。beta 测试对于发现你认为软件将被如何使用和预期它将被如何使用之间的差距是有用的,但在分析 beta 测试者的报告时必须使用统计技术。由于“客户总是对的”这种说法,改变一个 beta 测试者报告的问题的诱惑很大,但请记住,其他n-1个测试者并没有报告相同的问题,而且没有一个测试了替代方案。

在我参与的一个项目中,我们所说的“beta 测试”实际上是指客户环境测试。我们向客户提供了软件,希望他们的配置与我们的不同,并可能揭示出特定配置的问题。作为大型企业,这些客户并没有在他们的真实网络中测试 beta 版本,而是在专门为测试而设置的“不同”环境中进行测试。因此,团队仍然不知道软件是否能在客户的配置中运行。

当测试程序需要专业知识时,获取外部参与也是有用的。安全测试、性能测试和测试软件的本地化版本都是这种情况。

测试的其他好处

我在本章中已经表明,软件测试在识别软件的实际行为、明显行为和预期行为之间的差距方面发挥着重要作用。此外,我还描述了使用自动化测试作为回归套件的好处,这样一旦一个问题被修复,如果它意外地再次引入,就会被检测到。投资测试你的软件也会带来其他好处。

可访问性

传统上,在软件界,可访问性(或 a11y,在省略了 11 个字母之后)指的是使软件界面可供某些残疾或障碍人士使用。通常,它被狭义地应用于仅考虑视觉障碍者的考虑。

事实上,一个自动化的用户界面测试套件可以提高应用程序的可访问性。一些 UI 测试框架(包括苹果的 UI 自动化developer.apple.com/library/ios/#documentation/DeveloperTools/Reference/UIAutomationRef/_index.html微软的 UI 自动化msdn.microsoft.com/en-us/library/ms747327.aspx)使用为屏幕阅读器和其它辅助设备提供的元数据来查找和操作应用程序显示上的控件。在这个层面进行测试确保测试仍然可以找到标签已更改或已在屏幕上移动的控件,这对于基于图像检测的测试框架来说很难应对。

一些在其他无障碍性(a11y)方面有困难为他们的产品辩护的开发者发现,测试是一个方便的工具来做到这一点。根据我的经验,首先采取的是道德方法(“这是正确的事情”),然后是法律方法(“我们是否受残疾歧视法约束?”),然后是财务方法(“我们会吸引更多客户——竞争对手可能没有销售给他们的客户”)。即使是无障碍软件的积极倡导者(mattgemmell.com/2010/12/19/accessibility-for-iphone-and-ipad-apps/)也承认财务理由是动摇的:“我不会试图提出一个令人信服的商业论点来支持无障碍性;我甚至不确定我能否做到”——mattgemmell.com/2012/10/26/ios-accessibility-heroes-and-villains/)。管理者往往喜欢降低成本和风险:自动化用户界面测试,然后将其作为回归测试的一部分来维护,可以提供这两项降低。

结构

从单元测试到系统测试,无论你的测试在哪个层面进行,被测试的对象必须可以从你的应用程序中提取出来,以便在测试环境中执行。这一要求强制执行了关注点的分离:在每一个层面,模块必须能够在隔离状态下或用外部依赖项替换的情况下运行。这也强烈暗示了每个模块的单个责任:如果你想找到日志功能的测试,在“日志测试”固定装置中查找比在“摊销计算(顺便提一下,也做日志)固定装置”中查找要容易得多。

诚然,这种严格的关注点分离并不总是合适的解决方案,但通常情况下是这样的,直到你发现并非如此。这将简化开发中的许多方面:尤其是将工作分配给不同开发者。如果每个问题都在一个完全独立的模块中解决,那么不同的程序员只需就这些模块之间的接口达成一致,就可以根据他们的需求构建内部结构。如果他们需要以后出于某种原因将它们结合起来,那么你作为单独的独立组件进行测试的事实,即使你不得不移除一些回归测试以使一切正常工作,也会增加它们集成的信心。

我主要在性能优化中看到这种情况。当我正在为某个特定功能编写可视化时,另一位开发者编写了功能。这些部分各自独立工作,但接口使它们变得太慢。我们决定将它们结合起来,这使得它们变得快速,但引入了模块之间的紧密依赖。某些以前可以独立测试的事情现在需要其他部分的存在;但我们确实在独立测试了它们,因此对它们的工作方式和假设有一些了解。

第八章:第七章

架构

引言

“软件架构师”这个术语最近变得有些声名狼藉,可能是因为开发者与那些通过 PowerPoint-Driven Development 进行沟通的架构宇航员——www.joelonsoftware.com/items/2005/10/21.html合作的结果。西蒙·布朗(Simon Brown)写了一本名为《软件开发者的软件架构》的书——leanpub.com/software-architecture-for-developers;您可以查看这本书,以获得对软件架构师职责的全面讨论。本章的重点是思考问题作为代码和作为支持代码的架构之间的增量差异。它还涉及到在设计应用程序时需要考虑的一些事情,何时考虑它们,以及如何将这些考虑的结果传达给团队中的其他人。

非功能性需求至关重要

我几乎可以说,一个应用程序架构成功的主要指标是它是否支持客户描述的非功能性需求。任何人只要足够有耐心和固执,都可以随意将功能粘合在一起,直到所有必需的功能都出现。然而,以结合客户侧(NFRs)和开发侧(适应性、可读性等)所需属性的方式使它具有连贯性,这正是软件架构的艺术所在。

那么,这些非功能性需求是什么?通常说,这些是关于软件的“-ility”声明。需要稍微眯起眼睛才能接受这一点,但大体上是正确的:

  • 性能:这算是什么“-ility”?是速度性?速度性?还是速度性?无论如何,重要的是要理解“性能”的含义,因为它有许多不同的方面。它可能指的是软件在资源受限或大数据集下的行为。如果我们谈论“速度”,那可能是关于处理请求的速率,或者处理单个请求的时间(根据哪个更重要,以墙时或时钟周期来衡量)。它可能是平均值,或者在峰值条件下。如果是平均值,是在多长时间内测量的?它是均值还是另一个平均值?或许是中位数

    我在一个项目中处理过性能需求,描述如下:完成某些操作所需的时间和内存应在软件前一个版本的 105%以内。这是容易衡量的,软件是否成功是明确的。

  • 兼容性:软件需要在哪些操作系统上运行?哪些版本?它将与其他哪些软件组件进行通信?是否有选择特定语言、环境或第三方组件的理由?

  • 可靠性:当出现问题时会发生什么?是失败、恢复还是某种受限的操作模式?可以接受多长时间的中断,在多长时间内?或者可能有关于同时受影响的用户数量的限制?

  • 法律或监管要求:这些可以是要求不做什么(例如,不要将客户数据提供给第三方)或强制软件必须做什么(例如,记录任何数据请求)的规定。

  • 安全性:这是一个非常广泛的话题,以至于许多书籍都涉及,包括我自己的其中一本。现在,我确信安全专家会对我将安全性与其他 NFRs(非功能性需求)归为一类而感到恼火,但这就是它的本质。对于大多数软件来说,安全性不是客户想要的特定功能,而是他们希望功能以何种方式交付的特性。请注意,虽然安全性与合规性等其他要求没有直接关系,但它可以是确保其他要求在面临颠覆时仍然得到满足的前提条件。

  • 可用性:这可以涵盖广泛的需求:易用性,显然;但也包括应该支持哪些(人类)语言、可访问性、设计美学等等。我提到了可用性,但由谁来使用它?当然是使用它的人;但还有其他人需要考虑吗?谁将部署、安装、测试、配置和支持软件?这些人有什么可用性需求?

  • 适应性:软件支持的执行环境或(人类)系统中最可能发生的变化有哪些?当然,现在没有必要支持这些事物,但一个能够使这些更改更容易进行(当然,现在不会造成不可接受的成本)的架构可能是有益的。

有这样一个列表,我们可以给出一个不那么模糊的非功能性需求的定义:它们是产品需要在其中提供其功能性的约束——不是它所做的事情,而是它必须以何种方式完成这些事情。

正因如此,一个成功的架构必须支持满足非功能性需求。如果软件不保持在其操作的限制范围内,客户可能根本无法使用它;在这种情况下,软件将是一个失败。为了支持这些需求,软件架构需要提供一个连贯的、高级的结构,开发者可以在其中构建应用程序的功能。架构应该清楚地说明每个功能应该如何适应,以及每个组件实施中受到的限制。换句话说,架构应该指导开发者,使得功能的明显实现方式符合 NFRs。理想情况下,每当开发者有类似“我在哪里添加这个?”或“我应该如何进行这个更改?”的问题时,架构师(甚至架构)应该已经有了答案。

我应该在什么时候考虑 NFRs?

上述讨论可能让你觉得你需要在构建任何功能之前就确定架构,因为功能实现必须受到架构的限制。这在很大程度上是正确的,尽管你经常会发现,应用程序功能的需求会反馈到架构决策中。

我认为这种迭代最好通过一系列逐级提高保真度的原型来处理。(“保真度”在这里指的是原型的技术准确性和功能完整性;毕竟,这些是为了架构评估。我说的不是原型的 UI 测试适用性,这是一个完全独立的问题——dl.acm.org/citation.cfm?id=223514。)架构大致定义,一些功能大致实现;任何确定的问题都得到解决,设计略有改进。这个过程一直持续到一切稳定下来,这时产品就准备好发货了。

第十三章,团队合作中有一个关于项目方法的讨论。那些读过它或类似讨论的人会意识到,这听起来与螺旋软件开发模型有些相似——dl.acm.org/citation.cfm?doid=12944.12948,1986 年由波伊姆提出。这个提议与我在实践中分阶段原型化的不同之处在于每个迭代的长度:几天或几周,而不是波伊姆考虑的几个月到几年。

相信“先做出来再扔掉”这一理念的人此时可能已经从地板上抬起惊愕的嘴巴。这个理念的问题实际上在于如何真正地扔掉那个“扔掉的”东西。你打算扔掉第一个版本,但不知为何它竟然留了下来并最终投入了生产。你不妨从一开始就接受这种情况的发生,并编写一个尚未准备好但将来会准备好的原型,同时提供帮助团队理解原型与生产就绪之间差距的文档。

低保真原型中的性能

测量应用程序性能的工具是开发者可用的一些最强大的工具之一。时间分析器、内存管理器、网络数据包检查器等工具都有助于你发现应用程序的性能特征。但在它尚未编写出来时,你该如何做呢?

你编写具有预期性能特征的模拟。例如,如果你估计一个通过网络请求的操作大约需要 0.1±0.01 秒来完成,大约使用 4 MB 的堆内存,你可以编写一个模拟,分配大约 4 MB 的内存然后休眠适当的时间。应用程序的架构一次能支持多少这样的请求?完成任何单个操作的延迟是否可接受?记住在测试时考虑正常和饱和情况——queue.acm.org/detail.cfm?id=2413037

这种模拟形式对许多开发者来说并不陌生。正如模拟对象是为了在集成两个模块时测试功能而设计的模拟,这些模拟是性能的等效物。

低保真原型中的安全

将安全模型适配到现有架构可能会非常棘手。在数据流设计时未考虑这些方面的情况下,找到所有需要访问控制(这是面向方面编程的关键用例;可以在应用程序代码的“连接点”插入访问控制)或应该检查不同滥用情况的数据点是很困难的。对于关键的安全问题,包括访问控制和数据保护,最好从一开始就在设计中融入它们。

这并不一定意味着完全完善他们的实现;这只是意味着确保即使是早期原型也具备(即使是原型性的)保护能力。例如,在一个我参与的项目中,我们知道该应用程序需要加密写入磁盘的文档。该应用的早期版本使用凯撒密码en.wikipedia.org/wiki/Caesar_cipher来执行此操作——远非密码学上安全,但足以显示哪些文件受到保护,以及是否有其他途径写入。你可以想象通过确保即使占位符功能也不能被未经授权的人使用,为授权执行相同的操作。

低保真原型中的可靠性

你可以通过注入这些故障并观察发生了什么来轻松地探索架构对故障的反应。在本章的性能部分,我谈到了拥有一个模拟真实网络请求内存和时间需求的占位符网络模块。同样,你可以安排它时不时地失败,并观察系统其他部分如何应对这种故障。一些公司甚至在生产中注入随机故障**github.com/Netflix/SimianArmy/wiki,以确保他们的系统能够应对。

适当推迟;必要时承诺

与逐步精炼的原型一起工作意味着架构会迭代地更加完整;因此,某些决策会变得更加“固定”和难以更改。记住之前提出的概念:架构应该始终准备好回答开发者的疑问。这意味着开发者首先工作的东西应该是首先解决的问题。但这是一种同义反复的陈述,因为你可能能够安排开发工作以跟踪架构的变化。

从最具风险的事情开始是最好的。它们可能是产品探索性的方面,这些方面与团队之前所做的工作不相似,它们可能是与其他软件或其他组织接口的部分,或者它们可能是可能产生最高成本的部分。这些是应用中最有可能发生变化的部分,变化将非常昂贵。首先处理这些问题意味着在项目早期,在计划和成本变得过于固定之前,会有很高的变化率,而不是在项目结束时,当人们对何时一切准备就绪有期望。此外,在编写大量代码之前做出的更改意味着需要重写的代码更少。

这里存在一个期望管理问题。在探索性和实验性工作中,你必须能够说服客户、经理以及任何询问的人,对于“需要多长时间?”的回答是“我不知道;我们还没有确定”以及到目前为止所取得的任何进展都是虚假的。它可能看起来你取得了很大的进展,但其中大部分将是模拟代码,实际上并没有做它看起来要做的事情。在我领导的两个独立项目中,我们遇到了麻烦,因为某个利益相关者基于看到原型而对项目的进展做出了假设。这不是他们的错;这是我的责任,要提供关于项目状况的现实评估,让他们可以根据这个评估来判断如何继续进行。

证明你的决策是合理的

因此,你已经选择了将在应用程序的某个特定方面使用的技术。这是因为它将以最少的努力满足客户的需求,还是因为它是你自从参加那个会议以来就想使用的最新潮的东西?

当有人问为什么团队使用特定的语言、框架或模式时,仅仅耸耸肩,并说“适合这项工作的正确工具”并不是一个令人满意的答案。是什么让这个工具成为适合这项工作的正确工具?它是否满足了一些其他替代方案不具备的要求,比如兼容性?它是否比替代方案更便宜?(记住,成本是整体计算的:如果它显著减少了工作量并降低了引入错误的可能性,那么商业工具可能比免费工具更便宜。)

你需要说服其他人,你选择的解决方案适合当前的任务。在提升你的修辞技巧(这确实很有用——第十三章“团队合作”中有一个关于谈判的部分,以及一个关于批判性思考的整章)之前,首先要确保它确实是一个适合这项工作的工具。考虑人们可能会有哪些不同的考虑因素:

  • 客户:这项技术能否让你构建出满足所有要求的东西?你或其他人能否随着我们的需求变化而调整解决方案?我们负担得起吗?它与我们的现有环境兼容吗?

  • 开发者:我已经知道这些了吗,还是我需要去学习?学习这个技术会有趣吗?使用这项技术是否符合我的职业规划?

  • 管理层:这是否具有成本效益?这真的是这个项目的最佳解决方案,还是只是你一直想学习的东西?总线因素en.wikipedia.org/wiki/Bus_factor会是什么?我们能向其他客户销售这个吗?我们能从供应商那里购买支持吗?它是否与公司的能力和目标相匹配?

如果你能够诚实地回答这些问题,并且你选择的技术仍然看起来是最好的答案,那么,我并不会说你不需要你的说服力和谈判技巧——只是说你会更容易地运用它们。

但请记住,谈判就像是一种需要两个人的探戈。在《我们生活的隐喻》——theliterarylink.com/metaphors.html中,拉科夫和约翰逊提出,我们思考论点的方式受到我们使用战斗隐喻的影响。好吧,用巧妙收集的修辞手法击败对手在学校辩论社团中是可以的,但我们都需要记住,我们在软件领域是通过构建最好的东西来取得胜利的,而不是通过压倒性的反对意见。在压力之下,尤其是要放下自我,接受批评作为共同构建更好事物的手段可能会很困难。但这样做很重要:回顾一下人们提出的不同担忧,想想我可能忘记添加的其他担忧,并意识到你对项目最佳看法的看法只覆盖了故事的一部分。

何时修复和何时更换

作为软件架构师,你经常需要证明的一个特定决定是是否继续使用一些现有的代码,或者是否将其丢弃并用其他东西替换。好吧,你很少需要证明保留现有东西的决定;你经常需要证明替换它的合理性。

这本应是如此。虽然想象着抛开所有遗留的垃圾并开始一个新的绿色实施项目可能会让人感到满足——甚至让人感到平静——但避免这样做有很好的理由。现有的代码可能看起来有缺陷且难以理解,但你的团队已经对它有了现成的经验,并且可能知道问题和限制在哪里。对于尚未存在的替代品来说,情况并非如此,它可能会带来自己的困难和缺陷。

现在重要的是要认识到,这个论点与沉没成本谬误不同。那将是争论你不应该丢弃现有的代码,因为它已经花费了时间和资源;我在说的是,你应该仔细考虑开发新事物的成本是否真的低于继续使用现有事物的成本。

在许多情况下可能并非如此。这里有一个问题:你的替代实现将有多少个错误?这些错误是什么?修复这些错误需要多长时间?如果你能预测这一点,你可能不会留下这些问题,你也可以预测修复现有实现中的错误需要多长时间,并比较两者。然而,经验告诉我们,预测一项开发工作的质量是非常困难的。因此,存在一种可能性,即虽然你的新实现可能会修复原始版本中的一些错误(因为你在开发它时意识到了这些问题),但它可能会引入新的问题,包括回归问题,即早期版本的工作效果比其替代品更好。你必须将这种风险纳入你的决策中。

当替代品不是你打算内部构建的东西,而是一个你可以使用的开源或商业模块时,这种情况的经济学发生了重大转变。在这种情况下,获取软件的成本将是众所周知的,并且可以通过检查错误数据库或询问社区或供应商来调查其适用性。集成成本以及你将负责解决问题的程度(以及如果你不解决这些问题将产生的成本)是剩余需要考虑的成本。

关于重写的另一个想法:虽然它们对开发者来说可能不是明显的优势,但它们肯定不是对客户的益处。我见过许多应用程序,其中新版本被吹捧为“完全重写”,正如 GitHub 的 Danny Greg 所说,这不是一件好事。如果软件的新版本是一个完全重写,那么对我来说作为客户,它与上一个版本唯一共享的是名称和图标。

在重写版本中,我依赖的前版本中的某些东西可能不会像以前那样工作,或者根本无法工作。这对我来说是一个评估竞争产品的绝佳机会。

你面对的是一个已知且被充分理解的代码模块,存在一些已知问题。使用它是免费的,但你可能需要花一些时间修复一些问题,以便将其扩展以适应你的新项目。另一种选择是花一些时间构建做相同工作但具有未知问题集合的东西。尽管你的团队可能没有同样的经验,但它可能更符合你团队对良好设计代码的看法……这个月。在两者之间做出选择,以及我的代码是负债而不是资产的原则,我得出结论,我宁愿选择我认识的恶魔,而不是不认识的恶魔。

知道何时吹毛求疵,何时放手

一个优秀开发者的一个属性是能够分析问题的细节。开发者擅长这一点,因为计算机要求这样做;计算机在推理方面真的很差,所以你必须预测可能发生的每一个小情况,无论多么罕见,并告诉计算机如何处理它们。不幸的是,如果这个属性做得太过分,就会使程序员变成糟糕的交谈者——tirania.org/blog/archive/2011/Feb-17.html,在所有其他领域,包括其他软件创建领域。

当你或其他人正在为软件系统设计架构时,把它看作是对解决方案形状的低保真提案,而不是实际的解决方案。对于“这如何解决 X?”的问题,几乎可以肯定的是“它解决不了——这是一个早期阶段的原型”,所以甚至问这个问题都没有意义。你可以通过将解决方案构建到所提出的架构中来证明答案:如果它有效,你就创建了一个功能;如果它不起作用,你就发现了一些重要的事情。但通常,你会先认为某件事不会起作用,然后发现它实际上是可以的。

同样,“你为什么这样做?”这样的问题并没有什么用。如果那样做的人没有认为那样做是个好主意,他们就不会那样做。许多开发者不喜欢阅读其他程序员的代码,我认为这是因为开发者没有被教会如何有效地批判性地分析代码——blog.securemacprogramming.com/2012/12/can-code-be-readable/。如果你不能将“那是什么?”转化为关于所提解决方案的具体问题,就别问了。

这并不是说批评是坏的或不想要的。当然,它是受欢迎的——建筑将受益于多个人输入。但反馈必须与建筑本身处于相同的抽象级别

换句话说,反馈必须以对解决方案施加的约束以及它们是否可以在提供所需功能的同时得到满足为条件。像“我看不到frobulator接口的错误如何进入审计组件”这样的问题是好的。像“在持续饱和下这是如何退化的?”这样的问题是好的。像“如果我们使用这个模式,那么数据库插件可以很容易地互换”这样的建议是受欢迎的。像“这毫无用处——它不能处理当你打开命名管道时文件系统报告递归链接问题”这样的评论可以推迟。

支持,而不是控制

根据架构服务于在非功能性要求的约束内支持应用程序功能的定义,我们可以用类似的话来描述架构师的角色。

软件架构师做什么?

软件架构师的存在是为了识别影响软件产品技术实现的潜在风险,并解决这些风险。最好是,在他们停止或阻碍产品开发之前。

这可能意味着进行测试以调查提议解决方案的可行性或属性。这可能意味着向开发者和客户或经理传教,以避免这些人打断开发工作。这可能意味着给初级开发者提供关于某种技术的教程——或者让这位开发者辅导团队其他成员关于他/她擅长的东西。

软件架构师不做的事情

软件架构师不会对与他们合作的开发者进行微观管理。架构师不会通过备忘录和 UML 图来统治。架构师不会对没有经验的事情进行预测。也许有些令人困惑,软件架构师的职责与它所命名的职业相差甚远。如果你想找到与土木工程相关的类比,所有的开发者都像是建筑师。如果你想看到软件与建造者的对应,那是由编译器和 IDE 完成的工作。

架构师不会在没有必要的情况下做出决定。他们也不会忽视或轻视来自非架构师人士的建议。

简而言之

软件架构师的存在是为了让开发者更容易地进行开发。

第九章:第八章

文档

简介

软件项目中产生的文档数量差异很大。在深入探讨何时以及如何记录你的代码之前,我首先定义一下我如何使用这个术语。

在本章的上下文中,文档指的是那些旨在帮助其他开发者理解软件产品和代码,但不是可执行代码或产品本身的任何其他资源。代码中的注释,由于不是可执行的,是文档的一部分。单元测试,虽然可执行,但不包含在产品中——它们将是文档,除了我在第五章,编码实践中涵盖了自动化测试之外。UML 图、开发者维基、提交信息、错误报告中的描述、白板会议:这些都有助于向其他开发者解释——而不是计算机——代码做什么,如何做,为什么这样做。

另一方面,为其他利益相关者准备的文档,如用户手册、在线帮助、为用户提供的营销材料,或者为管理者准备的进度表和概述,将不会在此处讨论。这些同样非常重要,如果你需要制作它们,那么你需要做好这项工作。但慈善始于家庭,通过帮助他们理解正在工作的代码来节省他们的时间是肯定的一种慈善行为。

文档比你想象的更有用

不记录代码的常见理由是源代码本身就是准确的文档——www.codinghorror.com/blog/2012/04/learn-to-read-the-source-luke.html;也就是说,虽然文档可能会出错或者随着软件的变化而变得不准确,但源代码保证是软件行为的精确且精确的描述。

如果你假设框架和编译器没有错误,那么这个想法是正确的:源代码确实是软件行为的完整且精确的文档。问题是,它并不总是最合适的文档来阅读。

当然,源代码是完全准确的,但它也是抽象层次最低的。如果你刚刚加入一个项目并需要熟悉不熟悉的软件,按顺序阅读每个操作(一旦你甚至已经找到了正确的顺序),并不是最容易进行的方式。

即使你忽略这一点,仅使用源代码作为了解软件的唯一信息来源也存在问题。它确实能告诉你产品做什么。经过一段时间的学习,你也能发现它是如何做到的。但是,编程语言指令能告诉你软件为什么这样做吗?那个奇怪的if语句是为了修复客户报告的 bug 吗?也许它是为了绕过 API 中的问题?也许原始开发者只是无法想出解决这个问题的其他方法。

因此,好的文档应该告诉你代码为什么这样做,同时也让你能快速发现它是如何做到的。它应该提供背景信息,而不是细节,而源代码则提供了所有细节,但背景信息难以发现。换句话说,源代码代表了你创建的虚拟世界的精确计划,而你的文档应该是旅游指南www.infoq.com/presentations/The-Frustrated-Architect(据我所知,这个想法首先是由Simon Brownwww.codingthearchitecture.com提出的),它提供了地图,推荐去哪些地方(以及避免哪些地方),以及关于世界历史的资料。

时效性问题

关于创建除源代码之外的文档的另一个主要抱怨是,除非文档与源代码同时维护,否则它们会很快过时;阅读过时的文档比不阅读文档更糟;而且没有投入到工作代码中的努力都是浪费。

我首先来谈谈第二个问题。制作任何形式的开发者文档的目的,是为了让开发者更容易地使用软件。因此,创建文档的成本应该真正地与不制作文档的机会成本进行权衡。如果让开发者直接开始工作的努力节省的时间大于创建和维护文档所花费的时间,那么这样做是值得的。相反,如果权衡结果不佳,你需要决定是否放弃这种形式的文档,转而寻找更有价值的东西,或者找到更快的方式去制作它。

但其他问题又如何呢——过时的文档是否比没有文档更糟?这一点确实有道理,因为被引导到错误的方向不会帮助某人找到自己的路。然而,这个过程可能比你想象的要长得多,才会变得重要。记住,文档捕捉了高级功能:为什么(以及在一定程度上,如何)代码执行其功能。想象一下,你有一些文档,无论其完整性如何,都是最新的。你接下来的提交不太可能改变产品使用的框架,或者它连接到的数据库类型,甚至它如何对远程组件进行身份验证。从高层次来看,产品保持不变。

就像城市指南即使一些商店或餐厅改变了他们提供的服务仍然有用一样,你的代码旅游指南在方法有所改变时仍然可以有所帮助。文档真正过时无用的风险是一个在几年而不是几天内逐渐显现的问题。

自动生成的文档

在上一节中,我谈到了与生成文档相关的经济权衡:生产成本是否低于没有该文档的机会成本。可以通过两种方式使文档生产的平衡倾向于生产文档:要么降低生产成本,要么提高文档的价值。

从代码自动生成文档——通常称为文档的逆向工程——是一种降低生产成本的策略。这个想法很简单:如果开发者可以随时从源代码中创建文档,他们就可以随时获得关于代码如何工作的最新描述。

逆向工程工具,通常产生 UML 图,这是本章后面讨论的特定格式的文档(为了清楚起见,我并不是在谈论从代码注释中提取文档的工具;你仍然需要自己编写这种形式的文档),擅长提供项目的高级概述,其中省略了一些或所有细节。例如,给定一个类定义,如.java类或 Objective-C 的.h.m文件,逆向工程工具可以突出显示 API 方法和属性,如下面的图所示:

图 8.1:UML 类图

他们说没有免费的午餐(有些人说 TANSTAAFL),这是正确的。一方面,生产那个类图几乎不花任何成本。如果你理解 UML 类图(你还需要理解我是如何选择弯曲 UML 以使其更好地表示 Objective-C 的——U 代表统一,而不是通用),它确实比深入源代码并挑选出所有方法提供了一个更好的类 API 概述。但由于图表是从源代码生成的,而源代码没有告诉我们为什么它是这样的,这个图表不能为读者阐明这个类的背后的原因。

为什么 API 在一个地方使用委托回调而在另一个地方使用块回调?为什么使用NSURLConnection而不是其他类来下载内容?为什么一些实例变量是受保护的,而不是私有的?从这张图表中无法得知。

此外,你无法很好地了解如何。方法的调用顺序重要吗?在没有任何操作进行时调用取消方法可以吗?委托属性可以是nil吗?图表中没有说明。

因此,是的,自动生成的文档很便宜。它移除了代码中的信息,但没有提供任何额外的东西。拥有这样一个简要概述是有用的,但不太可能通过逆向工程生成的文档就能解决所有问题。

分析瘫痪

结合你从生成的文档中学到的知识,你可能会有将控制方向反过来。如果零输入努力的文档没有提供很多额外的价值,那么也许你投入更多精力创建文档,它就会变得更有用。

也许,在某种程度上,这是真的。然而,增加文档的增量价值是渐近的。事实上,情况更糟。创建过多的文档,人们甚至无法在没有一些指南——一些元文档的情况下使用那些文档——如果文档根本不存在,使用起来反而更容易。

注意,分析瘫痪http://c2.com/cgi/wiki?AnalysisParalysis)并不是直接与编写文档相关的;它实际上是一种有缺陷的设计方法。与文档的交互发生在你深入研究问题时。当你害怕从设计解决方案转向构建它时,就会发生分析瘫痪。你是否考虑了所有边缘情况?是否处理了每个异常条件?是否有你没有考虑到的用例?你不知道——你不想在弄清楚之前开始构建。

磨练你的架构文档或类图基本上是浪费时间。找到这些边缘情况的最佳方式是构建东西并看看什么不起作用——特别是如果你正在编写单元测试来覆盖 API 的角落。通过将软件提供给客户,你会发现缺少了一个用例。

因此,分析瘫痪并不是在创建文档时出现的问题;它发生在你专注于文档的时候。记住,在章节的开始,我说过文档的存在是为了通过帮助程序员来支持代码的开发。你的目标是你的产品:你的客户想要使用的东西。

如何编写文档

本章的前几节讨论了编写文档的原因,即其带来的好处,以及为什么如果你做得太少或太多可能会遇到麻烦。现在,我们来讨论如何编写文档,现有的文档形式,它们如何有用(或否则),以及如何着手编写它们。

编码标准

大多数有超过几个开发者共同工作的组织都有一个风格指南或编码标准。这份文档解释了编写代码以创建“公司风格”的细节:括号应该放在哪里,变量应该如何命名,缩进应该用多少空格,等等。如果你以前没有见过,GNU 编码标准——www.gnu.org/prep/standards/standards.html非常全面。事实上,我工作过的公司要求他们的代码符合 GNU 标准而不是自己编写:它已经存在,涵盖了大多数问题,并且很容易遵守。

编码标准对于确保新加入项目的开发者编写一致的代码非常有用——尤其是那些可能还没有意识到单一布局、变量和方法命名等价值的新手程序员。 (其价值在于你不会对变量的名称、表达式的位置等感到惊讶。代码的组织不会妨碍你专注于代码的意义——也许除了为什么、如何、什么,我应该再加上在哪里。) 对于熟悉他们使用的语言及其习惯用法的开发者来说,编码标准文档是浪费时间:他们可以从代码中看到你如何布局括号;他们可以自动适应你的风格,或者至少配置他们的 IDE 来为他们完成这项工作。正如 Herb Sutter 和 Alexei Alexandrescu 在《C++编码标准》中所说:

那些仅仅是个人品味问题,并不影响正确性或可读性的问题不应包含在编码标准中。任何专业的程序员都能轻松阅读和编写格式略有不同但符合他们习惯的代码。

很遗憾,许多编码标准文档并没有超出那些表面的特征。

编码规范中不具体描述如何布局代码的部分是没有用的。它们是那些想要控制别人写作的人的繁琐工作。告诉开发者“确保所有异常都被捕获”或“处理所有错误”并不是他们会放在心上的事情,除非这是他们工作的一部分。如果你想要确保程序员能够捕获异常或处理错误,那么你需要找到那些做不到的人,并指导他们将其作为他们思考工作的一部分。在第一天给他们一些文件上的命令是不够的,甚至第二天也不会记住。

一个经验丰富的开发者,如果还没有学会处理所有错误,仅仅因为一个维基页面告诉他们这样做,是不会开始的。一个已经学会处理所有错误(除了他们不知道的那个)的开发者,不会通过阅读编码规范文档来发现那个错误。一个不知道错误条件如何出现的初学者,仍然一无所知。

高级目标,如“处理所有错误”、“记录所有断言失败”(这可能是“断言所有前提条件和后置条件”之后的条目),等等,对于代码审查清单来说很棒。它们对于自动代码分析规则来说甚至更好。它们不属于标准文档:没有人会仅仅因为阅读了一个要求它们的要点就把这些事情变成“标准”。

编码规范与我

如前所述,我在一家使用 GNU 标准的公司工作过。我还在一个时期为开发团队创建了编码规范,当时所有团队成员(包括我自己)对我们所使用的语言都不太熟悉。

在过去大约 4 年的时间里,尽管我在多家不同的公司工作并签订合同,但没有任何一家公司有文档化的编码规范。我并没有真正觉得缺少它——所谓的“标准”布局变成了“IDE 默认的布局”,其他所有事情都是通过自动化或手动审查完成的。

那么,我会推荐编写编码规范吗?只有在缺乏规范导致问题的时候才会。实际上,编写一个在代码提交到仓库之前重新格式化代码的预提交钩子可能更容易——尽管这可能更加被动-aggressive。一些 IDE(例如来自 JetBrains 的 IDE)已经提供了这个功能。

代码注释

每当提到评论时,总会有人搬出一些陈词滥调:

真正的程序员不会注释他们的代码。如果编写起来很难,那么理解它应该也很困难,修改起来甚至更难(来自 真正的程序员不会编写规范——ifaq.wap.org/computers/realprogrammers.html)

任何代码都应该自文档化。(在互联网上到处都是;在这个例子中,在Stack Overflow上——stackoverflow.com/questions/209015/what-is-self-documenting-code-and-can-it-replace-well-documented-code)

显然,第一个引用是一个玩笑,如果不是,请阅读相关的文章。第二个引用不是玩笑,只是非常错误。

当你编写任何代码时,从心理上讲,你处于“状态”之中。你很可能专注于那个问题,排除所有(或者至少是许多)其他问题。你已经针对那个特定的问题工作了一段时间,并且在那个领域的问题上工作了一段时间。所以,当然,你认为代码不需要任何注释。当你阅读代码时,它会触发所有那些突触连接,让你回想起你为什么要写它以及它应该做什么。

没有人能享受到这些联系的好处。即使是你,当你稍后回到代码时,也没有这种好处:没有加强的记忆会随时间衰减——www.simplypsychology.org/forgetting.html。根据那个链接,如果它们没有被巩固,记忆就会从长期记忆中消失。

考虑到这一点,注释是你能创建的最好的文档形式之一,因为它们在两种不同的信息形式之间建立了联系。信息是代码和文字注释,联系是直接的:你可以在同一个地方看到它们(即你 IDE 中的源代码编辑器)。如果其中一个不能让你回想起你产生它的想法,那么它与另一个的联系会触发一些记忆。

回想(这里有点故意玩文字游戏)本章开头的讨论,代码可以很快地告诉你软件做什么,经过一点工作就能告诉你它是如何做到的。没有必要再通过注释重复这些内容——你已经在看一些能提供这些信息的东西了。(快速回顾代码的工作原理可以节省阅读时间。或者,正如弗莱泽·赫斯通过引用弗兰克·韦斯特海默的话所说,“在实验室的一个月可以节省在图书馆的一个小时”——twitter.com/fraserhess/status/299261317892685824。)因此,注释应该专注于为什么

许多人会因为看到类似这样的代码而感到厌烦:

    //add 1 to i
    i++;

当你编程经验足够丰富,知道你语言中各种操作符的作用时,这样的注释就是多余的噪音。如果所有注释都像这个例子一样,那么有能力的开发者阅读注释就几乎没有意义了——在这种情况下,确实很难为他们写注释的行为找到合理的理由。显然,并不是所有的注释都是这样的;实际上,你写的注释不需要是这样的。

如果你很难相信有人需要提醒++操作符的作用,那么你可能不记得学习编程的经历,也没有教过编程。《教学 H.E.编程博客》——teachingheprogramming.blogspot.co.uk 是一个很好的概述,说明了对于不经常做这件事的人来说,你每天做的事情有多么困难。

问题是多余的注释就是多余的。你读它们,意识到它们没有帮助,然后继续。这不会浪费太多时间。更糟糕的是阅读那些让你精神上感到冲击的注释:那些会打断你对代码思考并让你思考注释的。

那个在你脑海中看起来非常有趣的笑话——不要写下来。它可能在推特或公司聊天室里效果很好,但在代码注释中就不合适了。即使阅读它的人第一次觉得它很有趣,但如果他们每天都要停下来反复阅读这个笑话,以便理解代码,那么他们可能就不会觉得有趣了。

当我写这本书的时候,有人在问答网站上问,是否有关于代码注释价值的实证研究——programmers.stackexchange.com/questions/187722/are-there-any-empirical-studies-about-the-effects-of-commenting-source-code-on-s。更有用的一点是,有人回答了这个问题,并提供了参考文献。其中一篇论文,《模块化和注释对程序理解的影响》——portal.acm.org/ft_gateway.cfm?id=802534&type=pdf&coll=DL&dl=GUIDE&CFID=278950761&CFTOKEN=48982755,值得更详细地研究。

您的第一反应可能是查看这篇论文的日期——1981 年 3 月——并决定它不可能对现代程序员有任何相关性。但请稍等。这篇文章调查了人们(在三十年中变化不大)如何阅读(也没有太大变化)用英语(变化也不大)写的注释和按不同模块化线路组织的代码。唯一改变的是我们编写代码的方式,而且变化并不大。这篇文章调查了 FORTRAN 语言的代码,这种语言仍在使用,并且与 C 语言不太相似。它调查了使用不同模块化方法的代码,这种变化在现代代码中是观察到的,无论是使用过程式还是面向对象的编程语言。真的没有理由因为文章的年龄而摒弃这篇文章。

他们所做的是为一个问题实现几种不同的代码解决方案:一个单一程序,一个模块化程序,一个过度模块化程序(每个“模块”由 3-15 行组成),以及一个围绕抽象数据类型组织的程序。他们为每种程序产生了两个不同的版本;一个有描述每个模块功能的注释,另一个没有。有趣的是,为了消除关于程序操作的其它线索,他们使所有变量名非描述性,并移除了任何格式化提示。

是否这代表了一种与例如使用一致的(有意义的)命名和格式化策略在所有示例中一样好的控制,这值得探索。48 名程序员每人被分配了一个版本的代码和关于其操作的测验。他们总结了以下结果:

注释结果似乎表明,通过添加总结模块要执行的功能的简短短语,可以显著提高对程序的理解。与原始假设相反,结论是注释对逻辑模块识别没有显著益处。那些使用未注释的单一版本的人似乎能够理解程序并理解各部分之间的交互,就像那些使用带注释的单一版本的人一样。然而,似乎那些使用未注释的模块化程序的人发现理解模块的功能以及它如何融入程序上下文比那些得到带注释的模块化版本的人更困难。

这并不是说“注释是好的”或“注释是坏的”。确实说,特定类型的注释可以帮助人们理解模块化程序。请注意,它还指出,未注释的模块化程序比未注释的单一程序更难理解。这个结果与第五章“编码实践”中的 Dunsmore 等人研究有什么相关性吗?记住,他们发现面向对象的程序很难理解:

那些导致松散耦合对象连接系统的理想设计属性,也产生了一个难以发现执行流程的系统;你不能轻易地看到任何特定消息的结果控制流向何方。

文学编程

Donald Knuth 将注释回忆程序员思维过程的想法进一步发展,提出了文学编程www.literateprogramming.com)的概念。在文学编程环境中,程序被编写成“网络”,其中散文和代码可以交织在一起。

程序员被鼓励解释他们所创建代码背后的思维过程,包括将代码实现作为文档的一部分。在网络上使用超链接的代码引用树来生成仅包含源代码的网页视图(通过一个名为 tangle 的工具),然后可以将这些内容输入到常规的编译器或解释器中。另一个工具 weave 将网页转换成格式化的可读文档。

这个超链接图的目的是将编程语言所需的结构(例如,面向对象语言中的类和方法)与你的思维结构分开。如果你在思考两个不同的类以及它们将如何交互,你可以在想到它们的时候编写代码的部分,并告诉编译器它们应该如何排序。

在稍后阅读网页时,编写者会记得他们为什么做出这样的决定,因为代码的组织结构与他们的思维过程相匹配。其他读者将了解代码是如何演变的以及为什么做出某些决定:这是编写文档的关键原因。

我不确定文学编程是否是一种应该采纳的风格——我还没有用网络构建过任何大型项目。我虽然尝试过 LP 工具,并且发现这是一种编写软件的有趣方式(但无论如何,我也喜欢写散文)。我不确定它是否能够扩展——不一定适用于大型项目。如果我在写测试驱动 iOS 开发blog.securemacprogramming.com/2012/04/test-driven-ios-development/)时知道 CWEB,我可能会更快地完成它,并且错误更少。当《实用程序员》(pragprog.com/the-pragmatic-programmer/)的作者写那本书时,他们实际上重新实现了文学编程的一些部分,以保持他们的手稿与代码同步。

我所担忧的扩展问题是扩展到多个开发者。如果你觉得阅读别人的代码风格令人不快,那么等到你必须阅读他们的未经校对的散文。当然,有一种方法可以找到答案。

注释文档

当前的编程文献网站专注于你的思维和文档的结构,让代码适应这种流程,但许多其他工具存在,它们保留了代码的结构,但提取并格式化注释为超链接 API 文档。(注释文档不是在上述“注释”中讨论的吗?不完全是这样,因为其形式性和意图非常不同。)

这些工具——包括 Doxygen、Headerdoc 及其朋友——保留了代码与其文档的邻近性。当你对方法进行更改时,你可以看到其注释就在上面,这邀请你进行更新以保持一致性。

我发现为那些我认为其他人可能会使用的类和接口生成注释文档很有帮助。我通常不会生成美观的输出,但如果人们想要这么做,他们可以这么做。我当然欣赏这种选项,并且会使用格式化的文档来为其他程序员的 API 提供帮助。

一些静态分析工具,特别是微软的,会警告关于未记录的方法、类和字段。这导致仅仅为了存在而添加注释,而不一定导致文档标准得到提高。解释方法目的是“香蕉”及其返回值是“香蕉”的格式化注释到处都是。

注释文档中指定的大部分内容通常包括对方法输入值的限制(“index参数必须大于 0 但小于count”),何时调用它们(“在调用configure()之前调用此方法是一个错误”),或对返回值的期望(“返回的对象将具有小于2*countsize”)。这些可以作为断言(通常是在文档之外,而不是代替文档)表达的内容,或者你可以使用支持契约的语言。

Uml 图

UML 是一个很大的主题。关于这个主题已经写了几本书。我甚至不想尝试复制所有这些内容,所以这里有一个简化的版本,它还让你能够与其他绘图技术进行比较:

UML 图是以符合 UML 规则的方式表达你的代码某些方面的视图。任何理解这些规则的开发者都将从图中获得相同的信息(当然,前提是图实际上表达了足够的信息以避免歧义)。

这意味着你可以考虑 CRC 卡片、数据流图和其他技术,这些都被包含在本节中。

首先要注意的是,即使你不了解 UML,也可以理解 UML 图。它只是框和线,尽管有时“框”的意义比“事物”更精确,而“线”的意义比“连接到这个其他事物”更精确。不要被这种想法吓倒,即它是一种复杂的语言,有很多规则需要学习。这只有在你想让它成为这样的时候才是真的。

这样的图表可以在许多上下文中出现。我通常将它们作为快速草图创建,在白板上或用纸和铅笔(或它们的现代等价物——iPad 和触控笔)。在这些情况下,规则不是重要,但确实增加了其他读者理解我图表细节的可能性,以及我在记录同一件事情时可能会创建相同的图表两次。

很明显,这样产生的图表可能是暂时的,而不是永久的。它们可能会通过 iPhone 照片“以防万一”来保存,但可能性更大的是,它们将永远不会再次被查看。当然,没有人期望它们会进入某个“项目 X 工件”文件夹而被无限期保留。

你在这类图形上投入的精力越多,你越有可能想要保留它。对于像博客文章或书中的图表这样的东西,我通常会使用Omnigrafflewww.omnigroup.com/products/omnigraffle/),dialive.gnome.org/Dia/,或者其他一些让我可以使用 UML 中的形状和线条,但不关心规则的工具。

我也使用了一些关心规则的工具。我在一家公司工作过,该公司拥有Enterprise Architectwww.sparxsystems.com.au)的站点许可证,这是一个要求你构建符合规则的图表并支持通过代码“往返”的工具。往返意味着它可以从代码(前面讨论过)生成图表,也可以从图表生成存根代码。它还可以尊重现有代码,在向类添加新功能时不会覆盖现有方法。

一些其他团队广泛使用了这种方法,维护他们的组件或应用程序的 UML 设计,并在生成的 C++或 Java 类中实现行为。我的团队无法使用它,因为工具不支持(据我所知,现在仍然不支持)Objective-C。因此,我觉得自己没有资格谈论这是否是一个好主意:我的直觉是,这可能是一个好主意,因为它强迫你在设计时从高层次(图表中暴露的功能)思考,而不会陷入实现细节。另一方面,不同的语言有不同的习语和做事的偏好方式,这些在 UML 模型中并不容易表达。还有一些与配置代码生成器以符合团队喜好相关的开销——即使你不需要编写代码,你仍然需要阅读它。

摘要

当你需要它的时候,文档是一件好事。它有助于告诉你软件为什么和如何做它所做的事情,而当代码只能告诉你它做了什么,以及一点点的如何时。

维护文档会产生额外的成本,并存在文档和代码可能不同步的风险。有各种方式来记录代码,通过实验可以找到努力与收益之间最佳的权衡点。

第十章:第九章

需求工程

在过去几十年中,关于如何构建正确软件的思考可能与如何更好地构建软件的思考大致相当。20 世纪 60 年代至 80 年代的软件工程技术解释了如何构建需求规格说明,如何验证交付的软件满足规格,以及如何让在构建和测试软件过程中做出的发现反馈到规格中。

在 20 世纪 90 年代,出现了倾向于用户与软件构建者之间更紧密互动的方法。快速应用开发放弃了“前期大量”规划,转而采用快速迭代的原型,客户可以探索并给出反馈。极限编程将这一想法进一步发展,不仅让客户或客户的代表在开发过程中评估产品,而且在项目进行中还参与优先级排序和规划项目。(将这些 20 世纪 90 年代的想法称为简化。RAD 和其他方法背后的许多概念至少从 20 世纪 70 年代以来就已经存在,系统性的文献综述可以将这些想法更精确地定位到日历上。

然而,将这些想法综合成构建软件的提议系统是在 20 世纪 90 年代,也是在 20 世纪 90 年代,开发团队开始使用这些系统,供应商创造了产品来满足他们的需求。

与此同时,软件应用向用户展示的历史也在不断发展。这种展示的成功体现在 successive generations of practitioners( successive generations of practitioners 指的是连续几代从业者)逐渐远离了前一代使用的术语。如果使软件易于使用的尝试被证明是有效的,那么人们会乐意将自己与该领域联系起来。相反,人机交互已经不再受欢迎,同样不再受欢迎的还有人机界面设计计算机支持的协作工作交互设计用户界面设计等等。很快,用户体验将成为历史简历中的关键词之一。

如果构建软件的全部目的是为了让人们更容易地做事,那么我们应该调查人们试图做什么以及如何支持他们。在这个过程中,我们可以了解我们自己的工作方式,这可以帮助我们改进自己的工作(也许甚至可以通过编写软件来实现)。

研究人们

软件应用并非存在于真空之中。它们是由人使用的;一个由具有现有目标、想法、价值观以及彼此之间互动(是的,程序员,现有技术)的人群系统。将新的软件产品引入这个系统无疑会改变这个系统。它是否会支持现有的目标和价值观,或者用新的来取代它们?它是否会简化现有的互动,或者引入摩擦?

为了回答这些问题,我们必须有一种方法来衡量这个人群系统。为了做到这一点,我们必须了解我们应该就这个系统提出哪些问题,以便支持我们想要学习和发现我们应该衡量什么。

决定模型

第六章,测试中,我必须首先决定软件系统的需求并非源于宇宙的基本真理,而是基于使用该系统的人与世界以及彼此互动的方式。现在想象一下,你正在尝试理解像 Excel 这样的应用程序的需求。你会考虑每个百万级用户的个别需求吗?虽然这可能导致产品质量更高(或者如果你通过提供不同的解决方案来解决冲突需求,则可能是多个产品),但很少有,如果有的话,公司能够承担起涉及的研究,即使他们能够承担,从结果软件中获得利润也可能很困难。

选择少数具有代表性的用户来设计和开发软件要便宜得多。有些团队选择真实的客户,而有些团队则基于假设的客户或市场研究创建“用户画像”。无论采用哪种方式,产品都将代表那些真实或想象中的人的实际或想象需求。

用户画像给人一种为用户设计的印象,但实际上产品团队只是将他们对软件应该是什么的印象外化了。当鲍勃只是一个贴在白板上的股票照片时,很容易从“我想这个功能”变成“鲍勃会想要这个功能”。鲍勃不会参与讨论,所以不会告诉你相反的情况。关键是要进入虚构的鲍勃的头脑,问他“为什么”他会想要这个功能。有时,我所在的团队在使用用户画像时,会指定某人在讨论中担任他们的倡导者。这给了那个人挑战试图将话语放入用户画像嘴里的权利;虽然这并不完全等同于有真实客户参与,但仍然很有用。

初看起来,内部或“企业”软件的构建者似乎情况要好得多;找到将要使用软件的人,为他们构建。关于这种软件环境模型,还有一些重要的问题。一个明显的问题是你要在哪里停止。你正在为的团队是否代表公司中的一个孤立单位,有明确的输入和输出,还是你将这个团队和其他团队之间的互动视为系统的一部分?那么与客户、合作伙伴和其他外部实体的互动呢?文章关于企业架构的三种思想学派——ieeexplore.ieee.org/lpdocs/epic03/wrapper.htm?arnumber=6109219探讨了这些边界对考虑涉及的系统的影响。

在确定了系统的范围之后,你是为目前构成该系统的特定人员设计,还是为更抽象的概念,例如那些人员所担任的角色设计?在两种情况下,都要意识到政治偏见可能进入你的模型。根据经理与其下属之间互动的协作模型设计的软件将与基于受压迫工人与剥削资产阶级之间斗争的模型不同。因为软件最终会改变其部署的系统,这样的决策将影响人们相互工作的方式。

你不一定需要构建客户要求的东西

发现任何软件应用的需求都很困难,即使构建它的人将是使用它的人。在第六章,测试中,我探讨了每个人都有自己关于软件应该做什么的想法,而在第七章,架构中,有些需求并没有明确表达。所以,如果你只是要求每个人列出软件应该做的事情,并据此构建,那么它将充满冲突,可能不会满足任何一个人对它的所有期望。

虽然询问人们是找出软件应该做什么的不准确方法,但询问人们是其中最容易和最可访问的方法之一。你可以通过定向问卷或开放式讨论来采访人们,了解他们对感兴趣系统的看法,并希望揭示一些那些隐含的需求。你还可以召集一群人,作为一个圆桌讨论或焦点小组,共同讨论他们的需求和问题。即使人们在尽力回答你的问题时很有帮助,也可能会出现解释他们答案的问题。他们所做的是一项专业活动,制作软件也是如此。每个学科都将有自己的术语和公认的“常识”知识;在这些术语之间进行翻译将是困难的。每个人都有自己版本的对“每个人”做他们工作所知道的事情,可能不会想到告诉你那些事情。

所以,有一种艺术(或者也许是一种科学;我认为该行业还没有下定决心)在于超越直接回答你的直接问题,找出你应该提出的问题以及你永远不会得到答案的问题。这就是定制软件(尤其是所谓的“企业”软件)有机会提供比现成软件更好的体验的地方;你有机会观察你的用户真正在做什么,并提供支持这些行为的软件,而不是提供支持他们声明的需求的东西。

你还需要记住,是软件专家,而你的客户是解决他们所解决问题的专家。当他们在谈论他们遇到的问题时,关于如何解决这个问题的信息比他们告诉你他们预想的解决方案时更多。这并不是说你不应该接受他们的建议;但你应该记住,他们的专业知识在其他地方,而你的团队可能在设计软件方面有更多的经验。显然,如果你是一个在开发工具上工作的初创公司,你的团队可能比你的客户有更少的经验。

避免询问你想要听到的

如果你有一个宠爱的功能,在采访和焦点小组讨论中,很容易将其融入到对潜在用户的讨论中。你面临的问题是你很容易让人们同意这个功能是个好主意,即使它实际上并不是。

首先,你必须区分人们认为他们会使用的东西和人们确实使用的东西。考虑一下你使用的任何文字处理软件,想想它有多少你从未使用过的功能。当你购买该软件时,你是否被营销材料中对这些功能的讨论所影响?(关于文字处理器具有比人们使用更多的功能这一观点,已经被人机交互研究人员调查过——www.cs.ubc.ca/~joanna/papers/GI2000_McGrenere_Bloat.pdf,尽管他们发现一些功能被某些用户未使用,但用户仍然知道这些功能存在,并对它们的功能有所了解。因此,说这些额外功能完全没有价值显然是夸大其词;然而,由于这里描述的特征矩阵营销,我们通常将是否将功能纳入产品中的默认选择定为“是”。你认为大多数其他用户是否真的使用了这些功能?如果软件没有这些功能,它是否仍然具有同样的价值?在具有功能的应用程序和没有功能的应用程序之间进行选择时,人们通常会选择具有功能的应用程序,即使他们现在看不到需要它的理由。尤其是在收集需求时,没有其他信息可供参考;如果不能看到这两个(目前假设的)应用程序,潜在用户就无法比较它们的可用性、速度、质量或其他功能,所以选项实际上真的归结为“有”或“没有”。

还要记住,那些对某个声明没有强烈观点的人倾向于同意它。这在心理学领域被称为顺从反应偏差,在评估问卷结果时需要考虑。以下是一个例子。想象一下,你想要构建一个“清洁编码者”IDE,但首先你想知道是否有人会使用它。你创建了一份问卷,要求受访者对这些陈述进行评分,以表示他们同意或不同意这些陈述的程度:

  • 专业程序员编写单元测试。

  • 一个好的方法应该尽量减少循环和分支。

  • 长且描述性的变量名更好。

另有人想写一个“简化版”IDE,回顾那些“真正的程序员不吃千层面”的时代,他们只是完成工作。(这是一个对文章真正的程序员不用 Pascal——www.ee.ryerson.ca/~elf/hack/realmen.html的戏谑引用,该文章本身是对书籍真正的男人不吃千层面——bit.ly/2XjLjxw的戏谑引用。那本书本身是讽刺的,但我已经没有地方可以插入我的舌头了。)他们创建了一份问卷,让受访者对这些陈述表示同意的程度:

  • 花在编写测试上的时间是用来不增加价值的时间。

  • 一个好的方法需要有足够的循环和分支来提供一个简单的界面,进入复杂的工作。

  • 打字不是编程的重点;简洁是一种美德。

这些问卷将产生不同的结果;不一定完全相互对立,但确实每个都揭示了各自尺度高端的偏见。这是默认响应偏差;每个人都问了他们想听的问题,并且每个案例的受访者都倾向于同意它。两位研究人员应该各自从两个列表中选择问题组合,以获得更具代表性的调查。

最后,记住告诉你的客户“我认为我们应该这样做”会由于一个称为锚定的认知偏差而使他们倾向于这种方法——www.sciencedaily.com/terms/anchoring.htm)。一旦在他们的脑海中“锚定”了某个特定功能或工作流程,他们就会更喜欢包含该功能或工作流程的选项,即使从理性上看它比无关的替代方案更差。你可能会因为它是你首先想到并脱口而出的而偏袒一个次优或昂贵的方案。最好是尽早留出选项,这样你就不会让你的客户与你在后来创建的更好设计相对抗。

理解问题领域

如前所述,你和你的团队是软件制作的专家,而客户是软件将要完成的事情的专家。我曾警告过不要用这种区别来构建你想要的软件而不是客户需要的软件;这难道意味着软件人员坚持软件,而客户坚持他们的问题领域吗?

不。

你需要知道你正在为谁构建,所以你需要对问题领域有一些了解。是的,这是不对称的。这是因为情况是不对称的——你正在构建软件来解决一个问题;问题并不是为了你可以编写一些软件而创造的。就是这样,并且妥协必须更多地来自软件制作人,而不是我们为之工作的人。你越了解你试图解决的问题,你就越能从该领域和软件领域综合想法来创造有趣的解决方案。换句话说,如果你了解软件将要做什么,你就能编写更好的软件。这希望不是一个有争议的想法。

这种理解可以在不同的层面上实现,与与客户的不同程度的互动相关。第五章,编码实践描述了领域驱动设计和无处不在的语言:定义问题域概念的术语表,也应用于命名软件域中的部分。不用说,所有参与软件开发的人都应该熟悉无处不在的语言,并以相同的方式使用它——否则就不是无处不在了!无处不在的语言的目的是确保当人们使用技术术语或行话时,每个人——客户和软件制造商——都意味着相同的事情。因此,它更倾向于使用来自问题域的术语,这样非软件人员就不必学习软件术语,并且预期这些术语渗透到软件设计和实现中,而不仅仅是用于客户会议。

应该将无处不在的语言视为一个起点。包括极限编程在内的一些方法要求开发团队有客户代表在场,以确保开发工作始终在增加价值。这些讨论需要在业务层面进行,也就是说,在问题域的层面。(这也是程序员经常感到沮丧的原因之一,因为业务没有安排时间进行重构、开发基础设施或“偿还”技术债务。问题在于,在业务讨论的背景下提出这些事情是一个错误;这些是我们所做事情和我们如何相互合作的内部细节,与我们如何与客户合作以及业务价值无关。如果某些重构工作可以使软件更容易工作,那么就去做,让业务看到成本降低的结果。)这反过来意味着至少需要一个人能够与客户代表就手头的问题进行同行讨论。

揭示隐性需求

本章已经涵盖了这样一个观点:你需要找出客户从他们的软件中需要的东西,而这些东西他们并没有提到。但再次提出这一点是值得的,因为无处不在的语言可能存在无处不在的漏洞。

想想那些你对外部软件领域的人询问你正在编写的应用程序时感到惊讶的时刻。好吧,当然,我们为七英寸平板电脑制作的那个应用程序不会在三英寸的手机上工作。这是一件如此基本的事情,甚至不值得提,那么为什么有人会问这个问题呢?

现在考虑一下这种情况的反面。在你的问题域中,人们认为哪些事情是如此基本,以至于他们永远不会提到?那些教授在第一年讲座中告诉他们是“明显”的事情,他们从未质疑过?你将如何让任何人告诉你这些事情?

就像配对辅导一样,这是一个表现得像任性的幼儿可能对你有利的情境。领域专家可能有自己的做事方式;找出为什么将会揭示他们没有想到要告诉你的东西。这会让人感到沮丧。有些事情我们没有真正的理由去做;它们只是“最佳实践”或完成工作的方式。对这些事情进行探究会引发认知失调,这可能导致人们变得防御性;重要的是让他们知道你之所以提问是因为你意识到他们在这些事情上多么专业,而你只是需要了解基础知识以便为他们做好工作。

为什么会有认知失调?好吧,有时候我们只是因为“这就是他们做事的方式”而做事,而不是因为这种技术有任何已知的价值。我们可以在软件制作的领域中找到这样的例子。许多开发者(尽管并非所有)使用版本控制。这样做有什么好处?令人惊讶的是,没有找到任何研究——www.neverworkintheory.org/?p=451——来调查这一点。然而,包括我在内的许多开发者会告诉你,版本控制很重要,你应该这样做,并且可以提出好处。如果你告诉我们“但是没有证据支持这些好处,为什么不停止呢?”我们可能会感到困惑和愤怒,更加激烈地试图捍卫我们的立场,尽管论点存在问题。

你不应该建造客户想要的东西

至少,你可能根本不应该这样做。大多数时候,他们不会代表大多数用户,甚至任何用户。这种情况几乎在软件的每个领域都会发生:

  • 内部软件通常由 IT 部门委托,但会被销售、工程师、财务和其他部门使用。

  • 商业软件通常由产品经理推动,但会卖给成千上万(或更多)的人。即使你有专门的客户代表,他们也只代表众多用户中的一个。而且,就像内部软件一样,这个“代表”可能也不是应用程序的最终用户。

  • 即使是为参与决策的小团队定制软件的情况,过多的建议也会来自资历较深或更健谈的用户;最糟糕的情况是,具体请求会在被总结并呈交给开发团队之前,先通过高级经理的理解进行筛选。

这意味着,在几乎所有情况下,客户想要的东西最多只是对产品最佳利益(因此是其用户群,以及可能还有你的底线)的一个粗略近似。管理这个问题的技巧当然是政治性的,而不是技术性的;你可能不想得罪那些正在向软件需求提供反馈的人,尤其是如果他们正在支付账单的话。这意味着要翻过波佐比特——c2.com/cgi/wiki?SetTheBozoBit是不可能的。但如果某个想法是糟糕的,你可能不希望它在你的应用程序中出现。

但是什么让你确信这是一个糟糕的想法?即使你是你正在编写的软件的用户,这也只是不太具有代表性的一个用户与另一个用户之间的比较。是的,你可能对平台规范和预期行为有更多的了解,但这也可能意味着你对全新的想法持保守态度,因为没有其他应用程序是这样工作的。

通过数据可以解决这个冲突。我在第六章,测试中讨论了 A/B 测试和用户验收测试;这些工具可以在这里用来发现任何给定的建议是否改善了软件。这不必很昂贵;在这方面,你不必在发现是否有人想要它之前构建整个功能。你可以在白板上尝试一个原型来查看人们如何使用它,或者构建该功能的非常基本的版本来查看它的受欢迎程度。尽管如此,在尝试通过调查用户来了解一个功能可能会多么受欢迎时要谨慎:回答“是”或“否”需要同样的努力,但在一种情况下,他们有更高的机会得到一个新玩意儿,无论他们是否会使用它。对特征调查的响应中的风险/回报计算倾向于肯定请求,而我们已经看到默认同意偏差意味着人们倾向于同意他们面前提出的任何陈述。

当你拥有数据时,对话可以开始“那是个不错的主意,但看起来客户还没有准备好接受它”,而不是“我不会构建你的糟糕功能。”这是一种与客户保持持续关系的更容易的方式。不幸的是,这并不总是可行的;许多软件仍然在秘密中构建,直到 1.0 版本几乎准备好(甚至更晚)才与用户互动。在这些情况下,你只有不完美的客户代理,而且不管你喜不喜欢,你只能根据他们的建议和你的意见来工作。你仍然可以围绕假设的其他用户(通常称为角色)来框架讨论,以减轻对具有挑战性的“个人”功能请求的情感反应,但这只是一种不完美的修辞工具,而不是不完美的需求工具。1.0 版本中的应用遥测可以告诉你人们如何真正使用功能,并帮助你优先考虑未来的开发,但对于关于初始发布的讨论来说已经太晚了;记住,是初始发布在花钱,而它没有自我支付。

软件系统的人类因素

关于软件需求的问题在于,它们不存在。或者至少,它们不是孤立存在的。粒子物理学的标准模型基于这样的想法,即存在被称为夸克的基本粒子,它们结合成称为强子(包括质子和中子的重粒子)和介子(在高温相互作用中重要的中等质量粒子)的系统。夸克通过携带强力的胶子绑定到这些系统中。这个模型被普遍接受,尽管没有人曾单独看到过夸克或胶子;它们总是强子或介子的组成部分。

正如夸克和胶子本身没有存在一样,没有用户的软件本身也是没有意义的,没有软件的用户也没有事情可做。整体代表一个社会技术系统,而我们正是通过我们的软件开发努力构建和修改这个系统。因此,没有对软件需求的观点是不完整的,没有对软件将对与之互动的人的政治、经济、社会结构和心理学产生的影响的看法,以及这些人将如何影响软件的看法。

我已经对这个观点有了理论上的理解好几年了。最终,在罗伯特·安内特twitter.com/robert_annett)关于遗产软件系统的演讲中,这个观点对我产生了情感上的共鸣。他讲述的一个轶事是,他走过他在部署新系统的公司办公室,与将要一起工作的人交谈。当他们离开大约有 20 个数据录入员工作的房间时,他的新同事低声说,“这真的很遗憾——当你的新系统上线时,我们不得不解雇他们。”

有时,你提供给编译器的符号和文字的模式会对真实的人产生真正的影响,无论是好是坏

经济学

这场互动的经济方面在 Barry Boehm 1981 年的著作《软件工程经济学》books.google.co.uk/books/about/Software_engineering_economics.html?id=VphQAAAAMAAJ&redir_esc=y中得到了很好的阐述。他对于估计软件项目成本的模型在业界并没有被普遍采用,但它确实包括了被他称为“人际关系因素”的内容,这些因素可以影响软件系统的成本和收益。它包括了与别人合作的“修改后的黄金法则”:

己所不欲,勿施于人——如果你和他们一样

条件句的目的在于提醒程序员,并不是每个人都希望被当作喜欢解决软件问题并能理解计算机科学概念的人来对待。Boehm 认为,在软件项目中,可用性、满足人类需求以及允许用户实现其潜能的需求,在经济术语上需要被考虑。

虽然这肯定比完全不考虑这些因素要好(或者至少更完整),但试图为它们找到美元价值只是考虑它们的一个早期阶段。我从其中,以及从信息安全和其他领域的类似论点(记得在第六章,测试中关于无障碍经济价值的讨论)中推断出,我们要么看不到,要么无法证明这些属性固有的好处,但我们仍然希望将它们纳入我们的决策中。我们不愿意忽视它们的事实,使我倾向于第二个解释:我们知道这些事物是有价值的,但我们没有论据来支持这一点。

这并不是说这些关于人的因素的保护措施没有用;只是它们并不是辩论的顶峰。你可以看到,从成本的角度来看,可用性在经济上可能是合理的;在设计可用的软件上投入更多努力,可以使用户更有效率,更满意。满意(与另一个因素——实现人类潜能——相关)可以导致对工作的更大参与和更高的员工留存率,从而降低组织的 HR 成本。满足人类需求是赫茨伯格www.businessballs.com/herzberg.htm认为的卫生因素:人们必须满足他们的基本需求,才能被激励去追求其他目标。

有时在目标之间的权衡无法合理地用经济术语来表示。一个很好的例子是游戏:如果它有很好的可用性,那么它将非常简单,人们会快速完成它然后回到工作中去——这是一个经济上的胜利。但人们不会玩简单的游戏;他们玩那些给他们提供挑战的游戏,无论这个挑战是心理上的、灵巧上的还是其他什么。因此,玩家想要挑战或沉浸于游戏世界的愿望优先于其他,尽管很难为这种愿望赋予货币价值。

政治

软件开发的政治面可能会影响人们认为自己在使用软件的系统以及更广泛的交互系统中所受到的认可、支持、赋权和价值。让我们从这个案例研究开始本节:一个在企业中使用的共享日历应用程序。在一个团队中,每个人都可以在自己的日历上安排事件,而经理可以看到所有人的日历。此外,经理还有一位个人助理,可以在经理的日历中为经理安排事件。

经理感到自己处于权力地位,因为他们可以看到每个人的位置,可以在报告应该在那里的时候,策略性地走过他们的办公桌,看看他们在忙什么,因为他们没有记录任何会议。此外,经理感到赋权,因为更新日历软件的机械工作已经委托给了别人,而委托是管理者的一项关键活动。

另一方面,团队的其他成员感到赋权,因为他们可以通过日历软件控制经理。如果他们不想被打扰,他们可以为自己创建一个“会议”并找到一个安静的地方工作。他们可以与个人助理合作,安排经理在他们想要进行团队讨论而不希望经理参与的时间参加会议。

关于日历软件的讨论依赖于使用日历的团队中的潜在政治模型:我写它是基于一个马克思主义模型,揭示了经理(扮演资本家角色)和工人之间的斗争。每个团队都代表着自己的目标,根据模型,这些目标不可避免地存在冲突。通过确保冲突的目标不会在单一问题上直接对立,从而实现稳定。

参与这个系统的人们是否真的参与了这个系统模型所呈现的冲突——以及个人参与者是否认识到这种冲突或对这个系统有不同的看法,这个模型并没有捕捉到。这是一个内部一致的叙述,它没有告诉我们关于其准确性或适用性的任何信息。

在设计供多个人使用的软件时,人和我们对其政治的模型的真实政治都会塑造软件促进的互动。软件会支持现有的权力分配,还是会以牺牲其他人为代价赋予某一群体权力?政治结构是在粗略的层面上建模的(如上例中的经理/工人案例)还是捕捉了系统中每个个体的不同需求和期望?软件会促进任何新的关系还是会破坏一些现有的关系?它会消除不平等,加强现有的不平等,还是会引入新的不平等?

这些问题是复杂的,但为了全面理解协作软件对其用户的影响,回答这些问题是必要的。正如本节前面提到的轶事所示,软件系统可以对真实的人产生实际影响:一家大企业的管理层在部署新软件后可能会很高兴减少员工人数,以收回开发成本,并认为那些被裁员的员工有责任找到替代工作。一个有责任通过提供工作来支持当地人民的慈善机构可能更愿意保留工人并拒绝软件。只有通过理解政治环境,你才能确保你的软件对你的潜在用户和客户来说是良好的社会适应。

优先考虑需求

这一节实际上重申了之前的内容:你应该优先构建用户需要的软件,而不是他们想要的软件。这是意识形态,无论如何。现实总是有这种讨厌的习惯,在这个时候插上一句“实际上”。

向买家推销他们真正需要的东西比推销他们想要的东西要容易得多。推销东西是一个很好的机会,因为它允许你资助其他活动:也许包括开发客户仍然需要的其他东西。但是,实际上...

...良好的营销努力可以说服客户,他们真正需要的东西是他们确实想要的。然后,你可以通过让人们应该购买的东西并说服他们购买它来跳过所有上述讨论。这是一种高风险、高回报的情况:是的,向人们推销一匹更快的马——blogs.hbr.org/cs/2011/08/henry_ford_never_said_the_fast.html更容易,但利润不会那么高,成功也不会那么持久,就像你发明汽车行业一样。正如他们所说,利润是承担风险的一种奖励。

那么,你如何优先考虑构建软件,这实际上取决于你能够承受的风险水平。你可以通过找到人们肯定愿意购买的东西并构建它们来获得渐进的低利润收益。这是精益创业方法,你从无到有,快速迭代,直到数据告诉你人们想要购买什么。或者你可以承担风险:构建你知道人们需要的东西,然后说服他们这值得花钱。这种方法与史蒂夫·乔布斯著名的立场最为相似:顾客不需要知道他们想要什么

这真的是“工程”吗?

有一句古老的格言说,任何需要包含“科学”这个词的东西都不是科学。是的,原文作者是在谈论计算机科学。但也许我们应该对将“工程”一词归因于需求工程持谨慎态度。毕竟,工程是将科学应用于制造实体的应用,而需求工程是将社会科学(社会学的警告又响了!)应用于改善社会系统的业务。实际上,它将某些社会科学领域(政治学、经济学、人类学、民族志和地理学)转变为其他社会科学领域(社会学和商业研究),并创建了一些软件来实现这种转变。(在我完成这一节不久之后,保罗·拉尔夫向 ArXiv 提交了一篇论文,描述了软件设计的理性和替代范式——arxiv.org/abs/1303.5938v1。理性范式基本上是基于直觉的需求工程版本:软件需求作为宇宙中的基本真理存在,可以通过仔细思考推导出来。替代范式是经验主义:需求是人们之间互动的结果,只能通过观察来理解。拉尔夫的论文很好地解释了这两种范式,并将它们置于软件设计历史背景中。)

这并不是说“需求工程”这个短语需要被淘汰,因为人们知道它的意思,并将其用作该学科真实含义的占位符。但也许我们需要将其视为一种代际现象;对我们来说,它被称为“需求工程”,我们记得在教别人的时候给它一个不同的术语;比如“社会软件”。

第十一章:第十章

学习

引言

当你开始做这些事情时,无论是写 iPhone 应用、UNIX 迷你计算机软件还是未来编程的任何形式,你不知道如何做;你必须学习。也许你参加了一个培训课程,或者获得了计算机科学学位。也许你读了一两本书。无论如何,你开始时一无所知,最后……有一些知识。

这还没有结束。正如刘易斯·卡罗尔所说:

你需要尽你所能奔跑,才能保持在原地。

他是在谈论红后赛跑,但我在谈论学习和个人发展。如果你在读完第一本书后就停止了,你可能作为初学者程序员来说是“OK”的,但如果图书馆旁边的那位女士又读了一本书,那么她就会领先一步。

我们生活在一个经常被称为知识经济的时代。弗朗西斯·培根说,“知识就是力量”。如果你不学习,不根据你所学的东西来提升自己,那么你将落后于那些人。你的教育就像红后的赛跑,不断地奔跑以保持在原地。

尽可能多做

没有所谓的学习过多(尽管“工作不足”的真正问题有时会在大量学习附近被发现)。并非所有的教育都来自正式的设置,如培训或大学课程。(实际上,许多大学计算机科学课程的材料现在可以通过 iTunes U 和 Coursera 等计划免费获得。这可以成为一些有趣的午餐阅读,但我发现当我有一个教授课程的框架和提交截止日期的压力时,我学得更好。话虽如此,你不是我,你可能从更轻松的学习环境中受益。)在午餐时间阅读一本书、杂志文章或博客文章可能会有很大帮助,参加鸟类的开发者会议也是如此。

像培训课程和会议这样的大项目显然需要更大的时间投入。显然,有“工作不足”这样的事情,这是你需要解决的问题。如果你是自雇的,那么你需要平衡机会成本(通过参加课程,你将放弃多少工作?)和财务成本与收益(通过参加课程,你会变得多好?你将能够完成多少额外的工作?你将在会议上遇到哪些有价值的联系人?)。

当然,如果你有工作,这个决定可能是由你的经理为你做出的。如果你知道培训课程如何与公司的方向相匹配,你可以帮助这个决定……但我将把这个留给第十二章和第十三章关于商业和团队合作的章节。

不要局限于自己的学科

每个领域都有其拥护者和超级英雄:那些有成千上万名追随者的人,他们的博客文章总是被阅读和引用,并在所有会议上发言。人们会向这些拥护者寻求分析和建议,以指导他们的社区如何运作。通常,一个领域的领导者会被与“他们”,即另一个领域的领导者进行对比:那个在 iPhone 上编程的人是“我们”中的一员,而在另一个房间里发表演讲的 Android 程序员是在与“他们”对话。

这种“我们”和“他们”的定义毫无意义。它需要保持足够的流动性,以便总能找到新的“他们”。回顾我的历史小角落,我可以看到一些随着时间的推移而来又去的区别:Cocoa 与 Carbon;CodeWarrior 与 Project Builder;Mach-O 与 CFM;iPhone 与 Android;Windows 与 Mac;UNIX 与 VMS;BSD 与 System V;SuSE 与 Red Hat;RPM 与 dpkg;KDE 与 GNOME;Java 与 Objective-C;浏览器与本地;点与括号。

有时候,需要从不同领域的一个想法来给你自己工作带来新的视角。例如,我发现通过倾听函数式编程社区的人,我得到了很多关于编写面向对象代码的新想法。你可能会发现相反的情况是真实的,或者通过倾听一些 C#程序员,你可能会找到编写 Java 代码的新方法。

你甚至可能会发现,暂时放下程序员的工作,去其他领域学习一段时间会激发你的灵感——或者至少让你放松,之后以全新的状态回到编码。关键是,如果你只关注自己的狭窄学科,而排除所有其他学科,你最终会排除掉很多聪明的人和想法。

将其付诸实践

在历史的各个阶段,我学过各种语言,包括人际和计算机编程语言。我唯一记得的只是那些我经常使用的。

我预计这对你来说也是一样的。我之所以预计这一点,并不是因为我相信每个人都像我一样,而是因为这在理论上有依据。科尔布学习周期——www.businessballs.com/kolblearningstyles.htm表明,有四个过程构成了学习的实践:

  • 具体经验:实际上做某件事。

  • 反思观察:分析你(或其他人)是如何做某件事的。

  • 抽象概念化:构建一个模型,说明事情应该如何做。

  • 积极实验:只是玩玩橡皮泥,看看会出来什么。

并非每个人都经历过这个循环中的所有项目,但大多数人都是从某个地方开始的,并至少通过几个点进行进步,可能按照展示的顺序(承认作为一个循环,它应该是循环的)。因此,几乎每个学到东西的人都会经历实验或构建经验:不尝试很难学到东西。

如果你不尝试,很难将你所学的知识适应到你所做的其他事情中。一个想法本身并不能真正做任何事情;当它付诸实践时,它就会与其他想法和技术相结合,增加一些有价值的东西。

协作并分享你所学的知识

分享你所学的东西有很多好处。首先,你分享给每个人的经历都不同,他们可以告诉你你所学的知识如何(或不如何)适用于他们的情境。这种洞察力可以给你一个更全面的了解你所学的知识,特别是它可能存在的局限性。会议演讲和书籍往往带有一种说服性的倾向——并不是因为作者在说谎,而是因为如果你离开后想要应用你所学的知识,材料会更有成功的机会。

听取那些在你想要做的事情在特定情况下是否有效(或无效)的人的意见,可以让你对某个概念及其应用有一个比仅仅依赖你最初发现的第一个来源更全面的了解。作为回报,你可能会向你交谈的人讲述你自己的经验和问题,这样你们双方都能学到东西。

这就是我热衷于共享学习的原因——每个人都受益。这包括如果你在一个正式的学习环境中,如培训课程或班级中合作,老师也会受益。即使你的经验比老师少得多,你也会有一些独特的见解和想法,这些见解和想法在公开场合比保持沉默更有用。

Communications of the ACM 这样的出版物经常涵盖与教学计算相关的问题。确实,在撰写时的当前期号中,两篇文章—[cacm.acm.org/magazines/2012/11/156579-learning-to-teach-computer-science/fulltext **articles**—http://cacm.acm.org/blogs/blog-cacm/156531-why-isnt-there-more-computer-science-in-us-high-schools/fulltext](http://cacm.acm.org/magazines/2012/11/156579-learning-to-teach-computer-science/fulltext articles—http://cacm.acm.org/blogs/blog-cacm/156531-why-isnt-there-more-computer-science-in-us-high-schools/fulltext) 讨论了计算机科学教学的短缺。我相信要解决这些问题,我们需要从新手而不是专家那里获得反馈(专家们设法通过了初始学习阶段——无论可用的资源多么糟糕)。我们需要更多地了解目前是什么让初学者难以取得进步,如果我们想要扩大行业规模,让新同事能够快速达到比我们做得更好的水平。

当然,如果新手在和我们交谈,那么倾听新手会是最有效的;具体来说,告诉我们在哪些方面做得好,哪些方面出了问题。鼓励这种做法的一个好方法是以身作则。不幸的是,这似乎并不受欢迎。在 Objective-C 编程的世界里,两个优秀的博客内容聚合器是 Cocoa 文献列表cocoalit.comiOS Dev Weeklyiosdevweekly.com/issues/。也许我只是变得过于挑剔,但似乎这两个网站上的大部分内容都是教程和指南。这些内容要么重复了第一方文档中涵盖的主题,要么展示了作者创建的一些包装类,而没有深入探讨达到那里的艰辛。

我们真正需要理解的是,无论是新手还是经验丰富的开发者,实际上更接近于 Stack Overflowwww.stackoverflow.com 的内容,而不是博客圈的内容。如果许多缺乏经验的程序员在解决如何让两个对象进行通信(而且确实有很多——stackoverflow.com/questions/6494055/set-object-in-another-class)的问题上有困难,那么也许面向对象编程(OOP)不是适合编程新手的范式;或者也许需要改变其教学方法。

因此,这是一个对那些想要提升编程领域的人的请求,让他们挖掘 Stack Overflow 和相关网站,以了解常见的问题——试图决定任何用户的经验水平可能会很困难,所以将问题组织成“新手问题”与“专家问题”将会很困难。这也是对那些在 Stack Overflow 上发帖有困难的人的请求。原因是什么?

  • 通常,在构思一个好问题的过程中,你最终也会弄清楚答案是什么。这种努力并没有浪费在 Stack Overflow 上;你可以在发布问题时回答自己的问题,然后每个人都可以看到问题和解决方案。

  • 声望系统(第一次近似)奖励好的问题和答案,所以你得到有用答案的机会很高。

  • 如上所述,可以挖掘这样的问题和答案。

当然,有缺点:

  • 重复的问题不容易衡量,因为它们通常会被关闭,并且经常被删除。或者人们会发现现有的问题覆盖了相同的内容(按照“规则”应该是这样),因此不会提出重复的问题。投票系统和查看次数必须用作问题“流行度”的代理;这是一个不精确的系统。

  • 投票系统往往奖励陈词滥调而不是新颖的想法或技术准确性;被点赞的答案是“受欢迎的”,但这并不意味着“正确”。

一个更好的编程教学系统应该基于所有编程课程中教师收到的所有反馈的总和。但我们不太可能得到这个。与此同时,Stack Overflow 相当不错。我的意思是,你不应该只分享你学到的知识,还应该分享你遇到的难题。

学习机会

那么,你的培训预算已经用完,你喜欢的会议在上个月举行,而且一年内不会再有;就这样吗?你什么时候还能有机会让自己进入学习状态?

一直如此。以下是我如何挤时间进行额外学习的一些例子:

  • 我每天通勤大约一个小时。这意味着每天有两个播客剧集,每周十个。

  • 每周一次,我的开发团队有“代码俱乐部”,这是一个小时的会议,其中一位成员做演示或引导讨论。其他所有人被邀请提问或分享他们的经验。

  • 午餐时间可以读一些文章。

  • 我每个月参加一到两个当地的开发者小组。

你不一定需要深入研究某些信息才能使用它。仅仅知道它存在,并且你可以再次找到它,就足以在你的思维抽屉中给它留一个位置。当你将来遇到相关问题时,你可能会记得你在这篇文章中读到过它,或者在 Evernote 中做了那个笔记。然后,你可以回去找到你需要的数据。

当然,会议和培训课程确实是学习大量知识的好地方。一个原因是你可以(在某种程度上)放下其他一切,专注于正在提供的内容。

虽然有些抱怨

在会议上看到的最令人难过的事情之一是有人在笔记本电脑上做工作,而不是专注于会议。他们错过了——不仅内容,还有与其他代表在下一个休息期间讨论的共享经验。由于噪音和投影图像,这不是一个良好的工作环境,而且他们也没有从会议中获得任何东西。

重新发现失落的知识

你可能会认为,随着软件领域的快速发展,我们现在所做的一切都是基于去年所做的一切,归纳证明有一个连续不断的历史将当前实践与 20 世纪 40 年代的“ENIAC 女孩”和巨像鸟联系起来。事实上,真相几乎正好相反;被视为过时的做法,与被综合到现代实践中的做法一样,可能会被拒绝和遗忘。

以编程为例,我分享自己的经历。我出生在微型计算机革命时期,第一代家用电脑。在这些机器上教授的编程基于 20 世纪 60 年代的 BASIC 语言或使用汇编器。结构化编程、面向对象编程、过程编程和函数式编程的进步都被忽视或被认为是不适合微编程的高级主题。直到很久以后,我才接触到“新”的概念,比如 1973 年的 C 语言,并不得不掌握任何形式的代码组织或模块化。

拥有一本关于计算机编程的历史书或当代文献集,很容易看出,我不是唯一一个忽视或失去学科早期成果的人。毕竟,敏捷编程的“自我组织团队”不就是对 Weinberg 的适应性编程的再发明——dl.acm.org/citation.cfm?id=61465?是否存在清晰的血统,或者这个概念已经被重新发明?用户体验的“新”领域真的与 Boehm 的软件工程经济学的“人际关系方面”——dl.acm.org/citation.cfm?id=944370有什么不同?正如第八章,文档中所述,许多开发者不再使用 UML;多久之后 UML 会被发明来取代它?

软件创作的教学

对于上述重新发现问题的缓解,可能是你付出巨大的努力,从近 70 年的文献中找出什么,识别相关部分,并从那里综合出一个关于软件创作的观点。那将是疯狂的。但在短期内,这可能是一条唯一的途径。

和许多人一样,我是通过实验、阅读不同质量的书籍和杂志来学习编程的。这意味着,和许多程序员一样,我的形成性经验并没有受到一致的编程教学法理论的指导(或者根据你的立场,是被污染的);实际上,我认为这样的理论并不存在。编程教学由专业培训师和大学系所进行,确实,同一所大学的不同系所也会以不同的方式教授编程(正如我在其中教授编程时发现的)。

没有一套一致的知识体系被应用或甚至被引用,不同的课程会教授非常不同的内容。我说的不是在语言习惯层面的差异,这种差异在所有类型的教学中都是真实的;你可能会从两位不同的老师那里学习同一种编程语言,并发现两组完全不同的概念集。

这与编程仅仅是一个解决问题的工具的想法是一致的;不同的课程会针对解决不同的问题而编写。但这意味着新手程序员之间没有共享的经验和知识集合;我们注定要在职业生涯的前几年重复别人的错误。

不幸的是,我并没有快速解决这个问题的方法:我所能做的就是让你意识到,在行业中很可能有大量的经验是你甚至没有能够加以利用的。你为了发现、理解和分享这些经验所付出的努力取决于你自己,但希望这一章已经说服了你,你与社区分享的知识越多,你的工作和整个社区的工作就会越好。

我所学习的特定材料在描述操作符的工作原理和如何使用语言的关键词方面内容丰富,但在组织、规划和可读性方面却显得不足(关于代码可读性的意义在第十一章,批判性分析中有论述);也就是说,关于编写代码之外的一切,以及编写可用的代码的一切。是的,我学会了如何使用 GOSUB,但不知道何时使用 GOSUB。

在这些编码的其他方面,有很多好的资料。例如,在组织方面,即使在自我学习编程的时候,也有书籍解释了这些内容,并且做得很好:《计算机程序的构造与解释》——mitpress.mit.edu/sicp/full-text/book/book.html面向对象编程:一种进化方法——books.google.co.uk/books/about/Object_oriented_programming.html?id=U8AgAQAAIAAJ&redir_esc=y面向对象软件构造——docs.eiffel.com/book/method/object-oriented-software-construction-2nd-edition。问题不是信息不存在,而是我不知道我需要学习这些内容。如果你愿意,这是一个未知的未知。

你可以争论说,代码的组织是一个中级或高级话题,超出了入门书籍或培训课程的范畴。或者你可以争论说,虽然它确实是初学者应该知道的东西,但将它与“这是如何使用+运算符”的内容放在同一本书中,会使事情看起来过于复杂,可能会让人望而却步。

首先,让我提出一个立场,即这两个说法都不正确。我通过类比罗杰·彭罗斯的书籍《现实之路》——books.google.co.uk/books/about/The_Road_to_Reality.html?id=ykV8cZxZ80MC 来进行论证,这本书从基本的数学(毕达哥拉斯定理、几何学等)开始,最终结束于量子引力和宇宙学。每一章都极具挑战性,比上一章更难,但只要理解了前面的内容,就可以理解。人们(包括我自己)都曾花费数年时间来研读这本书——在开始下一章之前,先完成每一章末尾的练习。然而,这仅仅是一本单本书,长度不超过 1,100 页。

对于计算来说,是否也可以做到同样的事情?是否可以有一本“虚拟现实之路”,将人们从编程入门带到软件创作的全面概述?我会这么说:这个领域的范围比理论物理学要小得多。

现在,这里有一个不同的论点。我会接受这个观点,即这个领域要么太大,要么太复杂,以至于无法全部放入一个地方,即使是对于有强烈动机的学习者来说也是如此。在这种情况下,需要的是一个课程:一个关于软件创作不同部分之间关系的指南,它们建立在其他部分之上,以及一个建议的学习顺序。

当然,这样的课程是存在的。在英国,A-level 计算机科学——www.cie.org.uk/qualifications/academic/uppersec/alevel/subject?assdef_id=738不仅教授编程,还教授如何识别可以用计算机解决的问题、设计并构建解决方案以及记录它。那么接下来该怎么做呢?能够估计构建解决方案的成本和风险会有所帮助;在由多个人构建的解决方案上工作;维护现有软件;测试拟议的解决方案……这些都是建立在所呈现主题之上的。它们由软件工程研究生课程——www.cs.ox.ac.uk/softeng/courses/subjects.html涵盖;在学习如何编程和作为专业程序员提高之间似乎存在某种差距,在那里你只能依靠自己。

这些课程仅是为授课课程设计的。难道自学成才的程序员应该被排除在外吗?(该领域的某些人可能会说,是的;编程应该是一门只有专业人士才能从事的专业学科——或者至少应该有一个只有知情者才能获得的指定头衔,就像任何人都可以成为营养师,但只有合格的人才能称自己为营养师一样。这些人中的一些人自称“软件工程师”,认为软件应该是一个专属的职业,就像工程学科一样;其他人则自称“软件工匠”,并以中世纪的行会作为他们追求专属性的模式。我将把对这些立场的评价留到以后。但现在,值得反思的是,对任何关于我们工作的描述都伴随着隐含的负担。)

关于编程的书籍系列有很多:例如,Kent Beck 签名系列——www.informit.com/imprint/series_detail.aspx?ser=2175138关于管理方法和测试方法,或者Spring Into——www.informit.com/imprint/series_detail.aspx?st=61172系列简短介绍。

这些已发表的系列通常集中在初学者水平或深入且专注于寻求特定任务信息的经验丰富的开发者。无论是通过某个出版商的编辑策划还是作为外部资源,从一种水平到另一种水平都没有明确的路线。尝试在网络上搜索“应该阅读哪些编程书籍”,你会为每个对这一主题发表过意见的程序员得到多个结果——就像 Jeff Atwood 多次写过的那样。

构建课程是件困难的事情——比列出你读过的书单,或者假装你读过,然后告诉人们他们必须读过这些书才能成为程序员还要困难。你需要决定哪些内容真正相关,哪些可以省略。你需要弄清楚不同的材料是否与一致的学习理论相符;从一本书中获得价值的人是否可以从另一本书中获得什么。你需要决定人们需要获得更多经验的地方,在继续前进之前需要尝试的事情,以及他们的课程告诉他们这样做是否合适。你需要接受不同的人以不同的方式学习,并准备好你的课程不会对每个人都有效的事实。

所有这些意味着,尽管有 45 年的系统计算机科学教育,但在软件教学的课程设置上仍然有多种课程的空间;帮助下一代程序员避免我们(以及我们之前和之前的人)所犯的错误的可能性是开放的;在本节开头描述的“英雄般的努力”需要重做,但只需要做几次。

反思性学习

许多高等教育机构推广反思性学习的概念,即通过内省和回顾分析你所学的,决定哪些做得好,哪些不好,并计划做出改变,以使好的部分胜过不好的部分。考虑到我们在本章中看到的情况——即有无数的信息来源,不同的人从不同的媒介中学习得很好,反思性学习是整理所有这些信息并决定哪些对你有效的好方法。

这绝不是一个新颖的想法。在他的书《计算机编程心理学》——www.geraldmweinberg.com/Site/Home.html中,Gerald M. Weinberg 描述了一些程序员如何从讲座、书籍和音频记录中学习得很好。有些人——正如我们在讨论 Kolb 循环时所看到的——想要从实验开始,而有些人则想要从理论开始。当他告诉我们尝试这些事情并发现哪些对我们最有益时,他实际上是在告诉我们要反思我们的学习经历,并利用这种反思来改进这些经历。

反思性学习也是从日常经验中汲取教训的好方法。我这里有一本小笔记本,大约 4 年前,我每天都会根据当天的工作写一段话。我会思考我遇到的问题,以及我是否可以采取任何措施来解决它们。我也会思考哪些事情做得很好,以及我是否可以从这些成功中提炼出一些普遍的东西。以下是一个例子:

将我们的代码审查流程委托给[同事]。我是否给了他足够的信息,并解释了为什么给他这个任务?在我的代码中发现了一个常见问题,由于向集合中插入 nil 而导致多次崩溃。在许多 ObjC 中, nil 对象可以像正常对象一样使用,但不能在集合中使用,而且我已经知道这一点。为什么我在编写代码时会忽略这一点?专注于确保未来的代码中处理失败条件,并在代码审查中获得帮助以查看它们。追逐一个与[产品]相关的问题,结果发现这是我已经在主干上修复了但没有集成到我的工作分支中的问题。我本可以做些什么来更早地识别这个问题?频繁地将主干上的修复集成到我的分支上本可以消除这个问题。

你不一定要把你的反思写下来,尽管我发现保持日记或博客确实能让我比完全的内省更有条理。从某种意义上说,这本书本身就是我的一种反思性学习练习。我在思考在我的编程生活中我不得不做的事情,这些事情并不是直接关于编写代码,而是在记录这些事情。在这个过程中,我决定有些事情值得进一步调查,了解更多关于它们的信息,并撰写关于这些发现的报告。

第十二章:第十一章

批判性分析

引言

在你的职业生涯中,人们会告诉你一些不真实的事情。有时它们是谎言,旨在操纵或欺骗;有时它们是说话者相信(或可能希望相信)的事情,但在仔细检查后并不符合标准;有时人们会告诉你一些确实是真实的事情,但与他们的立场无关或用途有限,以说服你接受他们的立场。

谁会告诉你这些事情?你可能已经想到了营销人员和销售人员,他们急于让你或你的公司购买他们的产品并赢得佣金。会议上的演讲者也可能这么做,试图说服你认为他们推广的技术、风格或策略不仅适用于他们的情况,也适用于你的情况。你想要尝试的新语言网站可能正在做出夸张的声明。你的经理或队友可能过于努力地试图说服你接受他们的思维方式。

也会有很多人告诉你一些确实是真实的事情。其中一些可能会令人惊讶,特别是如果常识——rationalwiki.org/wiki/Common_sense告诉你情况相反;其中一些可能会令人怀疑;有些你可能愿意不经过辩论就接受。尽管质疑事情的真实性——en.wikipedia.org/wiki/Truthiness没有害处,即使它们确实是真实的。

批判性分析是关于研究论证以确定其良好形成的过程。在这个背景下,“论证”是一系列陈述,它们肯定了某个特定的立场;它不是两个人之间的口头对抗。一个论证如果包含一些前提并通过这些前提逻辑地得出结论,就是良好形成的。请注意,这样的论证是“良好形成的”,而不是“正确的”:论证可能依赖于有争议的知识,或者前提可能因为其他原因而不合理。尽管如此,揭示论证中存在的修辞技巧和谬误(如果有),可以帮助你理解论者的立场以及他们为什么想要你同意他们的结论,同时也有助于你决定你是否可以同意那个结论。

批判性分析往往缺乏批判性

“你错了。”我希望即使没有本章其余部分提供的信息,也能清楚地看出本章开头的那句话不是一个很好的论证。一个结论的片段被提出(我错了,但错在哪里?这个论证?一切?两者之间?是我的立场错了,还是我在道德上令人厌恶?),但没有前提或推理。

批评通常意味着对某个问题表达负面意见,但在这里它并不是这个意思。短语“批判性分析”使用了更学术性的“批判”一词的定义。在这个语境中,批判意味着分析论点的利弊,从论点形成的特定规则来理解它,并发现你是否认为结论是令人满意的。可以在网上找到许多完全缺乏批判性的讨论;参与者在事先已经决定了自己的立场,并试图找到更有效地展示自己观点和削弱对方观点的方法。

面对这种回应可能会感到沮丧。阅读反论点没有任何价值;它不是批判性的,所以它没有告诉你为什么对方不同意你。尽管如此,人们很容易将这样的回应个人化(关于这一点,稍后还会讨论),并对这个人(或想象中的网络化名背后的人)感到沮丧或愤怒。正是这些因素导致少数博客作者关闭了他们网站的评论功能。提出一个没有经过深思熟虑或缺乏批判性的论点比提出一个经过深思熟虑的论点要容易,因此,互联网上大量的评论都是这种无益的类型。

请不要成为那个问题的参与者。阅读是为了理解,而不是为了回应。如果你还有问题,试着用理性的解释来阐述为什么所提出的论点没有讨论这些问题。如果在这样做之后仍然不清楚,那么请毫不犹豫地发布你的解释。你们两个人都可能从中学习到东西。

如何构建一个论点?

我不会描述如何分析一个论点,而是解释如何构建一个论点。因此,批判性分析是探索论点是否包含这里描述的高质量特征,并且这些特征能够连贯地支持论点的结论或结论。

虽然它们不必在论点的开头提出(实际上可能根本不明确),但任何论点都依赖于一系列的假设或前提。这些是为了探索论点主题而接受为真的陈述。任何假设的有效性取决于上下文;在任何领域,有些事情被认为是无可争议的知识,而其他则是有争议的。计算机领域无可争议的知识的一个例子可能是通用图灵机的特性和操作;这些事实在 20 世纪 30 年代被记录并数学上证明,并被普遍认为是在计算机科学其他部分建立的一个坚实的基础。假设“Java 是一种教授初学者的有用语言”将被视为有争议的知识,因为它远未得到普遍接受。

这样的有争议的假设需要由读者或听众可以评估的外部证据来支持,以决定假设的有效性。在学术领域,可接受的证据通常限于经过审查的文献。确实,可以找到支持前一段中提出的 Java 断言的论文。在学术论证中,无争议的知识大致等同于“在教科书中找到的陈述。”

几乎是同义反复,大学以外的软件创作不是一个学术学科。对学术的正式性依赖很小,而对个人或共享经验的依赖很大。因此,基于权威(“马丁·福勒说...”)或个人意见(“我发现...”)的论点常被用来证明有争议的知识。在后一种情况下,假设可以作为一个中间结论来提出:它遵循其自身的一套前提,然后被用作后续论证的输入。在批判性分析教科书中未使用的论点通常建立在一系列中间结论的基础上,结合其他前提以达到最终目标。

基于意见或经验的论点很容易被持有不同意见和经验的人削弱,尽管这并不特别有用。在这些基础被使用的地方,经验的范围和形成特定意见的原因应该作为达到所陈述结论的正当理由。

结论本身应该是基于从假设和中间结论进行推理后所采取的立场。也就是说,它应该与前提相关;如果你觉得需要通过引入无关的事实来混淆问题,那么你的论点就不是很强。从前提到结论的逻辑过程,尽管可能很复杂,但理想情况下应该是机械化的;信仰跳跃是不恰当的,任何横向或其他的狡猾步骤都应该明确。论文风格的论点通常期望通过演绎推理而不是归纳推理得出结论;例如,诉诸类比会被认为是不恰当的。从实际的角度来看,作为一个程序员,你更有可能检查的是一个销售提案或客户的请求,而不是学术论文,所以“规则”将会更加宽松。

结论不一定是论证展示的最后一部分。一些作家以结论开头,以挑战读者并让他们思考论证可能如何进行,这种技巧也用于口头论证的展示。偶尔,结论在论证的裸骨形式之后出现,然后提供进一步的支持以使结论更具说服力。在任何情况下,结论通常在论证的末尾被重申;毕竟,这是你希望读者或听众印象最深刻的部分。

谬误的形式

本节以某种目录的形式呈现。它不会是完整的,也不会采用正式的方法来描述目录,就像例如设计模式处理其目录那样;一个完整的谬误目录至少会与本书的其余部分一样长。一个正式且一致的目录需要规划。

后果论,因此原因论

翻译过来就是“在这之后,因此因为这个。”给定两个事件,X 和 Y,论点是这样的:

先是 X,然后是 Y。因此,Y 是由 X 引起的。

这是一种不一定成立的归纳推理形式。这里有一个荒谬的例子:

红灯亮了,车停了下来。红光子对汽车有很强的制动作用。

在这种情况下,可能存在因果关系,但它并不像论点所提出的那么直接。

基本归因错误

人 P 做了 X。因此,P 是个傻瓜。

这也被称为对应偏差。人们通常理解他们采取行动的情境基础,但将他人的行动归因于他们的固有性格。这里有一个对程序员来说很好的例子:如果我走捷径,那是因为我务实,因为当前项目/迭代/什么的压力。如果你要走同样的捷径,那是因为你不理解良好的软件开发原则。

从谬误中论证

这是一个微妙但很容易在网上讨论中找到的谬误:

论点 A 说 X 跟随 Y。论点 A 是谬误的。因此,X 不跟随 Y。

仅仅因为一个论点包含谬误,并不意味着其结论一定是错误的。考虑以下具体例子:

艾萨克·牛顿的万有引力定律表明,当我扔下一个物体时,它将因为重力这种力的作用而落到地球上。爱因斯坦表明,实际上重力是由时空的弯曲引起的。牛顿是错误的;因此,当我扔下物体时,物体不会落到地球上。

事实上,在牛顿的万有引力定律预测物体将落到地球上的几乎所有情况下,爱因斯坦的广义相对论也会预测;物体确实会落到地球上。即使它们预测的结果在某些情况下可能不成立,这两个模型也不会被使用,尽管它们给出结果的原因可能并不是实际发生的事情。

连续谬误

连续谬误是在在线辩论中经常遇到的谬误之一,尤其是在像 Twitter 这样的媒体上,任何陈述的长度都是有限的。这种谬误是断言一个论点不正确,因为它没有满足某种特定条件。回到万有引力理论的例子,对牛顿的连续谬误反论可能是:“牛顿的万有引力定律不能预测水星近日点的进动,因此牛顿定律的任何结果都没有价值。”实际上,在人类尺度的相互作用中,牛顿定律非常有价值;它给出了易于计算的合理准确的答案。爱因斯坦的理论更普遍,在人类尺度上与牛顿(和观察)一致,并且成功地预测了水星的运动。但牛顿的显著成就并不需要与洗澡水一起倒掉。

我有一个关于连续谬误在程序员讨论中普遍存在的理论:我们的编程活动训练我们寻找和覆盖边缘情况。计算机在程序员大多数时候使用它们的方式上,无法进行归纳推理。当与计算机打交道时,程序员必须寻找任何未讨论的情况,并明确陈述遇到这种情况的结果。这种训练可能导致人类互动中的连续谬误,其中程序员将同样的敏锐的边缘情况检测意识应用到其他人做出的陈述上,这些陈述隐含地限制了范围或依赖于归纳的正确性。

滑坡谬误

如果 X,那么 Y。Y,那么 Z。Z,那么狗和猫住在一起,大规模歇斯底里。

滑坡谬误是一种常见的修辞手法,用于削弱论点。如果构建得当,每个步骤看起来都似乎合理,尽管它们实际上代表了连续的连续谬误,或者是对实际提出的观点的微妙稻草人变体。最终结果将是荒谬的,或者至少在论者的心目中是高度不希望的。

循环论证

这个术语在批判性思维的术语中有特定的含义,与它作为“提出一个明显问题的论点”的日常用法是分开的。正式来说,如果一个论点通过接受结论作为隐含的假设而变得有效:X,因此 X,那么这个论点就是循环论证。神学论证有时会循环论证;考虑以下论证——rationalwiki.org/wiki/Begging_the_question):

世界的秩序和宏伟是上帝创造的证据。因此,上帝存在。

第一句话只有在假设上帝存在以创造“世界的秩序和宏伟”的情况下才是真实的;因此,这个论点仅仅是“上帝存在,因为上帝存在。”

倡导新奇

这也被称为“以新为奇”谬误,它认为新事物仅仅因为其新颖性就更好。在讨论哪种技术“更好”的讨论中,尤其是在供应商营销材料中,这是常见的:我们的产品比竞争对手的产品更新,这意味着它必须更好(这种谬误是从头到尾完全重写——blog.securemacprogramming.com/2013/04/on-rewriting-your-application/ 软件产品营销立场)。

构建绕过“以新为奇”谬误的问题不需要超过几秒钟的思考:只需想想什么实际上会使一个选项成为更好的选择。如果你需要关系型、基于表的存储,那么一个新的 NoSQL 数据库可能比 RDBMS 更差,尽管它更新,例如。

诉诸个人

更常见的是以其拉丁文名称为“argumentum ad hominem”,这种谬误采取以下形式:

P 说 X。P 是[一个白痴、共产主义者、法西斯主义者、臭的,或者一些其他不受欢迎的特性]。因此,不是 X。

除了人际间的争论,在软件架构的论证中,也常见到诉诸个人的论证。 “我们不使用那个,因为它来自[谷歌、苹果、微软、Apache 等等]”是科技行业的版本。

这个立场背后是否有实质内容?“我们不使用那个,因为它来自苹果,而苹果没有提供我们需要的支持条款”可能是一个好的论据。“我们不使用那个,因为它来自苹果,我不喜欢他们”可能就不是。

关于论据的进一步阅读

这是对论据批判性分析的一个简要概述,首先告诉你为什么我认为它很重要,然后提供一些关于涉及内容的信息。比我更有口才的作家们已经致力于这项任务,因此有很好的资源可以推荐,以进一步探索这个主题。

理性维基百科——rationalwiki.org/wiki/Main_Page 主要是一个用于揭露伪科学、怪异主张和新闻偏见展示的网站。它有一个关于逻辑、论据和谬误的全面部分。Less Wrong——lesswrong.com 有类似的范围,最后,怀疑论者 Stack Exchange——skeptics.stackexchange.com Q&A 网站展示了社区评分的论据,支持或反驳了不同主题的广泛立场。(反驳意味着构建一个针对提出的观点的论据;否认意味着在没有正当理由的情况下否认对立论据的真实性。然而,这两个词通常都用来表示否认,并且根据词语的历史意义,这本身也是一种偷换概念——en.wikipedia.org/wiki/Etymological_fallacy。)

辩论与程序员

在上一个部分结束时,我们提到了词源谬误的危险,现在是时候再进行一个“这个词有特定含义”的部分了。虽然辩论通常被理解为两个人或更多人就同一主题表达不同观点,但辩论通常有规则来规定论点的呈现形式,以及选择“胜者”或就主题达成共识的方法。

一个具有规则(而且我也熟悉)的辩论系统例子是牛津风格的辩论。辩论的主题由一个动议定义,形式为“本议院提议[X]”。观众投票决定他们是支持还是反对动议(或者他们可以选择弃权)。然后,两位演讲者或演讲团队分别提出论点,一个支持动议,一个反对动议。与论文风格的论点不同,修辞和情感诉求是演讲的重要组成部分。

在两个演讲之后,主持人引导观众提问的问答环节。之后,演讲者进行简短的总结演讲,然后观众再次投票。辩论“获胜”的一方是能够将投票转向有利于自己的一方(因此,如果辩论前 5%的观众反对动议,辩论后 7%的观众反对动议,尽管大多数观众弃权或支持动议,反对派仍可能获胜)。

在竞争性辩论中练习的技能当然是主要构建有说服力的论点,有趣的是,你可能需要辩论你不同意立场。这并不容易,但它确实导致了主题的深入探讨和对你不同意指定立场的原因的质疑。

如在《软件工程的幸运小矮人》——leanpub.com/leprechauns中所探讨的,许多编程实践基于民间知识(或常识),而这些知识最终证明其证据基础并不稳固。现在,我们知道从人机交互的研究中,一个足够满意——www.interaction-design.org/encyclopedia/satisficing.html的解决方案——一个不是最优但“足够好”以完成任务的方法——使我们能够完成我们的工作。质疑这些满足需求的软件构建方法,并尝试找到最优方法,难道不值得吗?

辩论会是这种质疑的良好载体,因为辩论中支持与反对动议的权重是相等的。有人将负责识别民间知识中的问题或弱点,并提出有力的论据来推翻它。作为一个思想实验,你能构建一个反对动议“本议院提议对所有软件项目强制实施版本控制——www.neverworkintheory.org/?p=457”的论点吗?

软件作为文章

记住,在第八章,文档中,我说过代码只能告诉你软件正在做什么;很难用它来解释它是如何做到的,而且没有一些辅助材料,就无法发现它是为什么这样做。你还得考虑在进行解释;理解书面文字、可执行代码或其他,是一个主观的过程,这取决于读者的技能和经验。

你可以想象一种解释形式,即对满足感的诉求:作者为谁写作,作品又是如何达到满足这些人的目的的?作者探索了哪些主题,作品又是如何达到传达这些主题的目标的?这些问题,直到现代文学理论的兴起,一直是文学批评分析文本的关键途径。

让我们把这些想法应用到编程中。我们发现,我们不是问我们的程序员“你能请写可读的代码吗?”而是问“你能考虑这段代码的主题和受众是什么,并以一种促进该受众中成员的主题的方式编写代码吗?”主题是你试图解决的问题,以及解决这些问题的限制。受众,嗯,就是受众;是那些随后必须阅读和理解代码的人的集合。这个群体可以被认为是有一定局限性的;正如编写不需要的功能的代码没有意义一样,为不会阅读的受众编写代码也没有意义。

我们还发现,我们不能再问这种听起来客观的问题:“这位程序员写的代码好吗?”也不能问:“这段代码可读吗?”相反,我们问:“这段代码是如何向其受众传达其主题的?”可读代码的标志不仅仅是代码的结构;它是代码被读者如何解释。这是否让读者信服作者的隐含论点:“这就是代码应该做的。”

因此,在结论中,一个合理的编写可读代码的方法要求作者和读者达成共识。作者必须决定谁会阅读代码,以及如何将这些重要信息传达给这些读者。读者必须从代码如何满足传达目标的角度来分析代码,而不是他们是否喜欢缩进策略或原则上不喜欢点。

源代码不是用人类可读的符号编写的软件。它是一篇用可执行符号写的文章。论点是,展示的代码是其问题的解决方案。但代码必须解决这个问题,并用连贯合理的解释证明这个解决方案是合理的。

第十三章:第十二章

商业

引言

这一章有点像罗马神祇雅努斯。雅努斯是天堂的门卫,拥有两张面孔。雅努斯其中一张面孔朝前看,另一张朝后看;他的名字幸存于一月——展望新的一年,回顾过去的一年。

这一章同样有两面:一面朝外看,从开发者视角看他们与之互动的商业;另一面朝内看,从商业视角看开发者。为了保持故事的吸引力,叙述在这些立场之间多次变换。

“但我是自雇的,”我听到一些人说。你仍然在进行商业活动。你可能需要向客户而不是经理证明你的工作,但概念是相同的。

即使你是级别很高的初级开发者,了解你所从事的商业以及你的工作如何融入公司的目标仍然是有益的。这种做法的原因始于悲观:你可能会被期望从“更大的图景”或战略视角看待你的工作以晋升薪酬等级。但这也将是有所帮助的;如果你知道业务压力是什么,你就能理解为什么管理层会提出他们提出的建议和规则。

如果你确实是一名初级程序员,这可能是帮助你职业发展的最大变化(而且,不幸的是,这是我通过艰难的方式学到的)。如果你只看到代码,那么你在管理决策中看到的只是阻碍你编写代码的人。他们实际上面临不同的压力、不同的输入和不同的经验,因此这些人提出不同的优先级并不令人惊讶。理解这一点是同情他们的立场并成为更有价值的团队成员的第一步。

冷静评估风险

在任何项目中,都可能会有出错的事情。你对这些事情的现实感如何?在你之前参与的项目中,哪些事情已经出了错?你考虑过这次这些事情出错的风险吗?你思考过如何控制导致它们出错的因素吗?

根据一些灾害响应领域的学者,在风险估计中有五个考虑因素,导致五种不同的风险管理错误方式:

  • 评估概率不正确(通常表现为乐观偏差——错误地认为没有什么会出错)

  • 影响评估不正确(再次,通常乐观地假设损害不会太大)

  • 统计忽视(在预测未来结果时忽略现有真实数据,通常是为了民间传说或其他可疑的启发式方法)

  • 解决方案忽视(没有考虑所有风险降低的选项,因此未能识别出最佳解决方案)

  • 外部风险忽视,即未能考虑可能对项目产生影响的直接范围之外的因素

项目风险

回忆起我制定并最终未能实现估计的经验,使我相信忽视风险比低估完成工作所需的时间更频繁地导致未能满足的进度预期。换句话说,“我以为这需要 3 天,但实际上花了 5 天”这种情况确实会发生,但发生的频率低于“我以为这需要 3 天,结果真的只用了 3 天,但我因为其他事情被从项目中移除了 2 天。”

例如,速度因子resources.collab.net/agile-101/agile-scrum-velocity基于证据的排程www.joelonsoftware.com/items/2007/10/26.html等技术试图通过比较估计完成时间和实际完成时间,并提供一个“调整因子”来乘以后续估计,来考虑这两种影响。

假设进度失败和外部中断都遵循泊松分布,那么这个调整因子应该是大致正确的(假设有无限的历史数据作为输入,这可能有些棘手)。但如果这个假设是有效的,那么你就可以构建一个泊松模型(例如,如 Spolsky 在上面的链接中所建议的,蒙特卡洛模拟)来预测项目将如何进行。

商业风险

在撰写本文时,Twitter 客户端作者的领域刚刚因为 Twitter 的《开发者道路规则》更新而发生了变化——developer.twitter.com/en/developer-terms/agreement-and-policy.html。公司正在限制任何应用程序在向 Twitter 讨论他们的案例之前可以拥有的客户端令牌数量。他们还强烈反对(就像他们之前所做的那样)标准的“Twitter 客户端”产品类别,建议开发者避免开发那种产品。

很明显,对于一个 Twitter 客户端应用来说,Twitter 是一个单点故障。实际上,在技术层面上,该服务偶尔还会出现短暂的故障。但在业务层面上,它也是一个单点故障——如果他们不想支持你想要做的事情,那么在他们的平台上实现的可能性很小。你无法指出你为他们支付了多少钱,并且会继续支付;该平台对开发者和用户都是免费的。

是否有公司或平台可能会像从你业务下撤走地毯一样?这种情况发生的可能性有多大,你将如何避免或应对这种情况?你的竞争对手有哪些,他们的行为如何影响你的计划?你的工作可能会侵犯哪些专利?而且,在云计算的今天,圣安德烈亚斯断层或大西洋飓风对你的业务有多重要?

运营风险

运营风险被定义为由于组织内部流程可能失败而产生的风险。也许您的数据恢复计划无法应对失去主要办公室的情况:这可能是运营风险。销售人员未能将客户信息存储在 CRM 中,导致错失跟进电话,从而失去销售机会,这也是一种运营风险。

当然,一定程度的运营风险是可以接受的。内部流程不会产生任何利润,因此过度美化它们意味着在较小的损失而不是更大的收入上投入大量成本。根据我的经验,这是公用事业和商品公司的最终目标,在这些公司中,犯错比任何外部因素都危险。随着运营风险的消除,内部流程将趋于僵化,随着价格的降低,这将从另一侧挤压利润空间。

管理决策不当也可以归类为运营风险,因为它们代表了某种内部流程。

其他外部风险

您的业务是否存在监管风险?许多软件公司,尤其是那些在社交网络和移动应用中的公司,开始发现加州在线隐私保护法案——www.pcworld.com/article/2018966/california-sues-delta-airlines-over-app-privacy-policy.html与他们的工作相关,即使他们不一定在加州(苹果和谷歌,他们的独家分销商,确实在加州)。

哪些规则或法律可能会改变,从而影响贵公司所做的事情?这些变化是否可能发生?您将如何修改您所做的事情以考虑新的法规,并且您是否为此做好了准备?

您的定价是否正确?如果您降低价格,您将增加单位销量;如果您提高价格,您将增加每笔销售的收益。哪条路线是最好的?您当前的价格是否最优?当时的 Black Pixel 公司(现在的 Black Pixel)的 Michael Jurewitz 写了一系列关于这个主题的详细文章,使用了一个特定的软件定价模型:

职业风险

你现在正在做的事情。是的,那件事。哎呀,不是那件事;另一件事。你会在 10 年后仍然做那件事吗?

这里有一个提示:答案可能“不是”。不是“肯定”不是,但可能是不是。以下是我 2002 年所做的事情:

  • 学习凝聚态物理和粒子物理

  • 等待桌面 Linux 的成功

  • 自学 Objective-C 编程

  • 使用 Perl 和 Pascal 编程

在这些中,只有 Objective-C 与我目前所做的事情真正相关;也许我一年只使用 Perl 一次或更少。(当然,许多人仍然在等待桌面 Linux,所以也许我只是太容易放弃了。)那么,你现在正在做的事情中,哪些在 10 年后仍然存在?哪些会与目前一样明显相同?

你对此有什么打算?当然,教育是解决方案的一部分;这本书中有一个“第十章,学习”章节。但这一节是关于冷静地评估风险,这也同样重要:把你在 10 年后想做的事情放在一边。当然,你可以在 2022 年专注于用 Objective-C 编写 iOS 应用,就像你可以在 2012 年专注于编写 Mac Carbon 软件或 Palm Treo 应用一样。

你现在正在做的事情在那时仍然重要吗?它会被其他东西取代吗?如果是这样,它会是来自同一个供应商的吗?即使它仍然存在,其中是否还有足够的钱让你自给自足?如果答案是否定的:在它发生之前你会做什么?

处理风险

可能会因为犹豫不决而变得瘫痪:在我们理解和控制公司可能面临的所有风险之前,我们应该继续前进吗?这种心态不会非常有利可图;本质上,利润可以看作是承担风险的回报。

你可以通过评估它们的预期成本来决定哪些风险是重要的。有些人用货币单位来做这件事(即,发生这种情况需要多少美元来恢复,乘以在给定时间段内发生的概率);其他人则用抽象单位,如高/中/低可能性和影响来做这件事。无论如何,结果都是按重要性排序的风险列表,并且很容易选择一个低点,低于这个点,你只需接受任何风险。

接受(或称为“忍受”)是一种处理风险的非常便宜的方式。其他方式包括:

  • 退出:通过拒绝参与风险活动来消除风险事件发生的可能性和影响。退出活动当然可以非常可靠地减轻任何风险,但也意味着没有获得与参与相关的奖励的可能性。

  • 转移:你可以选择将风险转移给另一方,通常需要付费:这基本上意味着购买保险。这不会影响我们风险事件发生的概率,但意味着有人对损害负责。

  • 对策:找到一些技术或流程方法来降低风险。这当然意味着限制其可能性或预期损害。但想想部署这些对策:你现在使你的业务或你的应用程序变得更加复杂。你是否引入了新的风险?你是否通过减少其他风险而增加了某些风险的可能损害?当然,你的对策是否具有成本效益?你不想花 1000 美元来节省 100 美元。

    个人经验

    由于主要经验是编写 iOS 应用程序,苹果当然是我工作中的单一故障点。如果他们以我无法适应的方式更改平台,我就倒霉了:但我不认为这种情况很可能会发生。他们在他们的开发者大会上展示(并说)他们计划在可预见的未来迭代他们的当前平台。

    另一方面,他们可以并且确实拒绝与他们的提交指南不一致的应用程序。我在这里的方法有两个:首先,通过规划与指南不一致不明显的应用程序来使用过程对策。其次,转移:我不出售我制作的任何软件,但我向那些自己承担将产品推向市场风险的人提供开发服务(无论是作为雇员、顾问还是承包商)。

了解你需要知道的内容,以及如何了解它

真事:我考虑以唐纳德·拉姆斯菲尔德的“这里有已知的已知”引言来开启这个段落。很快我就发现,他并不是第一个说这句话的人:这是伊本·亚明的说法,摘自拉姆斯菲尔德演讲的维基百科条目en.wikipedia.org/wiki/There_are_known_knowns

知道且知道自己知道的人……他的智慧之马将飞上天空。

知道但不知道自己知道的人……他正在沉睡,所以你应该叫醒他!

不知道但知道自己不知道的人……他的跛脚骡子最终会带他回家。

不知道且不知道自己不知道的人……他将永远迷失在无望的遗忘中!

问题是,如果我没有查过这个,我可能会自信地将这个想法归功于拉姆斯菲尔德。"啊哈",我想,“我展示了一个例子,当我实际上并不了解我在做什么时,我却在思考我知道我在做什么。这是经典的达温-克鲁格效应rationalwiki.org/wiki/Dunning-Kruger_effect。"

然后,当然,准备找到并引用他们的伊格诺贝尔奖获奖论文,我在链接的理性维基页面上看到了达尔文的引言:

无知往往比知识更能产生自信

在这些情况下,有了我想说的想法,只需一次网络搜索就能找到:

  • 一个(我可以验证)比我想象的更早的想法;这也起到了……

  • …这是一个提醒,我并不了解我所写的一切。

真的是那么容易从“认为自己知道自己在说什么”转变为“意识到自己知道的并不多。”这意味着最好假设你知道的并不多;尤其是,如果你面对的是一个你以前从未处理过的新挑战。

当我在规划时,我喜欢用像OmniOutlineriThoughts HD 这样的大纲工具花大约 5 分钟,只写下与当前问题相关的问题。即使这样一点点的努力也给了我后续研究的计划,以及遵循它的谦卑。

你发现的东西可能并不符合你的喜好

有时候,当你查看你所做的研究结果时,你会意识到事情并不乐观。

可能你的产品客户没有你最初假设的那么多,开发可能会更困难,或者你发现了一个你以前不知道的竞争产品。也许你估计的任务需要 2 天,实际上需要超过一周的时间。

你需要决定如何处理这个问题。处理这个问题的最糟糕的方式是忽视问题:你是在依赖运气或信念来度过难关。你比这聪明;你可以想出一个克服问题的计划。

个人经验

我参与过的第一个软件项目有一个由三名开发者组成的团队,他们中没有人有太多使用我们所使用技术的经验,其中两人(包括我自己)实际上对任何事情都没有太多经验。

随着我们项目的进展,我们发现每个构建的交付时间比我们计划的时间要长,而且构建中有很多错误。我们说服自己,我们可以在下一个构建中弥补这些不足,即使这意味着我们会落后于进度(这是我们尚未证明能够做到的),并且达到足够的质量(同样如此),并且找到时间修复所有现有的错误。

最终,我们的经理不得不阻止我们,指出我们每做一天的新工作,就会增加3天的错误修复工作。换句话说,如果我们不改变我们所做的事情,项目永远不会完成。

与我最近参与的一个项目相比,看看计划,很明显,软件的一些部分会对目标硬件造成重大的计算负担,而且确实存在这样的风险,即无法提供完整的功能,因为它无法以可接受的速度运行。

因此,我首先写了这些应用程序的部分,将标准数据处理部分留到后来。不出所料,在 3 周内,我们就发现了一些可观察的性能问题,我们无法继续下去,直到我们找到了解决问题的方法。

结果是,这导致进度推迟了一周多——换句话说,项目被延误了。但由于我们一开始就公开了这可能是潜在的问题,并尽早识别和解决,这种影响得到了处理,并没有造成太大的摩擦。

其他书籍已经涵盖了这一领域;寻找替代讨论的好地方是埃里克·莱斯(Eric Ries)的《精益创业》(The Lean Startup)第八章中的转向或坚持部分——theleanstartup.com/book

如何面试程序员?

我曾经作为技术职位的求职者和面试官,都经历过成功和失败(两方面:我自己的成功率目前大约是 2/3,其中我把离职后不到 2 个月的工作算作失败)。这并不是一个关于如何进行面试或被面试的终极指南,但它是一个很好的指南,告诉面试官他们应该寻找什么。因此,它也是一个很好的指南,告诉求职者可以假设面试官在寻找什么。多么元啊!

心中牢记目标

面试的目的是找出——通常只需要一个小时左右——你是否可以每天与谈话的人一起工作,而不会让其中任何一个人发疯。有些人把这句话表述为是否可以和面试者一起被困在机场 8 小时。想象一下,但这是接下来两年每周 5 天的情况。

了解他们的技术技能足够多,以判断他们是否在简历上撒谎。你不需要深入探究他们的技能深度;如果你认为他们的简历没有表达出足够的相关经验,使他们能够胜任这个角色,你就不会邀请他们来面试。

这种对人际交往的关注在实习生、毕业生和其他你不会期望申请者带来太多经验的职位面试中尤其明显。在这些情况下,你不能仅仅依靠技术问题来支撑面试——几乎可以说,最佳候选人可能无法提供很多答案。与其通过展示你比他们知道得多来让候选人感到不舒服,不如让他们感到舒适,并找出他们是否是值得一起工作的好人。

面试是为了双方而设

当我还是一名应届毕业生时,我假设求职面试的目的是穿上我最合身的西装(当时我唯一的西装)并说服其他人让我为他们工作。我现在知道这并不正确,尽管我仍然遇到这种行为的人。我觉得面试的真正目的可能还在保密中,或者职业服务部门认为人们不需要被告知这一点。

面试的真正目的是了解你是否想为那家公司工作。 面试官——很可能是你的同事或上司——是你愿意与之共事的人吗?这份工作实际上是你所宣传的吗?它仍然看起来有趣吗?他们提到了广告中没有提到的各种额外责任吗?你正在交谈的人对这家公司有热情吗?在你预想的待在那里的时间里,以及之后,业务会走向何方?你喜欢那个方向吗?你会得到你所需要的自由和/或监督吗?

如果你把面试当作你适应招聘者需求的广告,那么他们最终会得到一个实际上并没有在面试中声称那样工作的人,而你最终会得到一份你不太了解的工作。

如果你能避免假设性问题会怎样?

无论你在面试的哪一方,避免询问如果某些假设情况发生会发生什么的问题。答案总是相同的。

Q: 如果你有一个紧迫的截止日期,你会怎么做?

A: 任何完美的员工会做的事情。

Q: 如果同事来找你寻求帮助,你会怎么做?

A: 任何完美的员工会做的事情。

或者,从另一个角度:

Q: 如果谷歌进入这个业务,你会怎么做?

A: 任何完美的公司会做的事情。

Q: 如果你的主要竞争对手突然推出一个巨大的杀手级功能,你会怎么做?

A: 任何完美的公司会做的事情。

通过提出这些问题,你不会发现任何有用的信息。你会发现对方如何想象牛奶和蜂蜜之地;如果你对沙发心理学感兴趣,这可能很有趣,但最终并不能告诉你在这种情况下会发生什么。

而是尝试发现当这些情况发生时,实际上发生了什么。

Q: 你上次在紧迫的截止日期前做了什么?

A: 我对同事大喊大叫,告诉我的经理一切都很顺利,并在构建截止日期前一天辞职了。

Q: 当谷歌推出类似的产品时,公司是如何反应的?

A: 执行团队行使了他们的股票期权。然后他们关闭了研发部门,专注于我们的核心业务。

承认,这些答案并不像第一个那样令人愉快。但它们更具体,更能表明真实的行为,因此也更能表明如果这些情况再次出现,可能会发生什么。最终,这些是更好的发现。

不要试图证明对方是愚蠢的

如果你曾在会议演讲结束后的问答环节中经历过,你就会知道这吸引了一种特定类型的人。总会有一个问题——尽管措辞被改变以使其听起来像是一个问题——“我对这个比你知道得多。”这对会议的氛围是有害的:提问者看起来很无礼,回答者感到困惑,观众从互动中学不到任何东西——也许除了避免与提问者合作之外。

现在考虑一下,如果你在面试中设法让房间里的人都不想和你一起工作会发生什么。你可能会错过一份好工作或一个好候选人,这取决于你坐在桌子的哪一边。

团队中不同的人思考和知道不同的事情是完全可以接受的——甚至可以说是必要的。如果你团队里有两个人带来的知识和观点完全相同,那么其中一个人就是多余的。这是我的前经理告诉我的。不用说,我不同意这一点,以便保持就业能力。

你可能知道面试中的另一个人不知道的事情,但你不应该炫耀。相反,你应该发现当面对信息不足时,另一个人会做什么。关于与商业伙伴合作的这一主题将有更多内容。

个人经历

我大学毕业后第一份工作包括管理一个异构 Unix 网络。当我开始那份工作时,我向我的经理约翰询问关于面试的反馈(无论是否被提供工作,你都应该这样做)。

在我的经历中,面试是一种相当常见的设置:我坐在桌子的一边,对面有几个人(招聘经理、人力资源人员和技术专家)。一个“新颖”的特点是我们之间有一个 UNIX 工作站,它有两个显示器和两个键盘。

显然,我面试中让评审团印象深刻的一个特点是我被问到一个我不知道答案的问题。其他候选人也不知道;我不记得确切的问题,但它与按时间顺序获取文件系统上特定文件的硬链接列表有关。

当我说我不知道如何做的时候,技术专家本可以轻易地展示他比我更擅长 UNIX,但他没有。他和其他面试官一样,什么都没做。在我咕哝着思考的事情,汗水浸湿了衬衫领口的时候,我摆出了让我的面试难忘并帮助我得到这份工作的那一招。

我输入了 man ls 到终端。面试官可以看到我把 man ls 输入到终端,因为他们也有工作站上的显示器。他们可以看到我不知道我在做什么。他们也可以看到我想知道我在做什么,我知道如何解决这个问题。

在解决完问题之前,面试就结束了,但面试官发现了他们想要的东西,因为他们没有停下来指出他们有多聪明。

与你的商业伙伴保持透明和诚实

我说的“商业伙伴”不仅指与你做生意的人,甚至还包括你与之有合作伙伴关系的其他公司。我指的是你为了完成项目而与之共事的所有人:你应该把这些人当作平等的伙伴,你需要他们的帮助,他们也需要你的帮助来向客户交付优质的产品。如果你想得到他们的尊重、诚实和帮助,你需要对他们表示尊重、诚实和乐于助人。

目前,这听起来像是一种陈词滥调的非教训,在与其他人合作方面已经保持了几千年不变:“己所不欲,勿施于人。”但请耐心听我说;这其中的科学性远不止这些。

你是否曾经描述过某人,或者听到有人被描述为“一无所知”?他们“就是不明白”?你可能经历过对应效应——en.wikipedia.org/wiki/Fundamental_attribution_error,这是社会心理学中一个被广泛研究的现象。如果你做出了一个次优决策,那是因为截止日期的压力、为了满足他人需求而做出的妥协,以及其他局部问题。如果其他人做出了一个次优决策,那是因为他们是个白痴。有趣的是,他们认为自己的错误是由于他们所处的环境造成的,而你的错误是因为你是个白痴。

可能你们两个都不是白痴。可能你们都在尽力调和相互冲突的需求和压力。可能你们对下一步该做什么的意见不同,是因为你们对问题的了解不同,而不是因为其中一个是天才,另一个是笨蛋。

这就是为什么透明度是关键。如果你宣称“我们应该这么做”或者甚至“我们这么做”,而没有提供任何支持信息,你让对话中的其他人自由地得出你为什么这么想的结论——自由地犯错。或者,你可以这样说:“这是我看问题的方法,这是我们想要通过解决这个问题得到的结果,这是我们的解决方案。”现在,你的同事和合作伙伴对你所提出的方案为什么有信心不再有疑问,如果你仍然不同意,你已经为他们设定了如何表达自己观点的先例。对话可以集中在项目面临的问题上,而不是桌子周围的傻瓜们。

关于说服的补充

以这种方式构建论点是众所周知的修辞技巧。首先,人们会将自己识别为面临你所描述的问题,这样当描述解决方案的好处时,你的听众就会同意这将有所帮助。当你最终提出你的建议解决方案时,人们已经知道他们想要它了。南希·杜尔特在 TEDxEast 的演讲——www.duarte.com/presentation-skills-resources/nancys-talk-from-tedxeast-you-can-change-the-world/对这个主题进行了更深入的探讨。

当然,人们可能仍然会不同意你的结论以及你得出这些结论的原因。倾听他们的论点。询问为什么(如果他们还没有告诉你)。记住,这是软件开发,而不是高中辩论俱乐部:你“赢”是通过创造一个满足业务问题的优秀产品,而不是确保你的论点被所有人接受。

选择适当的技术

如前一小节最后一句话所说,这里的目的是满足业务需求。如果这个业务需求与你的当前首选平台或闪亮的玩具不一致,你必须决定你想要追求哪个。对业务来说最好的选择——因此,作为开发者在其中的角色——是那种以最少的努力和最少的困难或妥协来实现目标的选择。

当然,“努力”可以包括培训——一种短期的(通常是固定长度)努力,需要让开发人员和其他人掌握一些新技术。但“努力”也包括维护和支持——在产品整个生命周期中逐渐积累的持续成本。在估算项目成本时,有时会忽略这一点,因为项目在发货日期结束,所以维护是别人的问题。但这是一种错误的经济行为;项目的维护成本在很大程度上是项目团队的问题。

顺便说一句:机会成本

没有对“机会成本”的衡量,成本分析就不完整。一项活动的机会成本可以被视为回答“如果我们做这件事,我们会错过什么?”这个问题。

因此,一项活动的实际成本包括直接成本(所需设备、人员等)和机会成本;还可能有其他被认为是“成本”的负面方面。

反过来,这种好处不仅仅包括从销售中获得的金钱。它还可以包括“防御性收入”——由于你计划进行的工作,现有客户没有转向竞争对手。其他好处可能包括改善声誉或市场地位。

这一切似乎都是经济学 101 的内容,但对于整天与精确数字打交道的人来说,记住全面成本/收益分析并不仅仅是简单地从收入中减去钱是很重要的。

将机会成本纳入维护工作会产生多重打击。有解决问题的直接成本;有开发团队的机会成本,因为你必须在你修复维护问题时承担较少的新项目工作;然后还有客户的机会成本,他们失去时间来绕过问题并部署维护修复。我不会引用维护修复要贵多少我以前犯过这个错误——blog.securemacprogramming.com/2012/09/an-apology-to-readers-of-test-driven-ios-development/

另一种看待这种技术选择考虑的方法是,我从乔纳森“狼”伦茨施那里听到的一个引用,尽管他肯定不是原始来源:

你编写的所有代码都是一种负债,而不是资产

在 MSDN 上埃里克·李的博客中有关于这个引用的良好讨论——blogs.msdn.com/b/elee/archive/2009/03/11/source-code-is-a-liability-not-an-asset.aspx。如果你的首选平台意味着需要编写比使用其他平台更多的代码来解决同样的问题,那么选择该平台就意味着承担更大的责任。我们有一个短语来描述已经解决的问题却还要进行工作的问题,我们主要在其他人犯这种错误时使用这个短语:Not Invented Here

所有这些都意味着,在某种程度上,在设计软件系统并选择将用于其中的技术时,你必须暂时放下个人偏好。但这并不是坏事;这是一个学习更多知识的机会。

操作与灵感

在上一节中,我提到了修辞和说服。在任何协作活动中,这些都是在恰当使用时是伟大的工具,而在其他所有时候都是危险的武器。

我对商业问题的看法都是基于长期游戏的哲学。这就是为什么我主张选择正确的技术,即使这意味着重新培训或为了其他原因在短期内放慢速度。如果最终结果是更好的产品,你将拥有一个更满意的团队,更快乐的客户,以及更多的重复业务和推荐。

与说服相关,长期游戏体现在激励人们跟随你的道路,而不是操纵他们去做你想要的事情。区别是什么?对我来说,灵感是向人们展示你提出的方案是最好的选择。操纵是贬义词,说服人们尽管对他们或其他人有缺点,也要遵循某些路线。

操纵往往意味着承担被“抓住”的风险,因为你的目标发现了你诡计背后的真正原因或它所包含的陷阱。然后你不得不隐藏或淡化这些问题,直到你的操纵性例子变得与做正确的事情一样或更昂贵。

工作示例:应用商店销售

让我们通过考察在移动应用商店中销售应用的情况,来探讨操纵与灵感之间的区别。

销售应用的通常做法是在初次购买时收取一次性费用,就像盒装软件一样。这可能会暗示一种操纵性的销售方法;我们希望让某人购买应用,但对他们之后的行为并不关心。事实上,软件的更新通常是免费分发的,所以我们可能更喜欢他们在购买后不再使用软件。

那么,操纵性的做法可能是投入最少的力量去构建产品,而将更多的精力投入到产品页面上对其功能和优势的描述。人们会看到我们对应用的溢美之词,并且会很乐意为此付费。等到他们意识到产品页面上的谎言,而应用并不像我们所说的那么好时,我们也不会在乎,因为我们已经得到了他们的钱。

上述方法的一个大问题是它只能维持一段时间。从长远来看,客户会互相交流。他们会阅读商店和其他网站上应用的评论,并发现应用并不像我们所说的那么好。我们将不得不说服评论者写出关于应用的正面评价。

我们可以尝试贿赂他们(“虚假草根支持”——创建虚假草根支持的做法),但这并不容易扩展;此外,它仍然留下了不受我们控制的“诚实”评论被发布的可能性。

另一种获得好评的方法是创造一个优质的产品。如果人们真正喜欢使用这个产品,那么他们会很乐意告诉其他人它有多好。现在,我们的受害者^W 潜在客户将看到其他快乐客户留下的评论,并想要分一杯羹。成功!我们已经巧妙地让人们购买了我们的应用,而我们所需要做的只是...制作一个出色的应用。

这种短期与长期利益之间的紧张关系不仅仅出现在与客户的互动中——事实上,它会在生活的方方面面发生。最好是将你的权衡恒温器调整到长期利益的一端,因为(希望!)你的职业生涯将远远超过你当前的项目。

正是出于这样的考虑,我再次回到本节的主题:重视灵感而非操纵。你不想在你迎接下一个挑战时,你的声誉是基于人们在回忆与你合作这一项目时所体验到的苦涩滋味。

但你需要人们从你的角度看待项目和其挑战,你确实需要其他人的帮助来完成所有工作。这就是灵感的来源。灵感实际上应该是激发其他人,而不是说服他们。如果你想要做的事情对所有人都有益,那么不需要任何特别的技巧来让其他人也想做同样的事情。如何做到这一点,最好的办法是留给第十三章,团队合作

你不需要成为创始人就能成为一名程序员

感谢 Rob Rhynetwitter.com/capttaco Saul Moratwitter.com/casademora 在他们 NSBrief 采访中启发了这个部分。

作为软件开发者,我们现在是一个很好的时代,可以开始自己的业务。一个人,与几个自由职业者合作,使用一些在线服务,可以在几个月内推出一个应用程序,几乎不需要任何支出。有很多关于程序员放弃为“老板”工作,转而“独立”工作——换句话说,成立自己的软件公司的成功故事。

关于确认偏见的补充说明

小企业创始人有很多成功故事的一个原因是,失败者没有被邀请去发表演讲。事实上,有些人声称10 个新企业中有 9 个在一年内失败——www.gardnerbusiness.com/failures.htm,这个数字得到了我的会计师的证实。因为这不是新闻,也不有趣,所以没有被报道;因此,我们只听到成功的故事。关于偏见和谬误的更多内容可以在第十一章,批判性分析中找到。

但这并不是适合每个人。有些人(包括我自己)更喜欢让别人去找客户和做市场营销,然后专注于编写软件。不仅仅是工资开发有更大的吸引力。一家管理良好的公司可以提供一个更有结构的环境,有明确的目标和奖励。有些人喜欢经营企业的混乱,而有些人则希望看到成为更好的程序员的可观察进步。

在许多国家,对于想要为公司编写软件的人来说,有很多工作机会。即使是在最近的衰退期间,人们仍然在招聘程序员。对于那些不想自己创业的人来说,机会和回报都是存在的。

我的故事

我已经从事这项开发工作有一段时间了。我为大公司和中小企业工作过,作为承包商自雇,还经营过自己的咨询公司。这两项独立业务都是“成功的”,因为我赚到了足够的钱来维持我的生活方式,并且获得了新的和重复的业务。一旦尝到了独立生活的滋味,为什么我又回去了?

至少部分原因是因为上面解释的结构。我喜欢知道对我有什么期望,并努力提高自己以符合这个标准。我发现经营自己的生意,目标(至少在第一年,也就是我每次都达到的程度)过于短期:要么找到客户,要么完成他们的项目。我只是不知道自己做得足够好,可以设定并朝着长期目标努力。

另一个原因是,我太合群了。我真的很享受与他人一起工作——我尝试过的独立工作都涉及大量的在家工作或远程办公,这并没有提供我需要的与人接触。当你自雇时,你可以雇佣其他人或在共享环境中租用办公空间来解决这个问题。在我所在的城市,我负担不起这样做。

所以,如果你想开始自己的生意,那很酷——你应该尝试一下。祝你好运!但如果你不这么做,或者你尝试了但发现它不适合你,那也没有问题。许多人(包括我自己)都乐于成为职业程序员。

第十四章:第十三章

团队合作

引言

除非我在过去一百多页中完全失败了,你应该有这种感觉,软件是一种社会活动。我们与其他人合作来制作软件,我们作为软件制作者的共享价值观塑造了我们制作的软件。我们(或出售)我们的软件给其他人使用,这塑造了他们看待自己和彼此合作的方式。软件可以加强现有的联系或创造新的联系,但它也可以破坏或降低现有联系的重要性。从专业角度来看,我们的软件影响最接近我们编写代码时经验的联系是与我们每天互动的团队。

本章讨论了这些关系:我们如何作为一个团队工作,我们的同事如何与我们合作,以及可能出现的利益和紧张关系。

专注与干扰

我们已经听到了响亮的号召。我们听到了程序员需要进入“状态”——www.joelonsoftware.com/articles/fog0000000068.html,以便完成他们最好的工作,而且进入状态很难。我们听到了一个简单的电话或朋友的聊天就可以让我们退出状态,但重新进入可能需要 15 分钟。那么为什么每个人都不在家工作呢?如果周围有人对生产力是如此有害,那么为什么任何企业甚至要考虑办公室的资本支出呢?

因为,虽然好人可以独立工作得很好,但两个人一起工作可以很棒。让我描述一下我写这段话之前的一天。早上我并没有真正完成多少工作,因为一位同事问他正在工作的代码中的内存泄漏问题,我帮助了他。这比他自己解决得更快。

因此,午餐前我真正进入“状态”的时间只有大约一个小时,而且并不顺利。我在我的问题上取得了一些进展,但后来遇到了一个问题,一个简单的更改来添加新行为破坏了已经存在的东西。我无法找出原因。无论如何,那时是午餐时间,所以我们去买了三明治,我把问题告诉了我的同事。在我们到达三明治店之前,我们已经商定了问题以及我应该采取什么措施来解决它,当我回到我的桌子时,这第一次就成功了。

这个故事的要点是,如果我们俩都留在“状态”中,我们无疑可以工作得更快:直到我们无法单独解决问题为止。我们会在工作中更有效地失败。实际上,有一起工作的可能性让我们汇聚了知识,尽管这意味着我们中的每个人都可能在某个时刻被带出“状态”。

我在各种各样的环境中工作过。在我第一份工作中,我有一个自己的办公室——尽管其中一堵墙是电梯井,整个办公室都在地下。(事实上,这个办公室之前是负责维护我负责的系统之前几代计算机的现场数字设备公司工程师的宿舍。要么他们是非常沉睡的睡眠者,要么他们在夜班期间关掉了电梯。)从那时起,我在格子间空间、开放式空间以及在我的家中工作过。我知道“心流状态”是什么样的:但我也知道当你无法解决问题且没有人可以询问时,头撞墙的感觉。我知道连续 10 小时专注于一个真正有趣的问题是什么感觉,以及当 5 分钟后被告知公司已经有了你可以使用的解决方案时是什么感觉。我知道角落里的吉他是如何召唤你,以及从附近几个全神贯注且充满动力的人身上汲取能量的感觉。

事实上,“心流状态”并不总是相关的,正如上面案例所示。你可能想要进入“心流状态”来从书籍或互联网上做一些研究,但那时可能有助于从其他人那里获取一些输入,以比较他们的经验和观点与你所学的内容。在编码时,“心流状态”是有帮助的,但前提是你知道或能弄清楚你应该做什么。如果问题有任何难度,与其他人讨论将更有帮助。

最后一点:人类是“一个高度社会化的物种”——thoughteconomics.com/,进入“心流状态”的最佳环境——在家工作或在隐蔽区域——却是最不适合与同事共享社交体验的。我们中的一些人足够幸运,能够在工作之外与朋友或家人互动来满足我们的社交需求,但对于那些重视持续社交接触的更外向的人来说,独自工作可能会对心理健康产生负面影响。

因此,独自一人在有利于独立工作的环境中工作有时是有用的,但可能会缺乏情感上的刺激。与他人一起工作可能会带来回报和益处,但也可能令人分心并感到沮丧。我们如何平衡这两个方面?常用的方法之一是“耳机规则”。耳机戴在头上:我在集中注意力。耳机摘下:请随意与我交谈。耳机规则的变体是生产力鸭——www.youtube.com/watch?v=oBw_cKdnUgw&index=11&list=PLKMpKKmHd2SvY9DLg_Lozb06M2MLcNImz&t=38s

在我的经验中,强制执行戴耳机或降低生产力规则是困难的:无论耳机状态如何,耳机两边的人都会觉得忽略朋友和同事是不礼貌的。改变整个团队的社会规则可能很困难。我工作过的团队提出了一条更简单的规则,更容易合作:如果我在办公室,那么我来这里是为了与每个人交谈并完成一些协作工作。如果我在其他地方(食堂、会议室、在家、附近的咖啡馆),那么我是在独自完成工作。

需要平衡的地方因人而异;因此,团队采取的最佳方法取决于团队的人员构成。外向的人可能希望花更多的时间与他人合作,因此,如果想要不受打扰地工作的人待在家里,这会让他们感到孤立。

我遇到的一种更通用的区域相关技术是基于非常轻量级的计时跟踪。这需要设置一个厨房计时器(或者一个应用程序——或者,如果你在酒店并且喜欢打扰接待员,可以设置闹钟)为 25 分钟。在这 25 分钟内,专注于你的问题。如果有人需要帮助,询问你能否在时间结束后再回复他们。在 25 分钟结束时,短暂休息,回答任何出现的干扰,并计划进入下一个 25 分钟。如果你绝对需要做其他事情,建议你中断(而不是暂停)工作周期,并在有机会时重新开始。

关于这种技术的一个重要观察是,如果你在休息或帮助他人时不在计时范围内,这是完全可以接受的:这都是你工作日的重要部分。你可能有些日子只能管理一到两个 25 分钟的爆发,但至少你可以控制“区域”和其他所有你必须做的事情之间的权衡。

我只使用了这种技术一段时间,但我发现它确实有助于提高专注力。最初,我对 25 分钟的时间段可以持续多久感到惊讶!当我写这一点时,这似乎很荒谬,但它表明我允许社交网络等干扰分散我的注意力有多严重。

即使是 25 分钟的专注也需要你环境(下一节的主题)和工具的支持。我开发的一个 iPad 应用程序只能在连接的 iPad 上测试,因为模拟器没有提供第三方库。构建和运行或启动任何单元测试需要大约 30 秒——对我来说,这足以让我被电子邮件或 Twitter 分散注意力。我还发现,我的自律能力在午餐后下降;我仍然能完成工作,但我更有可能在休息时间继续工作或在中途停下来。

工作环境

你与同事的互动只是构成你整个工作环境的大量经验和输入的一部分。不出所料,最好的环境与个人化程度不亚于在独立工作和团队合作之间的最佳平衡;我能做的最好的事情就是描述对我有效的方法,以及你可以考虑的一些事情来反思你自己的环境。

首先,如果你工作的地方期望有一个“标准”的桌面布局,没有任何装饰或个性化,那根本不是一个非常健康的环境。人们喜欢装饰他们的环境以表达他们的个性——www.colorado.edu/cmci/academics/communication。一个同质化的工作空间可能有助于确保设施经理的木兰油漆不会弄脏,但不允许员工有任何创造性自由。限制我们软件制作者的创造力不利于制作出有创造性的软件。

《人件》1999 年版——books.google.co.uk/books/about/Peopleware.html?id=eA9PAAAAMAAJ&redir_esc=y 中有很多关于工作条件的内容。我进入职场太晚,没能亲眼看到他们抱怨的全封闭式格子间(尽管我当然看过 《tron》和《办公室空间》),但他们讨论的办公室设置的其他方面仍然相关。

我工作过的一些地方有那些巨大的VoIP桌面电话,上面有所有按钮、重定向选项、开关等等,这些是《星际迷航》中首次引入到人机交互中的。我早期的发现是,没有人知道如何操作这些电话,这意味着如果你需要,你可以对你的任何行为都有合理的否认。在网上找到手册,找到你需要的一个静音/重定向按钮,然后平静地工作。当有人抱怨他们试图给你打电话时:

  1. 为你在尝试将电话重定向到手机时按错按钮道歉。

  2. 建议电子邮件或其他异步通信是联系你的更好方式。

对我来说,工作环境中的两个重要特征是书架和白板。即使我在家工作,我也有一块白板和一支记号笔,可以随时进行快速绘图——一个相当小的,如果需要,我可以把它举到 Skype 摄像头前。没有白板可能会对整个工作空间产生不利影响。我工作过的一个办公室只在会议室里有白板,所以我们抓起干粉笔在(便宜、白色、纤维板)书架上画图。我们很快发现墨水很难清洗;但既然已经毁了书架,就没有理由回头了。随着新的“艺术”在无法擦除的旧东西上绘制,图解很快变成了褪色的手稿。

我之前提到我的第一个办公室是在地下的。一项关于自然光对建筑物使用者 影响 的文献综述——indoorenvironment.org/effects-of-natural-light-on-building-occupants/ 发现,人们在有自然光的环境中感觉更好,因此表现也更好。这个结果不仅适用于工人;学生甚至购物者也会受到影响。正如德马尔科和利斯特观察到的,没有理由建造一个工作环境,让一些人无法看到窗户。认为不可能给每个人一个窗户的人需要看看酒店是如何设计的。

优先处理工作

大多数制作软件的人在任何时候都会同时处理多项工作。选择可能是在一个项目中的不同任务,不同项目中的任务,以及其他工作,如准备演示文稿、回复电子邮件等。

有些人喜欢将这些任务全部记录在一个大的审查系统中,例如 GTD (www.davidco.com/),这样他们就可以在任何时候审查当前环境中的待办任务,并选择一个来工作。我曾在 Sophos (www.sophos.com) 的人力资源部门学到一个更简单的方法,他们是从艾森豪威尔总统那里学到的,就是画四个象限来表示任务的紧急性和重要性。

![图 13.1:艾森豪威尔矩阵图片 B15099_13_01.jpg

图 13.1:艾森豪威尔矩阵

现在考虑目前挂起的任务,并将它们放入这些象限中。位于右上象限的任务既重要又紧急,所以可能需要尽快完成。那些重要但不紧急的任务现在还不需要做,而那些紧急但不重要的任务则根本不需要做——至少不是由你来做。

程序员的大量工作实际上是由其他代理优先处理的,这意味着在大多数情况下,你接下来应该做什么是明确的。在本章的后面部分,我们将探讨一些旨在让整个团队决定他们正在做什么的软件开发方法。 (在我对现状的罕见愤怒中,我将它们称为“软件项目管理模式”,而不是“方法论”。这个词在许多语境中与“范式”一起使用,以至于变得模糊不清。我看到芭芭拉·利斯科夫在一次反思她关于数据类型工作的演讲中使用了“方法论”一词,意思是整体软件设计方法:因此,面向对象、结构化、过程化等都是“方法论”,同时瀑布、敏捷等也是如此。)

告诉专家需要做什么

换句话说,不要告诉专家们要做什么。其中一些更愤世嫉俗的人会按照你说的去做。这听起来并不那么糟糕,直到你意识到他们是专家,并且是出于怨恨而这样做。尼尔·斯蒂芬森在他的小说《密码经济》中扩展了这个想法——books.google.co.uk/books/about/Cryptonomicon.html?id=Lw-00wTgBy8C&redir_esc=y

他对这些军官讲话时的极端正式方式隐含着重要的含义:先生,您的问题是决定让我做什么,而我的问题是去完成它。我的热情态度表明,一旦您下达命令,我就不会打扰您任何细节——而您的一半承诺是您最好待在您的这一边,先生,不要用您为了生计必须处理的任何鸡毛蒜皮的政治问题来打扰我。下属毫不犹豫地愿意服从命令,这种不言而喻的责任压在军官的肩上,对于任何有半点头脑的军官来说都是一种沉重的负担,沙夫特已经不止一次看到经验丰富的士官仅仅通过站在他们面前,愉快地同意执行他们的命令,就把新任中尉吓得发抖。

而不仅仅是虚构的军事人物会谈论这一点。乔治·S·巴顿将军:

永远不要告诉人们如何做事。告诉他们要做什么,他们会用他们的独创性让你惊讶。

这有两面性。一方面,其他人可能比你更了解你要求的事情:好吧,你是一位优秀的程序员,但说到图形设计、用户交互、文档、翻译、市场营销或构建软件所需的其他任何事情,可能有人能做得比你更好。你的最佳选择是找到其中之一,概述目标,然后让他们去做。

另一个需要注意的问题是,让别人去做事情比接管控制要容易得多,因为涉及的工作要少得多。一旦你进入《密码经济》中描述的军官-海军关系,你必须下达每一个命令,因为这将成为互动中预期的一部分,即你会下达每一个命令。直接说“这是需要解决的问题”并让擅长解决这个问题的某人承担责任,这样更快,而且结果更好。

因此,委托的核心技巧是放弃对所委托任务的控制。这意味着要学习的基本事情是信任;与其说是信任其他人不会搞砸,不如说是信任自己能够找到不会搞砸的人,并且能够清楚地向他们传达问题。

与初级程序员合作

经验较少的程序员只是专家的一个特殊案例——他们是正在培训中的专家。上述关于处理专家的规则同样适用,所以让他们知道需要做什么,确保你清楚地做了,然后让他们继续做。

阐明方面是需要最仔细检查的部分。记住第十章,学习,你会发现不同的人以不同的方式学习。有些人通过实验来接近新问题,有些人通过阅读关于概念。图表对许多学习者有很大帮助。在我的经验中,作为团队的高级开发者,重要的是不要无意中颠倒专家-客户关系,因为那样你就会回到微观管理的情况。让初级程序员提问;实际上,鼓励他们提问,但扮演先知而不是讲师的角色。

当他们在工作时,让自己保持可用,当他们完成任务后,进行一点回顾。尽量避开“我不会那样做”——这只是一个对显而易见之事的冒犯性陈述。无论对方的经验或技能如何,你都会以不同的方式去做。关键问题不是他们是否完成了你的工作,而是他们是否完成了好的工作。找出那些有效的事情,并指出它们。询问为什么初级程序员那样做,并强化那些决定。找出那些不有效的事情,询问为什么他们那样做,并讨论问题。专注于问题;不要谈论工作的质量(或工人的正直)。作为教师,你的角色是帮助学习者建立一个关于他们想要做的事情的通用心理模型。

关于不同学习方式的简短评论:我曾经发现,肢体语言能给你一些关于人们如何思考他们问题的极好线索。我正在和一个初级程序员一起工作,感觉我并没有很好地沟通。这个程序员会解决我提出的问题,但经常遗漏一些细节或者不是按照我(认为)描述的方式解决。

然后,在一次讨论中,我有所领悟。当我描述一个软件系统时,我是从空间上描述的:我的手画方框和圆形,并将这些形状移动来表示软件中的消息或数据流。当另一个程序员描述它们时,他的手保持平坦,从上到下移动,当他解释步骤时。我在画图;他在听列表!我转而通过列出功能来解释问题,发现细节在我们之间不再消失了。

与经理合作

对于“经理”这个词在职场环境中的解释有两种(几乎是相反的)观点。保守的观点认为经理是负责一群人的控制力量。这个经理的位置被视为确保他们的下属完成业务期望的工作,并且通过扩展,不去做任何意外的事情。

自由主义的观点是,经理是领导者或赋能者。这个经理的角色是确保他们的下属拥有完成工作所需的资源,不受业务其他部分(或其客户和供应商等)可能强加的干扰。

在本节中,我将暂时放下政治模型,讨论与你要汇报的人一起工作的总体思路。如果你是一名自雇程序员,你没有一个明确的经理。你可能会时不时地发现,某些人扮演着类似的角色;我知道一些独立开发者会聘请“商业导师”来担任指导和咨询的角色。在某些情况下,你作为顾问或承包商工作的社会结构可能要求你向你所服务的公司中的特定人员汇报。“经理”这个词可以简称为所有这些人。

回顾一下我关于与专家一起工作的说法:你应该告诉他们需要做什么,而不是如何去做。你应该期待你的经理知道并理解这一点,当然,作为专业人士,你应该表现出自己的专业素养。实际上,经理的任务是一个适配器。一方面,他们把业务目标和战略转化为战术——现在是能够做的事情,以改善战略地位。另一方面,他们把你的问题和担忧转化为业务可以采取的措施来缓解或消除这些问题。

我合作过的最佳经理似乎能够做好那部分与上下文无关的工作。这关乎指导;不是承担你工作的困难部分。通过用他们自己的问题来回应我们提出的每一个问题,他们迫使我们进行自我反思,诊断我们自己的问题,提出建议,并评估我们自己的解决方案。他们不会免除我们解决自己问题的责任,即使他们在某些时候接受(并拥有)实施解决方案的责任(和权威)。

这种看似与上下文无关的管理可能并不完全现实。在一项由大约 80 名开发者完成的问卷调查中,杰里米·莱比希——arxiv.org/abs/1303.2646v1 发现,开发者认为如果经理来自技术背景而不是商业背景,他们与经理的关系会更容易。

我的假设是这是一个沟通问题,因此它影响了管理的第一部分(将企业的需求转化为我们需要做的事情)。每个团体都有自己的特定语言,自己的行话和俚语。计算领域当然不陌生于此(我想象一个 Ubuntu 论坛和 Mumsnet 对“僵尸儿童”等讨论的反应会非常不同)。当你沉浸在其中时,这可能很难看到,但这种语言创造了社会不平等:理解行话的圈内人和不理解行话的局外人。如果你的管理者不是圈内人,那么你们可能都会在寻找解释事物和识别它们的方式上遇到困难,并在某种程度上将它们视为“不是我们中的一员”。

第九章,需求工程中,我确定了使用通用语言如何帮助每个人理解你所编写的软件是如何解决客户需要的问题的。现在,我们发现通用语言也有政治上的好处:它使你们所有人都成为同一个团队的一员。(我在一家公司工作过,该公司通过制定项目代号成功创建了员工隔阂。通常,这些名字都是一些有趣的小名字,既可以用简短的方式描述整个项目,又能让每个人都感到归属感,就像拥有一个团队名称一样。在这个例子中,公司没有提供一份所有项目名称及其内容的中央词汇表。这加强了公司内部“我们和他们”的感觉:你要么是知道“梅洛”意味着什么的精英团体成员,要么就不是。)找到与你的管理者在语言上的共同基础是你的责任,即使你拥有计算机科学背景,而他们有 MBA 学位。你会发现这样更容易记住你们都在同一个团队。

关于与管理者合作的一些最终思考,这实际上关乎职业道德:在过去的几年里,我发现管理者不喜欢听到坏消息。但如果他们没有听到坏消息,而是后来自己发现,那就更糟了。诚实地提醒人们早期出现的问题会导致一些尴尬的对话,但最终,你将比假装一切顺利直到灾难时刻到来时更受尊重。我不应该让自己在这个问题上积累太多经验,但我确实有了,所以你不必。

软件项目管理模式

在过去的五十年里,提出了许多不同的软件项目运行方式,并得到了实践。在过去的十年里,我接触到了其中的一些。哪些对你有效取决于你合作的团队和你为谁工作的人的期望。

水坝式(瀑布模型)

我第一次经历“死亡行军”是在一个瀑布式项目中。产品经理编写了一份文档,说明了新产品的需求。这些需求按照 1-3 的优先级排序(1 表示“我们可能会按时完成这些”,2-3 则占据页面空间)。然后,主开发人员编写了一份功能规范,解释了产品中的每个控件将是什么,以及每个控件将如何满足第一份文档中的需求。

在功能规范的基础上,主开发人员(不一定是之前提到的那位)会估算构建所需的时间,而主测试人员会估算测试所需的时间。然后,发货日期就是那项工作结束后的第二天!在构建和测试完产品后,文档编写人员可以编写手册,翻译人员可以翻译所有内容,然后进行 beta 测试,最后,市场营销人员可以编写新的网站,所有这些都会在办公室的中庭举行啤酒和小吃活动。

我应该强调,死亡行军并不是遵循瀑布流程的结果。死亡行军是经验不足的团队、沟通和协作不佳,以及对业务或客户认为产品应该是什么的愿景不明确的结果。

瀑布流程确实使得对这些问题的反应变得更加困难。在项目通常运行中有限的可见性意味着大多数参与人员都有一个理想化的项目应该如何进展的观点,并将此视为现实。他们没有看到项目实际进展的情况,因为这种反馈既没有被要求也没有被提供:等你准备好进入测试阶段再回来。涉及高级管理人员签字的昂贵变更控制程序,这些管理人员通常不参与项目的日常运行,这使得对最后一刻的反馈做出反应变得困难,甚至不受欢迎。不幸的是,第十二个小时与第十二个小时的相似度远大于与第一个小时的相似度。

第五章,编码实践测试驱动 iOS 开发部分,我试图将瀑布流程描绘成一种历史上的怪癖,对现代开发者没有任何相关性。这并不完全正确。如果你在做合同或代理工作,客户通常会形成一个类似的心理模型:

  1. 我告诉你我想要的应用。

  2. 你构建这个应用。

  3. 也许我们每周都会通一次电话,这样我知道你还活着。如果你给我一个原型,我可能会建议移动一个按钮或更改一个单词。

  4. 你把应用放到商店里。

  5. 我退隐到一个被可卡因浸泡的山顶复合体。

你可以消除那个神话。事实上,你可能应该这样做:如果你从客户那里得到更多反馈,他们会感到更加投入,并且享受这个过程。他们最终也会得到他们想要的产品,而不是几个月前他们要求的产品。而且如果你要求客户反馈,他们会给出那个反馈,而不是关于按钮和单词的反馈。

敏捷开发(Scrum)

我看到多个项目以多种方式运行,都被称为“Scrum”,这就是为什么我把这些称为模式而不是规则。它们大多数都有以下共同点:

  • 短迭代长度,只计划即将到来的迭代的工作

  • 对整个团队就当前迭代的工作进展进行频繁反馈

  • 在迭代结束时对上一迭代工作的接受或拒绝

  • 对上一迭代中可以从中学习的内容进行某种形式的回顾

这些事情本身并没有争议,看看我上面提到的瀑布项目中的问题,我们可以看到频繁反馈、质量测量以及尤其是尽快从我们的错误中学习的益处。但实施往往让人摸不着头脑或更新他们的简历。

以“频繁反馈”这一点为例。这通常体现在站立会议中。每个人都真的站起来了吗?如果有人迟到,我们是等待还是不等待他们继续?需要多长时间(我的记录是一个半小时,在一个有 16 名开发者的团队中,显然每个人只花了 5 分钟)?我实际上需要知道会议中出现的每一件事吗?你为什么每天都问我是否完成了你告诉我需要一周才能完成的事情?(实际上,这是我的错。我认为如果估计代表超过半天的工作量,那么估计就没有价值。如果我认为某件事需要超过那个时间,那么我可能不知道涉及的内容,应该在开始依赖我的猜测之前弄清楚。)会议记录了吗?如果我想对某事进行澄清,我现在问还是在我们解散后再问?

问题是,尽管在方法上存在这些差异,但事情往往会真正发生。事情会完成,你可以看到它们正在完成,因为你对每个人在做什么有感觉。我倾向于认为 Scrum 是你能得到的与敏捷软件开发www.agilemanifesto.org/最接近的东西——在一个仍然希望有紧密管理监督的组织中,尽管在我遇到的大多数情况下,它并不完全符合原则www.agilemanifesto.org/principles.html

精益软件开发

精益软件开发并不是真正运行软件项目的方法,而是一种用一些日语词汇描述组织软件项目的原则,这些词汇被用来帮助销售 MBA 教科书。确实,它是上面提到的 12 个敏捷原则之一:

简单性——最大化未完成工作量的艺术——是至关重要的。

这就是精益的全部内容(加上教科书)。专注于做有价值的事情(解决客户的问题),而不是做无价值的事情。找出你正在做的事情没有价值,然后停止做。

有趣的是,可能是因为我们喜欢这样做,我们有时会忘记编写软件本身并没有价值。是的,已经编写好的软件是有价值的,但实际编写它是要花钱的。也许我们应该更多地关注软件的重用,甚至找到客户可以使用而不是新定制产品的现有事物。推广精益理念的社区已经创建了五个原则www.lean.org/WhatsLean/Principles.cfm

  • 识别对客户的价值

  • 消除业务链中任何没有增加价值的步骤

  • 创建剩余步骤的顺畅流程,最终将价值交付给客户

  • 每个步骤应从上游步骤按需获取其有价值的输入

  • 对上述内容进行迭代

到目前为止,这听起来还算合理,尽管我知道我(以及我想象中的很多人)觉得这听起来有点过于商业化-MBA 风格。这就是危险所在。这个价值集合实际上处于一个适当的抽象层次,而且是我们自己过于关注我们目前所做的事情,而不是它是否有用。如果你尝试用编写代码的术语重新表述上述内容,你会得到类似以下的东西:

  • 识别编写代码的价值

  • 消除那些阻止我们编写代码的会议和其他事情

  • 编写大量自动化内容,以便代码能够自动交付给链中的下一个人

  • 管理一个看板板en.wikipedia.org/wiki/Kanban_board

  • 对上述内容进行迭代

这对于提高编写代码的效率很有用,这几乎肯定会让开发者更快乐,并逐步改进流程。但这并不能帮助确定编写代码是否是最有价值的事情去做;事实上,它反而阻碍了这一点。

锚定偏差和项目管理

关于本章运行软件项目的最后一思。上一节解释说,如果我们过多地从我们已做的事情的角度考虑一个流程,那么质疑这样做是否值得就会变得困难。结果发现,思考事物还有其他问题与之相关——我不是建议任何人停止思考。

在决策过程中有一个叫做锚定的因素—www.skepdic.com/anchoring.html,其中人们倾向于在做出后续判断时固定在早期呈现的信息上。锚定是电视购物广告在告诉你价格是 10 美元之前先问“你愿意为这个支付多少?100 美元?”的原因。你可能不会期望价格是 100 美元,但它给你提供了一个锚定,这将设定你进一步的期望。

与此相关的是社会锚定——dictionary-psychology.com/index.php?a=term&d=Dictionary+of+psychology&t=Social+anchoring因素。人们倾向于以群体的方式投票。有一个很好的演示,由所罗门·阿希(1951 年)设计——www.simplypsychology.org/asch-conformity.html。要求“参与者”判断三条线中哪一条最长;前七个都是托儿,他们都选择了错误的答案。阿希发现,只有 25%的(实际)参与者从未服从群体并给出了错误的答案。

这是一个真正的问题,因为我认为它还没有被研究:这些锚定偏见对软件项目有什么影响,我们可以做些什么来纠正它们?给人们提供线框或其他原型是否会锚定他们的期望,使他们渴望类似原型的产品?像规划扑克这样的游戏是否会无意中将估计锚定到第一个揭示的人所想的数字?我们是否可能在会议中讨论无关的数字时无意中引入偏见(“我希望我们能在 45 分钟内完成这项工作……现在,这个功能有多少故事点”)?

偏见偏见

一个不幸的现象是偏见盲点——dataspace.princeton.edu/jspui/handle/88435/dsp013j333232r,在这个现象中,我们更愿意报告他人推理中的偏见,而不是我们自己的。关注诸如上述锚定偏见等认知偏见的问题在于,意识到偏见后,我们现在处于一个可以识别他人依赖偏见的地位,并相信我们因为了解它而免受其害。这并不正确。意识到它并不能阻止我们应用偏见:分析、检测和纠正我们工作和决策中的偏见才能做到这一点。这本书中有一个关于批判性分析的章节,即第十一章。

谈判

你需要与其他人进行协商。好吧,如果你在销售一个消费型应用,你可能不需要与你的客户协商:你设定一个价格,他们要么支付,要么去别处。但这并不意味着协商仅限于与恐怖分子和绑匪打交道的人。你可能想要说服你的团队其他成员,重写某些组件是值得的,或者你想要构建的功能应该纳入产品中。你可能想要向你的经理请求更多的责任。也许你希望供应商修复他们软件中的错误,或者供应商给你一个优惠折扣。在任何这些情况下,你都需要进行协商。(我在牛津美国词典中查看了“negotiate”的词源。显然,它来自拉丁语“otium”,意为闲暇,所以“neg-otium”是“非闲暇”或换句话说,是商业。这与本书无关,但它真的很有趣,所以我想要分享它。)

在协商中失败的一个确定方法就是忽视对方的立场。所以,你想要时间用FluffyMochaGerbilScript重写那个服务器组件,而你的经理说不行。这是因为你经理是个笨蛋,根本不懂吗?难道只有你能看到世界的真实面貌吗?

不。这又是基本归因错误(参考第十二章,商业)。这是一个常见的问题,但如果你发现自己认为你在和一个白痴说话,那么你很可能只是在和一个有不同问题要解决的人说话。也许他们担心重写会引入回归:你能做什么来证明这不会发生?也许他们知道公司很快将承担一些额外的工作,而你认为你可以用来重写的时间实际上并不存在。

最可靠的方法是询问对方的顾虑是什么,因为基本归因错误是双向的。当你认为他们根本不懂干净的代码或工艺或本周的热门词汇时,他们可能正在想,你不懂这是一个需要赚钱的商业,不能支持一个紧张的开发者的异想天开。你们两个(或更多)中的任何一个都需要通过分享你所知道的信息并询问对方知道什么来打破僵局。这可能就是你。

我发现第一次讨论很容易过于情绪化,尤其是当它是我已经工作了几个月的项目的变更,并且疲惫已经降临的时候。对我来说,最好的做法是休息一下,思考我们如何能够达成妥协,稍后再回到讨论中。仅仅内省并思考对方的立场有助于和解,但同理心差距——en.wikipedia.org/wiki/Empathy_gap意味着这并不是万无一失的。我可能认为对方是理性的,低估了情绪因素在他们的决策中的重要性。但是等等,我退出了对话,因为我变得过于情绪化。很可能是对方在做决策时也基于直觉因素。

同理心

前一节清楚地说明了成功的谈判在很大程度上依赖于同理心:能够看到你正在与之交谈的人的驱动力,并确定如何以解决他们的担忧、满足他们的需求和欲望的方式提出你的解决方案。让我们更深入地看看这是如何工作的。

情绪对协作的影响

你可能可以描述你的情绪如何影响你与他人的工作方式。我知道当我变得烦躁时,我重视独处,会对打扰我的人大声吠叫,试图避免进入对话。这可能意味着我更不可能倾听其他意见,要么有意义的参与讨论,要么学习如何更好地完成自己的工作。我宁愿在这种情况下自己犯错,也不愿接受帮助。

在《ACM 通讯》的一篇文章中,名为“情绪”——cacm.acm.org/magazines/2012/12/157887-moods/fulltext,彼得·J·登宁探讨了情绪如何影响我们之间的互动,甚至在社会团队的成员之间传递情绪。他指出,当每个人都保持积极时,合作变得容易;当每个人都保持消极时,结果很可能是糟糕的,因此最好避免可能演变成对抗的情况。

当人们处于混合情绪时,结果最难预测。消极的人或人们是否会利用他人的乐观情绪,还是会对此感到怨恨?你如何最好地帮助改善消极人的情绪?

在群体的整体情绪中存在一些高级模式。布鲁斯·塔克曼描述了团队建立过程中的四个发展阶段:

  • 形成:团队尚未存在;它是一群个体的集合。每个人都在寻求接受,因此团队不会解决任何重大或分裂的问题。人们大部分时间都是独立工作的。

  • 冲突:团队成员的个人偏好和意见发生冲突。团队了解到成员之间的差异和相似之处,哪些是他们愿意接受的,哪些会引起问题。团队开始发现愿意被引导的方向和方式。

  • 规范:通过一系列的协议和妥协,团队决定如何解决冲突,确定目标,以及如何朝着这些目标努力。

  • 执行:在共同商定团队规范后,团队成员在团队框架内工作的效率更高。

你可以通过一个人使用的语言来判断他们的情绪。丹宁专栏中的一个例子是询问团队成员为什么他们认为最近的产品发布受到了负面评价。有一个人表现出好奇和惊奇:

...我非常想采访我们的客户,找出他们反应背后的原因。我相信我会学到一些有助于改进我们软件的东西。

其他迹象表明困惑和怨恨:

我也不知道到底发生了什么。但我知道那些客户很糟糕...

语言提示可以提供关于某人情绪的信息,这可以指导你如何与他们互动。

语言与内向

语言也能告诉你一个人的性格。心理学家在评估个性时,常常会根据一个人是内向还是外向来划分。内向的人从独处中获得能量,觉得与人互动会感到疲惫或压力过大。外向的人从与他人相处中获得能量。

内向的人使用更多具体的短语——www.bps-research-digest.blogspot.co.uk/2012/11/introverts-use-more-concrete-language.html,比外向的人更少使用抽象语言。在描述人物互动的照片时,内向的人更可能坚持事实(“这位女士正指向右边,嘴巴张开”),而外向的人更可能推断照片所描绘的原因(“这位女士很生气,正在对这位男士大喊大叫”)。

能够检测到某人性格的特征,对于与他们产生共鸣大有裨益,因为你可以开始预测他们可能对某些情况或事件有何反应。语言是这一过程的工具;而且与心理测量测试(如迈尔斯-布里格斯类型指标)相比,人们更容易参与其中。

知道何时交谈和何时倾听

因此,外向的人比内向的人更可能使用抽象的语言,但还有其他原因可能导致人们在不同的抽象层次上讨论问题。你需要记住这些,以便从与团队的互动中获得最大收益。

你正在与之交谈的人扮演着什么角色?如果你在与另一位程序员讨论一个错误,那么你看到的奇怪现象——属性在视图中被设置,但由于被不同的线程重置而没有更新——是完全相关的。这可能会引起你同事的兴趣,他们会有相关的经验可以提供,并且他们想了解问题是什么,以防将来看到类似的情况。

如果你正在与客户公司的商务发展经理交谈,他们可能并不那么感兴趣。当然,这并不一定正确……但可能性很大。他们可能更关心的是错误是否已经修复,他们何时会收到修复,以及错误是否会影响产品的其他部分。

作为对你的一种礼貌,商务发展经理可能不会深入讨论他们与你们公司之间的合同细节以及关于你的老板每年必须给他们传真公司商业保险政策副本的棘手条款。他们期望得到同样的礼貌。同样,你的客户想知道为什么他们可能想购买你的产品,而不是你如何完全用SuperFluffyAwesomeSquirrelNode重写了它。

即使在与同行开发者的讨论中,也有时候细节很重要,有时候则不然。正如我们所见,你的同事的情绪可能会影响他们的接受度:也许在他们感到冷漠或认命的时候,不要去讨论你的数据库检索方法比他们的方法好多少。

情境与个性或情感一样(或更多)地扮演着重要角色:如果某人正处于崇高的“Zone”状态,正在解决一个复杂问题,他们可能不想听你关于pre-incrementpost-increment操作符相对优点的意见,尽管这些意见可能很有趣。(如果你实际上对pre-incrementpost-increment操作符的相对优点有意见并想分享,请将它们发送到/dev/null。)

共同语言和闪亮的流行语

任何社会群体都有其行话——那些加快专家之间交流的特殊词汇和短语。(行话还有另一个含义:群体用来保护其对话不被窃听的秘密语言。在这个意义上,行话和押韵俚语/背地俚语是行话。我们将在这个书中坚持使用行话的含义。)想想“树”这个词的意思;现在想想在计算机科学背景下它的意思。这个意思就是计算机科学家行话的一部分。

在某种程度上,行话术语定义了群体界限,因为它们是排他的。如果你在一个特定环境中没有学习到这些流行语,你就不会包括在那些已经学习到的人的对话中。因此,虽然行话促进了那些知情者之间的对话,但它也阻止了那些不知情者理解这些对话;这是不平等和分裂的原因。

重要的是要意识到,有时,次级行业、公司,甚至公司内的团队会发展出自己独特的俚语短语,这些短语甚至与它们所在行业或部门的俚语都有所不同。我在一家公司工作时,用“train”来描述一系列半独立的、都计划一起发布的项目,而其他项目经理可能会用“program”这个词。

我在电信行业工作的前几个月,一直被大量的三个字母缩写(TLAs)轰炸。当我询问它们的意思时,人们通常会展开这个缩写...当我询问它们真正的意思时,他们会看着我,好像我在好奇我们正在销售的这些“电话”东西是用来做什么的。沉浸在用行话交流的世界里,新员工会很快学会这些行话。然而,客户或供应商可能没有能力或意愿去这样做,所以当你使用它们时,他们可能会感到困惑或被误导。

应避免与困惑或被误导的供应商和客户打交道。供应商和客户(以及同事)也不应该感到被排除在外,但行话的使用可能会产生这种效果。如果你意识到你语言中哪些部分是你所在行业、领域或团队中发展的俚语,你就可以知道在何时使用它们会帮助讨论,何时会阻碍对话。

第十五章:第十四章

伦理

引言

开发者——无论是新手还是经验丰富的——随着其应用商店的推出而转向 iPhone,这被比作一场“淘金热”——www.adweek.com/news/technology/inside-iphone-app-gold-rush-98856。尽管如此,很少有人会将 1849 年的加利福尼亚淘金热视为人类以人性行为的典范。

自私的利润驱动破坏了现有的社区:旧金山四分之三的成年男性在淘金热期间离开了这座城市,兴奋地寻找新的金矿来开发。他们甚至破坏了其他社区,在挖掘土地时与该地区的美洲原住民发生冲突,这些土地是土著人民居住的地方。偏执的自我保护导致了暴民统治和异常严厉的财产犯罪惩罚:被认为从别人那里偷走黄金的人常常被处以绞刑。

那么,淘金热是否是今天程序员的可接受模式?我们是否可以自由地追求最高的经济收入,无论对他人——朋友和陌生人——造成什么代价?我们应该是“每个程序员为自己”,还是我们需要与程序员和非程序员一起合作?暴民统治是否可接受,或者我们应该遵循的行为准则是什么?

伦理准则的例子

许多职业都有它们的伦理准则(关于编程是否是“职业”的讨论将在下一章进行)。实际上,在线伦理中心(www.onlineethics.org)有很多例子、案例研究和讨论。而不是翻阅那些,我将专注于来自计算领域的一些例子。

计算机协会的伦理准则和专业行为准则—— www.acm.org/about/code-of-ethics是一份简短的文件,包含 24 项伦理要求,成员应遵守:其中之一是,协会的成员资格取决于遵守其他要求。

准则是技术中立和实践中立的,因为它应该以整个行业职业生涯的抽象层次来编写。简而言之,四个部分如下所述:

  • 尊重他人及其财产,不做伤害,努力使人类变得更好

  • 保持对行业现状的了解,帮助他人保持更新,并努力达到行业目前认为的最高标准和最佳实践

  • 确保你的组织和受其影响的人受到这些相同标准的保护

  • 遵守并推广准则

毫不奇怪,英国计算机协会——www.bcs.org/category/6030拥有非常相似的伦理原则。尽管它们的准则组织方式不同,但它涵盖了与 ACM 准则完全相同的内容。

我觉得没有必要对任何一种规范进行补充;每种规范都阐述了一些组织所追求的原则,并希望其成员能够遵守。讨论是否应该添加或删除某些内容是一个大话题,但让我们现在就保持这些规范不变。剩下的问题是:我们应该如何解释这些规范,以及是否应该应用它们?

道德规范的运用

遵守某些道德规范的成本比忽视它要高。ACM 规范告诉我们“尊重财产权,包括版权和专利权”:显然,窃取他人的版权作品比创建一个等效作品要便宜。可以在规范中的其他道德义务中找到类似例子。

从广义上讲,法律体系通过引入不遵守的成本,使得理性行为者也应该成为遵守规则的行为者。这是本书第十五章“哲学”中讨论的消除外部性的一个例子。如果窃取版权作品将使窃贼面临法律费用、赔偿和声誉损失,那么其他途径就变得有吸引力了。

对于我们大多数编写软件的人来说,我们所在的法律框架并不直接适用于我们的行为。存在涵盖数据保护的法律,某些领域受到严格的监管(主要是生命攸关的系统,如医疗设备的控制软件)。在很大程度上,软件制造商可以自由行动,受市场力量制约。这很大程度上是包括 ACM 在内的团体在软件行业游说自我监管的结果。他们希望有一个道德规范,但如果法院可以强制执行,他们就不愿意了。

此外,在大多数情况下,软件制造商并不是BCS(英国计算机协会)等组织的成员,因此不会因为未能遵守道德规范而面临被开除的威胁。最后,道德或道德观念是否进入招聘过程也不明显(尽管一旦你为一家公司工作,该公司的人力资源部门应该负责确保整个公司都按照该公司的道德原则行事)。我确实从未在面试中被问过我是否曾经不道德地行事。我被问过我对 Perl 的了解以及我如何与团队成员互动,但从未被问过我是否未能尊重他人的隐私。

那么,如果遵守道德规范会带来额外的成本,这种义务从何而来?

一个答案是,不道德行为确实有成本,尽管不是直接的财务成本。违背自己的原则会带来情感成本,而个人只能支付这么多。

情感成本的概念已经在网络安全策略的相关领域中得到了应用。人们普遍认为,当要求用户遵守安全策略时,通常需要付出额外的心理努力——hal.archives-ouvertes.fr/docs/00/69/18/18/PDF/Besnard-Arief-2004--Computer-security-impaired-legal-users.pdf,而不仅仅是采取简单但不可靠的方法。如果这种心理成本过高,用户可能会选择不承担这种成本,而选择更容易但不符合规定的途径。这仍然涉及到一些心理努力,包括意识到自己在违反雇主的信任,以及可能被发现的恐惧。这种焦虑可能会分散他们在其他工作中的注意力,甚至他们可能会因为违背自己的原则而离职。

此外,不道德行为还有声誉成本,因为供应商或客户可能选择不与他们认为不道德的公司或个人做生意,而更愿意与那些价值观与他们自己非常接近的人做生意。如上所述,这并不是软件市场运作方式的一个公开输入;这并不意味着它不是因素。

这个声誉因素是黄金法则(在这里,以波姆修改后的版本提供)的一个重要输入:己所不欲,勿施于人。这可以形成一个相互支持和有价值的人际关系和组织网络,他们共同维护彼此的价值和利益。这可以使道德工作比其他选择更高效、更容易。

道德模糊性

总是将世界建模为一种排他性选择系统总是更容易:这是好的,那是坏的;这是对的,那是错的;这是快的,那是慢的。不幸的是,这样的模型很快就会被发现存在太多的局限性。不同的道德原则很容易发生冲突。作为社会成员,我们的一部分责任是识别和解决这些冲突(毕竟,如果道德只是简单应用规则,我们现在可能已经让计算机来处理了)。

让我以自己的经验为例。我正在向另一位程序员提供建议,关于申请和面试新工作,这时这个人告诉我他们参加的一次面试。他们描述了感觉面试在候选人种族的基础上存在歧视,这显然违反了任何专业道德体系。参照 ACM 的准则,这违反了第 1.4 条:公平行事,并采取行动避免歧视。

有些人可能会建议我“吹哨子”,公开指责公司的歧视性做法,并且如果他们的员工是某个专业团体的成员,就让他们被那个协会“封杀”。但等等!这样做意味着我要应用我自己的不公平标准:在未听取和评估另一方的意见的情况下,偏袒故事的一方面。这也意味着我要公开讲述我被告知的秘密面试故事,这违反了尊重隐私和保密性的道德规范(ACM 规范中的 1.7 和 1.8)。

最后,我决定建议告诉我这件事的人应该就这次面试提出投诉,并且我会支持他们。无论你是否同意这个特定的结果,你都可以看到存在一些情况,在这些情况下没有明确的“道德”行为方式。拥有你意识到的、可以描述(即使只是对自己)、可以与你所做的事情相关联的道德规范是很重要的。寻求他人的指导并吸收他们的建议是很重要的。知道“一条真正的道路”来行动最好留给道家哲学。

事实上,真的没有一条真正的道路。道德规范是规范性的:它们源于相互互动的人们的共同信念和价值观,定义了他们认为可接受(如果你愿意的话,适当的行为)和不可接受的行为。现在被认为是道德的,将来可能不被认为是道德的,反之亦然。对一组人来说是道德的,可能对另一组人来说不是。

随着时间的推移,道德规范的变化可以在心理学实践中看到。二战后战争罪行审判揭露了纳粹政权对囚犯进行的残酷实验后,心理学家们接受了需要一套专业伦理和操作规范来规范他们的实验。这些规则最早于 1949 年以《纽伦堡法典》的形式发布——history.nih.gov/research/downloads/nuremberg.pdf

注意,代码中没有任何关于儿童受试者(或者如现代心理学家所说的“参与者”)的信息。实际上,儿童参与的问题在不同的国家、不同的时期有不同的回答。当阿尔伯特·班杜拉进行他著名的波波娃娃实验——www.simplypsychology.org/bobo-doll.html研究儿童对攻击行为的模仿时,参与实验的父母会知道他们的孩子参与了实验,但孩子们却无法知道。在现代实验中,很可能孩子们自己需要被告知他们正在参与一项实验。实际上,即使是灵长类动物研究——digitaljournal.com/article/343702也可能涉及自愿参与——这是在制定纽伦堡法典时没有考虑到的因素。

尊重隐私

在软件行业至少过去几十年来的伦理辩论前沿的问题,并且可能在未来至少十年内仍将处于前沿,是个人数据的滥用或使用。为了推动采用,许多软件供应商最终以低于成本的价格分发他们的软件,并通过收集关于用户的数据并将其出售给广告商和其他聚合器来获得收入。

这种出售用户数据的行为可能被视为不道德,因为它可能违反了尊重他人隐私的必要原则。这尤其适用于用户没有给予分享数据的知情同意;如果用户是一个不理解分享数据后果的孩子;或者如果收集的信息超出了支持分享活动所需的最小信息量。

由于这是一个如此之大且紧迫的问题,它不断地在科技媒体和权力走廊中被提出和讨论,我将隐私原则应用于个人数据共享,并提出了“不要做混蛋”的数据隐私指南(威尔·惠顿应得到赞誉,因为他普及了“不要做混蛋”这个短语,在某些圈子中被称为惠顿法则):

  • 你有权知道的唯一事物是用户告诉你的事物。

  • 你有权分享的唯一事物是用户允许你分享的事物。

  • 你可以与之分享的唯一实体是那些用户允许你与之分享的实体。

  • 分享用户物品的唯一原因是因为用户想要做一些需要分享这些物品的事情。

这很简单,这为良好的用户体验提供了基础。它是明确的,这意味着文化中关于可接受的隐含分享的观念不会使问题复杂化。

它也是普遍的。我观察到的一个关于隐私讨论的问题是,不同的人对现在必须解决的绝对最大的隐私问题有不同的看法。对许多人来说,是位置;他们不喜欢组织(公共或私人)能随时看到他们所在位置的想法。对其他人来说,是唯一标识符,这允许实体在多个功能中形成对他们的数据的汇总视图。对其他人来说,则是他们与老板、情妇、告密者或其他人之间的对话。

因为指南没有提到这些,它涵盖了所有这些——以及更多。谁知道未来的智能手机套件中会有什么传感器和能力?它们可能使用能够准确定位用户在人群中的位置的网络。它们可能包括自动人脸识别,当你的朋友附近时发出警报。一部手机可能包括血糖监测器。事实上,由于没有停止涵盖任何特定形式的数据,上述指南涵盖了所有这些以及我没有想到的其他任何东西。

有一个问题它没有涉及:用户想要分享某样东西,应用应该允许吗?这尤其是一个应用儿童应用的开发者应该问自己的问题。然而,孩子们也应该得到关于在互联网上分享什么好或不好想的公正指导,这应该融入应用体验中。“在按下此按钮之前请咨询负责任的成年人”是不够的:就是不要给他们按钮。

后记

当然,我在本章开头所描述的淘金热是故意带有片面性的。当人们意识到他们只能通过淘洗等单人技术获得微量的易得金子时,他们开始合作。这种合作——伴随着新的社会结构和规则——导致了水力采矿技术的进步,提取了金子和有用的矿物。

第十六章:第十五章

哲学

引言

当这本书的手稿逐渐成形时,我意识到其中很多内容是基于一种有限且天真的软件创建的哲学。我正在概述这种哲学如何应用于每一章,然后解释各种相关任务是什么以及它们如何融入这种哲学。以下是它的具体内容,明确地、独立于书中的其他考虑:

我们作为软件制作者的角色是“解决问题”,而制作软件只是顺便的事情。仅仅为了制作软件而制作软件,充其量是时间和金钱的无害浪费,最坏的情况是会对那些接触到它的人造成伤害。我们始终首要考虑的是我们正在解决其问题的人,以及这些问题本身。

如果这是 20 世纪 70 年代,你可能会称之为新时代的胡言乱语。如今,你可能会把它看作是那些关于成为更好的管理者的自助书籍中的废话;也许我现在应该把软件放下,去做管理咨询了。但只有通过同意一个学科的哲学,我们才能决定哪些工作代表了有价值的贡献。考虑一下科学哲学在千年间的变化(这里的讨论基于我的第一位经理约翰·沃德在牛津大学物理系的一次演讲)。

在古希腊文明中,任何你可以构建逻辑论证的结论都可以被接受为科学事实。因此,女性的牙齿比男性少,木头可以燃烧,因为它是由重土和轻火组成的,而火想要逃向天堂。这些事情被接受为真实,因为人们思考了它们,并决定它们是真实的。

在接下来的几个世纪里,科学的面貌发生了变化。理查德·P·费曼可能是在引用法国哲学家-牧师布尔迪安时表达了他的信念,即“所有知识的检验是实验”;这个观点在费曼的时代,已经花费了几个世纪的时间逐渐融入社会的科学哲学中。在皇家学会成立的时候,如果一个可敬的人在一个正确的圈子中提出了某事的证据,那么它就是真的:这就是我们如何知道海怪的存在,因为绅士们航行到美洲并报告说他们看到了它们。如果某个有声誉的人看到了什么,那么它一定在那里。

在 20 世纪,卡尔·波普尔为科学哲学提出了一个可证伪的哲学:与其寻找证明一个理论是正确的证据,不如弱化它并寻找证明它是错误的证据。这就是科学家今天采取的方法。所有这些并不是为了展示我们当前启蒙状态的进步而呈现的伟大历史。科学哲学的接受可能会随时改变。提出这个轶事的原因是为了表明,被认为是好的科学、坏的科学或值得的科学,都是定位在占主导地位的哲学观点之中(除了其他考虑因素,包括伦理)。通过类比,如果有人想要论证存在好的编程实践、坏实践或值得的实践,他们必须这样做,无论是明确还是隐含地,都要参考特定的哲学和价值体系。

在这一章的结尾,我想通过考察整体软件构建哲学的角色和输入,将整本书的内容综合起来。

软件作为一种追求

制作软件(为了金钱——我们将业余爱好放在一边)是一种职业吗?它是一种手艺?它是一种科学?一个工程学科?一种艺术形式?一门社会科学?

质疑专业程序员的观念很容易。职业的特点是进入门槛的教育壁垒:你不能成为一个自学成才的律师或建筑师,例如。教育确保(潜在的)从业者了解机构的知识体系和道德规范——这些在编程的“职业”中是缺失的。某些组织,如特许信息系统学会——www.bcs.org/计算机协会——www.acm.org正在试图将其定位为这样的职业,但它们代表了少数从业者。

我们有专业风格的会议;这些会议服务于一小部分从业者,通常在问题解决研讨会和技术演示中穿插(或代替)销售说辞和自我推销。没有专业性的结论:如果你忽视编写软件的伦理,你不能被禁止编写软件。编程的伦理在第十四章,伦理中讨论过,并发现其很大程度上是缺失的。

将软件组织为职业的另一个困难:正如我在第十章“学习”中描述的,编程教学过于随意,无法代表核心知识体系的转移。在 1968 年推荐大学计算机课程的教学大纲时,ACM 在学术计算机科学和野外的计算实践之间划了一条厚重的界限。即使在最新版本的教学大纲中——ai.stanford.edu/users/sahami/CS2013/,专业标准和伦理影响也只是计算机科学课程提供的培训中的一小部分。(在撰写本文时,教学大纲 13 仍处于草案状态。)无疑,完成计算机科学学位的人对计算机的工作原理有很好的了解,但可以争论的是,这虽然是成为新手程序员所必需的,但并不充分。

软件从业者将我们的工作视为职业的程度,一直以来都是多样化的。这也很大程度上是按需定制的。编写软件的实践并不是一种职业,而且不会在短期内实现职业化。目前几乎所有自称为程序员的人,除非他们接受过一些适当的培训,否则都会被排除在职业之外,除非有某种方式可以“祖父式”地加入,这会削弱成为认可职业成员的价值。突然出现的“许可”程序员可用性的下降,要么会削弱企业,要么会看到他们合法或非法地使用现有的、未经许可的从业者。例如,想象一下,如果 BCS 设法在英国为该职业获得保护提名,英国的公司会等到他们的程序员成为特许专业人士后再进行 IT 项目,还是会解雇现在不合格的员工并将工作外包到国外?

那么,编程是否可以成为一种艺术形式,或者是一种结合了手工艺能力和某些技术知识的技艺或贸易?在《计算机男孩们接管》一书中,Nathan Ensmenger 为这一立场提出了有力的论据。他观察到,尽管有大量的技术知识和计算机科学可以用于编程,但许多程序员对这个知识体系只有部分了解。他们通过自学模式来增强他们的技术知识——经验告诉他们以前有效,将来也会有效的东西。任何程序员或程序员团队都会建立起一个局部的手工艺知识领域,结果是编程技艺因环境而异。

恩斯梅格纳也注意到程序员负责在“组织的科技架构和社会架构之间进行调解。”他得出结论,这种将手工艺技术与科学知识和社会整合相结合的做法,使程序员不是一个专业人士,而是一个技术人员。他还观察到,专业程序员的修辞学具有流动的界限:程序员会根据听众的不同,将他们的工作描述为科学、工程、艺术。在整个讨论中请记住这一点——既要评估所描述的各种立场,也要分析我自己的结论,看看是否有 Humpty-Dumpty 主义的表现:

“当我使用一个词时,”Humpty Dumpty 带着一种轻蔑的语气说,“它的意思就是我选择的意思——不多也不少。”

软件工艺运动manifesto.softwarecraftsmanship.org/ 使用的是深深植根于中世纪贸易学校的语言。信徒们谈论学徒制和行会成员(以及继续一个早期的参考,关于鞋子和船只和封蜡;关于卷心菜和君主们),尽管与中世纪欧洲的行会(以及他们所实践的排他性,与专业机构相当)的平行关系往往不被提及。重点是社区互动,从他人的经验中学习,并通过结合这些经验来综合一种新的工艺方法。

虽然它吸引了几百年的传统,但软件工艺显然是对“软件工程”职业的直接回应和撤退,或者也许是对它的一个稻草人观点的撤退。皮特·麦克布伦的《软件工艺》序言问道:

软件工程是否适合于少于 100 开发者年的项目?软件工程中固有的专业化是否是一个好主意?软件开发甚至可以用工程术语来表达吗?

就麦克布伦而言,答案当然是“不”;这里更倾向于学徒制、实践和自我组织的团队。麦克布伦告诉我们,软件工程可能适合于建造航天飞机软件,但对于生产压缩包装商业软件或内部业务应用程序来说则失败了。这类应用程序需要个性化的触摸,而一个好的程序员不仅应该理解软件构建的技术细节,还应该理解制作定制作品所需的艺术技巧。

然而,它没有解决软件工艺运动实际上是否促进了软件制作作为一种工艺,或者它是否像软件工程中讨论的工程版本一样,同样是一个稻草人。中世纪工艺的形象与专业贸易一样,既具有排他性和分裂性。大师工匠是该地区控制工艺实践(及其秘密技术的传播)的行会成员。除了行会成员外,只有学徒被允许实践(而且只有在其师傅允许的有限方式下)。任何完成学徒期的人都会被赶出去成为“行商”,在他们旅途中寻找一个尚未被行会控制的城镇,在那里他们可以开店,或者直到他们可以提交“杰作”并成为行会成员。

软件工艺运动中的成员将这种排他性视为吸引人的证据是显而易见的。《软件工艺宣言》—manifesto.softwarecraftsmanship.org 以明确和隐晦的方式阐述了这一点:

我们开始重视 [...] 一群专业人士的社区。

…以及隐晦的方式:

我们开始重视 [...] 精心制作的软件。

第二个例子相当微妙,但“精心制作的软件”究竟是什么?这是一个如此模糊的短语,唯一获得定义的方式就是加入专业人士的行会;也就是说,通过向大师们屈服。

罗伯特·C·马丁喜欢通过定义“专业人士”为那些表现出可取品质的人,而将“非专业人士”定义为那些不具备这些品质的人来区分软件语言的不同:

  • 当程序员表现出专业行为时,遗留代码并非不可避免twitter.com/unclebobmartin/status/298762801164451840

  • 这里是一个最小的列表,列出了每个软件专业人士都应该熟悉的(来自《清洁程序员》www.amazon.com/The-Clean-Coder-Professional-Programmers/dp/0137081073,第一章,原文强调

  • 专业人士知道他们很自大,但并不假装谦卑。专业人士了解自己的工作,并为其工作感到自豪。专业人士对自己的能力充满信心,并基于这种信心采取大胆而谨慎的风险。专业人士并不胆怯。(《清洁程序员》,第一章

这里使用的语言自动在程序员之间造成了一种划分:那些符合马丁的理想的人是“专业人士”,而其他人则是,嗯,其他什么。不专业的?业余的?不是程序员?这也同样在专业程序员和他们合作的人之间造成了划分。经理和客户最好不要敢质疑我们的工作方式——我们在专业地工作。(布拉德·科克斯在他的书《超级分销:电子前沿上的对象作为财产》——virtualschool.edu/mon/Superdistribution/中提出了程序员和非程序员之间划分的观点,所以他写作于 1996 年时,这种划分已经存在。他半开玩笑地说:“客户,尤其是经理,的角色是站在一边,手拿支票簿,赞赏这位程序员技能的卓越和对他的手艺的奉献。”)

手工艺运动质疑软件是否真的是一门专业工程学科,在回答“不是”时,它推广了许多与软件工程运动或任何受监管职业相同的理想和划分。

我想提出一个不同的问题:编程真的是一门社会科学吗?程序员应该了解多少关于软件构建的社会、人际方面的知识?本书的大部分内容都集中在编程的协作性质上:文档、管理、团队合作和需求工程都是程序员为他人或与他人一起做的事情的例子。因此,我认为很少有情况是程序员可以需要这些技能的。本章剩余部分将通过对社会科学各个分支的视角来探讨制作软件的实践。

软件的经济哲学

直接经济因素

软件产品通常作为固定期限的项目被创建或扩展。项目的预估成本与预估产生的收入进行比较,如果平衡有利,则项目获得批准。高级项目资助者会考虑保护性收入(如果添加此功能,有多少客户不会跳转到竞争产品)和机会成本(如果我们拒绝这项工作,我们可能在做些什么),将这些因素纳入项目决策中。

我提到了巴里·W·博伊姆和他的书,《软件工程经济学》——books.google.co.uk/books/about/Software_engineering_economics.html?id=mpZQAAAAMAAJ&redir_esc=y,在第九章,需求工程中。他介绍了人类经济因素的概念;例如,为软件将给其用户带来的满足感(或否则)分配美元价值。我将在下一节,关于外部性的下一节中回到这一点,但在此刻,请记住,预期的人类经济因素被认为是项目成本的一部分。

所以,奇怪的是,维护成本在博伊姆的 COCOMO 模型中被视为项目经济的一部分。记住莱赫曼关于 E 型软件的定律,部署环境会进化,软件系统必须与之保持同步。在博伊姆的模型中,这种进化在项目成本的一个条目中得到体现。

这种维护成本估算似乎表明我们在为软件预算的方式存在问题。一些进化变化(功能添加)必须被视为明确的项目,其成本必须明确计算并与其预期的收入相平衡。其他进化变化(维护修复)只是被视为编写软件的必要风险,其成本被纳入编写新功能的计算中。

新功能总是比错误修复更大、更昂贵吗?不。错误修复总是花费我们金钱,并且从未吸引或保护收入吗?不。新功能有时会悄悄地融入维护中吗?是的。错误修复有时会推迟到新项目发布吗?是的。那么为什么它们没有被一起预算呢?

可能是出于道德原因:也许程序员认为维护问题是他们应该承认并免费纠正的错误。但记住,莱赫曼定律之一指出,软件带来的满足感会随着社会环境的涉及而衰减。并非所有在编写时被认为是错误的错误在编写时都是错误的!你不能为在环境变化之前你做正确的工作道歉。

对我来说,这表明需要一个更敏捷的经济模型;一个无论它是错误修复、功能添加还是内部质量清理,都同等对待任何变化的模型。忘记我们已经在这个产品上花费和制造了什么(因为这样会导致沉没成本谬误),提议的改变将花费多少?这将给我们带来什么?它有多大的风险?我们还能做什么?我们有什么替代方案?

外部性

上述问题仅考虑了制作软件的直接经济影响。还有其他因素;这些因素以某种形式产生成本或效益,但不会对工作的价格或收入产生实际影响。在经济学中,这些被称为外部性

外部性可以是正面的,也可以是负面的,但它们也可以是个人层面的,而不是与公司及其工作相关。作为职业的软件开发具有各种外部性,包括利益和成本,这些外部性并没有反映在我们的工资中。让我们考虑那些既影响个人又影响企业的外部性。

开源软件对许多企业来说是一种正面的外部性。软件行业的许多公司采用已经免费发布的组件或系统,并将它们纳入自己的产品中,或者提供“增值”服务,如支持。这些公司从开源软件中获得价值,而无需为创建该软件付费。作为一个开源软件作为外部性的例子,编写 OpenSSH 的成本并没有计入 macOS X 的价格,尽管 OpenSSH 是该系统的一个组件。

对于个人程序员的情况则不太明确。暂时不考虑那些个人和商业目标紧密相连的微型独立软件开发者,一个求职的职业程序员可能会被要求展示他们创建或贡献的开源项目组合。我推断,创建开源软件具有积极的影响:它提高了我们的声誉,增加了我们被雇佣的可能性。另一方面,创建开源软件的行为可能是负面的:如果你不是作为工作的一部分来做这件事,那么你实际上是在增加工作量而没有直接报酬。

缺陷是负面的外部性。软件公司通常在向消费者销售时根据市场力量定价,如果他们向客户企业销售,则基于初始项目的日费率。在两种情况下,后续的维护成本都没有计入销售价格;与无需维护的相同产品相比,这将导致利润减少。客户本身并没有将缺陷计入使用软件的(经济或心理)成本。正如 David Rice 在《Geekonomics: the real price of insecure software》一书中所论证的——books.google.co.uk/books/about/Geekonomics.html?id=k6cRhfp2aWgC,在评估软件产品时,客户通常只有功能清单可以参考,无法了解其质量。但质量是有成本的;你支付测试人员,你分类处理缺陷报告,你监控支持渠道以发现问题,并致力于修复工作。

一些组织会举办黑客马拉松或黑客日,在这些活动中,人们通常会组成团队,为某些挑战提供基于软件的解决方案,获胜者将获得奖品。这些黑客日可能会对职业生涯产生积极影响,因为一些雇主可能会将参与黑客日视为社区参与的证据,并且它们提供了“磨砺锯子”和尝试新技能或工具的机会。另一方面,花费更多时间工作(尤其是在某些黑客日中需要的通宵达旦)会对你的健康产生不利影响,这是一个负面影响。

最后,考虑一下制作软件产品所投入的所有工作是否都反映在标签价格上。如果你在制作用户指南上花费的金额加倍,价格会上涨吗?可能不会。同样适用于本地化:你将拥有更大的潜在客户池,但大部分情况下,你无法提高价格。这表明,对你的客户来说,本地化是一种外部性:本地化软件的好处,但不是改变他们支付金额的好处。

公司可以通过内部收费或将成本或节省转嫁给客户来将他们对外部性的价值纳入其决策中:一个简单的例子是,如果允许他们共同品牌化最终产品,一些代理公司会对项目收取更少的费用。代理品牌与产品的关联以及可能推动未来工作的可能性是一种正外部性。将节省转嫁给客户——即在存在正外部性时降低成本——显然比转嫁负外部性的费用更易于接受,但后者是可以做到的。想想有机食品的价格溢价——www.mint.com/blog/trends/organic-food-07082010/,这比生产成本差异(在某些情况下,由于补贴可能低于非有机食品)要高。通过说服购买者有机食品确实有实际的好处,供应商可以要求更高的价格。

传统供需经济学

许多经济学教科书都会从讨论供需作为影响市场价格的关键因素开始:当需求高或供给低时,价格上升;当需求低或供给高时,价格下降。将这种经济结构应用于软件定价的问题在于供给是无限的:没有“单位成本”,所以一旦软件制作完成,就可以不断复制,直到所有想要副本的人都有为止。那么,软件如何才能在价格不立即跌至零的情况下进行销售呢?

面对证据,有些人不相信这是可能的。这就是数字版权管理的宗旨:试图将物理商品的稀缺性重新引入软件(和其他数字商品)的经济学中。但人们确实成功地销售了软件、音乐、文档(例如这一篇)等,而没有使用 DRM。我们不仅要注意到无限供应“问题”并试图限制供应,还需要尝试理解那个确实存在、可持续,但与长期模型不匹配的市场。

我将从假设开始:所交易的不是软件本身,而是首先能力,其次是时间。鉴于有做某事的愿望,但无法做到,任何通过使该事物成为可能来解决该问题的东西都是有价值的。这就是经济学家赫伯特·西蒙描述的有限理性,或满意解法www.economist.com/node/13350892。因此,第一个发现的解决方案,无论是否理想,都是有价值的。这已经解释了为什么无限供应“问题”不是真实的:在发现可以购买到满足他们需求的产品时,消费者可能会满足于作为满意解法进行购买——许多人不会花额外的时间去研究盗版应用。对于一些人来说,使用盗版应用确实会带来成本,即焦虑。任何与一个人的道德相悖的决定,无论多么理性,都会产生心理成本。信息安全行业将这一点视为通过政策控制安全的限制因素之一。

找到问题确实可以解决后,顾客随后可以花一点时间思考如何改进这个解决方案。这就是节省时间的地方。现在他们知道自己能够做什么,就有可能提高这种能力,以便有更多的时间去做其他事情:这也很有价值,正如本杰明·富兰克林所明确指出的。(这个论点以修改后的形式适用于游戏。只需颠倒这两个因素。鉴于我有时间可用,你能提供让我享受其过程的能力吗?)

在这个模型中,软件本身没有价值,这与传统经济学的无限供应问题相符合。但顾客的时间和能力是有限的,软件可以用作工具来解锁这些。从这个意义上说,为软件付费类似于为教育付费:你想要的不是教学,而是已经受过教育。因此,我们可以这样说,软件的价值不在于创造解决问题的方案,而在于问题已经被解决。由于满足的本质,如果成本和能力“足够好”,顾客会为解决方案付费。

回顾本章的第二段,我们看到这种经济模式只是同样的哲学,用经济术语表达。我们作为软件制作者的角色是解决问题——我们通过解决问题为我们的客户提供有价值的服务。

软件管理哲学

想象一个世界,在这个世界里,程序员的地位与中层管理者相似。但首先,请摒弃管理者天生无用且邪恶的观念,让我来解释一下管理者的定义。

管理者通常不会因为工作而获得报酬;他们通常根据团队的工作表现和团队完成的工作量来获得报酬。大量工作做得不好并不好,但工作做得不够好也不理想。

这通常意味着他们会避免做工作。给他们一些工作要做,他们通常的做法是找到团队中最有能力完成这项工作的人,并让他们去做。他们会让那个人负责完成工作,并且(如果他们做得好)给予他们完成工作的权限。

但他们并不是因为告诉别人如何做工作,或者因为委托责任或授权的任务而获得报酬。事实上,如果工作没有完成,或者没有做好,那么公司其他部门会认为管理者应该负责。他们是因为工作已经完成而获得报酬。

现在,想象一个程序员与中层管理者一样被重视的世界:一个程序员是管理者,计算机向程序员汇报的世界。程序员不是因为编写软件——向计算机解释需要完成的工作而获得报酬。程序员是因为计算机完成了分配的工作而获得报酬,无论是数量足够还是质量足够。如果计算机没有完成工作,那么程序员将承担责任。

再次强调,这只是在章节开头所采取立场的重申。虽然前一个部分的重申告诉我们购买软件的人重视什么,但这一点告诉我们,在软件制作人身上应该考虑什么是有价值的。我们看到,“编写的代码行数”、“完成的用户故事点数”、“增加的功能数量”和“修复的 bug 数量”本身并不是有价值的东西,但也许我们可以看到每个指标作为我们工作有用代理的程度。

软件社会哲学

在第九章“需求工程”中,你了解到软件并非独立存在,而是嵌入在其使用的社交系统中。本书的其余部分主要讨论了另一个社交系统:软件的开发系统。许多软件是由多个人共同制作的。即使在罕见的情况下,一个人完成了所有的生产(编码、设计、UI 文本、营销、销售等),也可能会有些客户反馈,即使那只是以支持邮件的形式。

那么,这些两种社会系统在这个领域是如何被考虑的?典型的程序员形象是某人(通常是 20 多岁的白人男性),独自工作,盯着显示器。如果外界被承认,那也是通过其排除:程序员戴上耳机以避免干扰,编写代码。(在撰写本文时,以及就我的观点而言,谷歌图片搜索“程序员”的结果——www.google.co.uk/search?q=programmer&aq=f&um=1&ie=UTF-8&hl=en&tbm=isch&source=og&sa=N&tab=wi&ei=4J2TUbOrOZSV0QWI7YHABQ&biw=2560&bih=1368&sei=452TUcKIFoi40QXmjIDYCQ支持了这种“典型”形象的描述。)

我们在这里自动看到了各种问题。制作软件的人是程序员,而不是其他任何相关专家。他是男性,而不是女性或跨性别者。他是白人,而不是其他任何种族。他是年轻人,而不是老年人。他是独自一人,而不是与他人一起工作。所有这些不平等都存在于软件制作者的描绘中。所有这些都未能捕捉到围绕软件系统的社会系统的多样性和复杂性。许多这些不平等存在于软件制作者的描绘中,因为它们存在于软件制作的现实中。

社会科学家会对他们研究的任何社会系统提出两个高级问题:社会是如何构建和修复的?它支持哪些分化和不平等?通过审视“传统”的程序员观点,我们已经看到了软件行业目前支持的一些不平等。

我们可能还能找到更多。Shanley Kane 研究了硅谷初创公司使用的语言——blog.prettylittlestatemachine.com/blog/2013/02/20/what-your-culture-really-says,寻找潜在的偏见,例如:

我们没有休假政策

你的文化可能实际上在说……我们欺骗自己认为我们有一个更好的工作/生活平衡,而实际上人们即使有休假政策,休假时间也比没有休假政策时还要少。社会压力和对工作的沉迷已经取代了政策,成为休假时间的调节器。

如果这是真的,那么这表明那些能够工作更长的时间并且休假更少的人在这个系统中处于相对有利的地位。这反过来又赋予了某些阶级特权:例如,那些年轻且没有孩子的阶级。

那么,这就是软件是制造的社会系统。那么,软件是使用的社会系统又如何呢?那里也存在不平等和分化。商业软件系统(甚至运行在商业平台上的免费软件系统)只有那些买得起的人才能访问。

在英国,国家统计局估计有超过 700 万人从未使用过互联网。他们确定了互联网接入能力与人口统计状况之间的相关性,因此在线服务(例如)不太可能对 75 岁以上的人或残疾人可用(在我们考虑特定服务是否具有开发者通常理解的“可访问性”功能之前,这种可访问性的缺乏就已经存在了。)

还可以找到其他不平等。许多应用程序只支持英语,而且即使它们可以本地化,它们也不处理非格里高利日历、从右到左的书写系统、带变音符号的字符以及其他“非英语”(或非美国)地区特性。

知道这些不平等存在(其他也存在)并报告它们是一回事,但可能并不新颖。我们该如何利用这种意识呢?

你觉得哪些不平等是不公正的,这很可能取决于你的政治观点,尽管前一章中描述的伦理文件为我们提供了一个实用的指南。从ACM 伦理准则—— www.acm.org/about/code-of-ethics

不同群体之间可能存在的不平等可能源于信息和技术的不当使用。在一个公平的社会中,所有个人,无论种族、性别、宗教、年龄、残疾、国籍或其他类似因素,都应享有平等的机会参与或从计算机资源的使用中受益。然而,这些理想并不能证明未经授权使用计算机资源的合理性,也不能为违反本准则的任何其他伦理规范提供充分的依据。

这相当明确。ACM 期望其成员遵守的,是在其他伦理准则范围内的完全不歧视——正如以往一样,潜在的伦理冲突仍然存在。从特权方窃取计算机资源供弱势方使用(我将其称为“罗宾汉调度”)就是这种冲突的一个例子。

在歧视中需要注意的一个重要因素是他者化。社会心理学家区分了标记身份和非标记身份——cak400.wordpress.com/2012/10/01/marked-and-unmarked-identities-and-social-hierarchy/。一个“非标记”的身份是人们认为正常的东西,而其他身份通过不同于这个基准来区分(“标记”)。谈论移民的人是在标记一些人作为移民,并由此隐含地将本土人定义为正常。谈论女性的人是在标记一些人作为女性,并隐含地将男性定义为正常。

关于“他者化”的重要方面是这种区别的不对称性质:它是在“正常”人和“不像我们的人”之间。重要的是要认识到我们确实这样做,这是我们的思维方式,要识别我们何时这样做,并自觉地纠正它。正如 Mike Lee 所说—twitter.com/bmf/status/333960606837272577

我们把那些我们自己在自己身上拒绝的品质放入其他人的身上。但这使我们忽视了现实。

所以,下次当你认为“普通人不会想要那个功能”或“任何有常识的人都不会那样使用它”时,问问自己你是否真的认为“不像我的人不会想要那个”,然后考虑你是否在为像你这样的人的少数人编写软件,还是为所有人编写。

软件的教育哲学

这是哲学章节中最技术性和最基础的组成部分,也是我最没有资格谈论的部分。我在大学教了几年编程,但作为大学教学最明显的特征之一是,在你开始之前没有人会培训你,我不确定这算不算。

很容易找到这样的断言:学术计算机科学与实践无关shape-of-code.coding-guidelines.com/2013/05/15/wot-apply-academic-work-in-industry/计算机科学不足以作为软件职业的准备。这是一个问题吗?如果是,原因是什么?有哪些替代方案?

商业软件和学术软件实践之间的差异在计算机历史的早期就已经开始。在《软件作为一项追求》中描述的 ACM 课程的第一版是课程 68dl.acm.org/citation.cfm?id=362976。在这门课程的介绍中,作者明确指出,学术计算机科学课程不适合培训专业 IT 人员:

例如,这些建议并不是针对计算机操作员、编码员和其他服务人员的培训。对于这些职位以及许多编程职位,可能最好由应用技术项目、职业学院或大专院校提供培训。也很有可能,在商业数据处理、科学研究、工程分析等领域的大多数应用程序员将继续是受过相关学科领域教育的专家,尽管这样的学生无疑可以通过参加一些计算机科学课程而受益。

因此,课程是在这样的认识下创建的:它不会直接适用于那些希望成为专业程序员的人。虽然存在职业课程,但遇到没有接受过该领域正规介绍的有能力自学程序员是非常常见的——包括我自己。世界上关于如何制作软件的信息很多,自学的人必须以某种方式发现这些信息:最终,很多知识将通过试错来学习。《软件工程知识体系》(www.computer.org/education/bodies-of-knowledge/software-engineering)可以被视为从已发表的软件工程文献中学习内容的指南。当以书籍的形式呈现时,这个指南比本文更长。就像这本书一样,指南本身并不处于“这是如何制作软件”的水平,而是处于“在制作软件时应牢记的这些事情”的水平。因此,我们有一个 200 页的指南,涵盖了 13 个“知识领域”,这些领域包括你应该知道的事情列表,并附带一些对可用文献的引用。知识领域、每个领域选择的主题以及参考文献的时效性和有效性都是(正如你可能从这个领域预料到的)有争议的,因此SWEBOK软件工程知识体系)代表了保守选择的一组已经广泛应用的观念。

自学程序员如何才能跟上这个庞大且不断发展的知识体系?支持“软件作为一门职业”的人会说他们不能;他们认为这是专业机构的责任去教授和维护知识体系,并确保只有跟上进度的人才能被认为是程序员。支持“软件作为一门手艺”的人也会说他们不能:他们认为他们需要来自学徒期的专家指导,然后是作为熟练工人的自我探索期。

但是,反思第十章“学习”,我必须问:SWEBOK 除了是一个学习课程,无论是教授的还是自学的,还有什么吗?它以相当抽象的水平(以及非常枯燥的风格)呈现,因此可能更适合教师决定教什么,而不是初学者试图找出要学习什么。

那些内容——不一定是 SWEBOK 本身,但类似于它——可以很容易地改编成自学指南。我发现最合适的模式是能力矩阵:我在过去几年中,将我对计算机科学的知识与程序员能力矩阵www.starling-software.com/employment/programmer-competency-matrix.html)进行了评估,并在撰写本文的过程中创建了程序员礼仪矩阵blog.securemacprogramming.com/2013/04/rebooting-the-programmer-competency-matrix/),以总结材料。

矩阵成功的地方在于,它为学习者提供了一个方便的方式来评估自己的进度(无论是通过反思,还是与评估者或教育者的讨论),并了解在矩阵的任何特定行中需要什么来前进。列布局提供了关于“接下来”是什么以及可以留到“以后”的指导。

这种排序是我职业生涯早期遇到的一个难题。我在一家大型公司工作,该公司有通过技术角色的晋升途径:软件工程师、高级软件工程师、资深软件工程师和软件架构师。我被雇佣在第一个级别,但很快就被提升为高级软件工程师。因为我专注于下一个级别,所以我试图在巩固和扩展我对高级角色的理解之前,学习资深工程师的职责。因此,我没有成为一个特别好的资深工程师:这是继续前进的先决条件。

矩阵在SWEBOK做得好的部分失败了:为每个级别提供参考资料,让学习者知道在哪里找到进步所需的信息。这部分课程内容更加具体:自学课程可能会指向书籍、文章、会议演讲或网站,以了解学习资源;而指导学习课程可能会建议特定的培训或大学课程,或者由教育者评估的问题集。关键是,没有理由一个自学程序员不能,通过能力矩阵提供的领域意识和自身能力,像职业程序员一样进步——可能进度比教授或硕士程序员慢,但仍然在进步。

将这次讨论(以及第十章,学习)与本章开头的位置声明联系起来,软件制作者的教学实际上应该被视为在软件系统背景下进行问题识别和解决方案的教学。从这个角度看,学术和商业领域的教学目标是一致的;只是选择要解决的问题(因此关注知识体系中的特定领域,相当于能力矩阵中的特定行)不同。

对于新手程序员,无论是自学成才、学徒出身还是受过教育(请注意,这句话中不存在错误的二分法;自学和学徒出身的程序员并非“未受过教育”,他们只是没有从教育者那里学习如何制作软件而已),从爱好者到专业软件制作的课程——无论软件是在何种背景下制作的,无论我们选择何种“专业”的具体定义——都始于对软件作为解决问题的手段而非目的本身的认识。下一步是认识到他们新手能力与当前技术水平之间的差距。他们选择如何填补这个差距并不那么重要,重要的是认识到这个差距的存在。

成为“擅长”制作软件意味着什么?

关于制作软件的人的生产力,有很多说法。许多人声称有些程序员的生产力是其他程序员的 10 倍——www.johndcook.com/blog/2011/01/10/some-programmers-really-are-10x-more-productive/。这意味着什么?

想要得出一个数量,即使是像“10 倍”这样的相对数,我们可能有一些可以应用于在不同背景下制作软件的人的定量指标。这个数量是什么?是编写的具有重大意义的代码行数吗?如果是这样,我们应该解雇那些一天只写-2000 行代码的程序员——folklore.org/StoryView.py?story=Negative_2000_Lines_Of_Code.txt

那么,修复一个错误所需的时间,最初是用来发现 10 倍数的指标(针对少数程序员)?也许程序员并没有更有效率,但我们恰好捕捉到了他们效率高的一天?那么,那个花了更多时间确保错误最初就不存在的人呢?这个人是不是更勤奋,还是在浪费时间进行过度设计?

如果你接受这里提出的软件制作观点,那么一个人能编写的软件数量,无论你如何衡量,都与制作人是否优秀无关。相关的问题是,软件制作人从(或引入)他们的客户正在工作的系统中移除了多少问题。

这种生产力度量标准最有效的展示之一来自一位朋友,他被一位潜在客户要求设计一个移动应用程序来解决客户业务中存在的特定问题。在与客户会面并讨论他们的问题后,这个人观察到电子表格比移动应用程序是一个更好的解决方案。因此,他们拒绝了浪费客户金钱创建次优解决方案的机会。那个人可以写出电子表格,软件制造商可以将他们的注意力转向更合适的技能应用。

不幸的是,关于软件在系统中的净效应是解决问题还是引入问题的问题尚未得到解答,也许在系统变得庞大时是无法回答的。例如,直到 20 世纪 80 年代,西方组织中的许多办公室都雇佣了主要由女性组成的打字员,尽管工资较低且环境嘈杂。在引入台式电脑后,那些打字员被那些使用文字处理应用程序准备自己文档的传统上地位较高的工作人员所取代。那些应用程序以及它们运行的电脑得到了主要由男性 IT 支持工作团队的支撑。

对于经历了这些变化的企业来说,IT 支持部门与打字员相比,其成本效益是更高还是更低?使用文字处理器的打字是否比为打字员手写稿件更有效地利用了高管的时间?台式电脑和办公打印机是否比几十台打字机造成的问题更少,还是更多?

在社会层面,失业的打字员是否从打字员的专制中解放出来,或者他们被排除在劳动力之外?电脑对性别平等是好事还是坏事?软件是否创造了比它消除的更多机会?

这些问题是复杂的,我将不回答它们就结束。只需说,虽然我们新的生产力度量标准在哲学上比代码行数等事物更好,但应用起来要困难得多。

结论

我写这本书是为了反思我所知道的关于制作软件的知识,以及理解我关于制作软件所不知道的知识。我出版它是为了让你们能够利用我在从事这一职业的十年中所发现的东西,并激发你们对自己经历的反思(希望你们能像我所做的那样与我们分享)。

我首先从我们在煤场工作时的事情开始:我们用来将想法转化为软件的工具和实践。然后我观察了我们如何与其他人合作:我们如何记录我们所做的工作;我们如何找出需要编写的软件;我们如何利用机会从他人那里学习,解释他人的论点,并在团队或商业环境中与他们合作。最后,我试图构建一个高级模型,将所有这些工作定位其中,通过考虑制作软件的伦理和哲学,以及如何通过教授这一代新手的技能来推进我们的知识。

通过这个过程,我发现,虽然计算机科学可能能够告诉我们关于我们在计算机上使用的编译器和语言的一些信息,但软件产品不能脱离它们被制作和使用的社会系统。心理学、社会学、民族志和经济学:所有社会科学都有智慧可以传授,可以帮助我们作为软件制作者使用我们的技能来解决人们的问题。

不幸的是,这项工作以一个困境结束:虽然不同的软件制作者群体已经确定了避免歧视的道德必要性,但我们不能明确地说我们的行业没有在其影响的社会中造成新的分裂和不平等。关于是否使用 Web 或本地技术,或者是否使用函数式或面向对象的编程风格是“更好”的问题,要么会被回答,要么变得无关紧要,或者两者都会。我们工作是否消除或加强了人与人之间的分裂的问题永远不会消失,这将是历史评判我们所作所为的标准。

posted @ 2025-10-27 09:07  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报