编程中的极简主义:复杂性如何影响您的生产力

2020-10-20 23:16:33

本文基于我即将出版的新书“从1到0:极简主义编程方法”中的一章。

我的编程专业的学生经常写信告诉我他们的挣扎和失败。许多学生最终克服了他们的挣扎,但在意识到创建软件有多么困难后,他们中的很大一部分人放弃了编程野心。这些学生一开始的目标是成为专业的程序员,但最终,他们没有实现这个目标。

在与这些学生进行了数千次个人对话之后,很明显,许多新程序员不会因为他们不了解Python的其中一个特性,或者因为他们缺乏技术技能、智力,甚至才华而失败。

相反,他们之所以失败,是因为他们被编程中无处不在的复杂性淹没了。复杂性使他们不得不认输。这是不幸的,因为有许多方法可以减轻编程复杂性的有害影响。在上一章中,您已经学习了一些关于80/20原则的策略(关注重要的少数人,牺牲那些微不足道的多数人!)。

在这一章中,我们将全面探讨这一重要而又未被深入探讨的话题。复杂性到底是什么?它发生在哪里?它看起来怎么样?

让我们从快速概述开始-选择正确的选项非常复杂。

要处理的编码项目-从数千个开源项目和无数问题中,

押注于新兴技术-Alexa应用程序、智能手机应用程序、基于浏览器的Web应用程序、集成的facebook或微信应用程序、虚拟现实应用程序-以及

鉴于这些复杂性来源造成的巨大困惑,“如何开始?”也就不足为奇了。是编程初学者最常见的问题之一。

要立即回答这个问题,最好的开始方式不是选择一本编程书籍,然后阅读编程语言的所有语法特性。令人惊讶的是,这些编码书卖得很好-就连我也是这类书的卖家。然而,在与数以千计的编程学生个人互动时,我意识到许多有抱负的学生购买编程书籍是一种承诺,将学习任务放在他们的待办事项清单上-如果他们已经在这本书上花钱,他们最好读一读,否则投资就会失去。但是,就像他们待办事项清单上的许多其他任务一样,阅读一本编程书籍很少是一项需要完成的任务。

那么,开始学习编程的最好方式是什么呢?在我看来,最好的开始方式是选择一个实用的代码项目-如果您是初学者,这是一个简单的项目-并将其推向完成。

只需设置项目,并用您有限的技能和常识开始编码即可。

如果你不明白你在做什么,你会逐渐增加你的理解。你阅读书籍和文章只是为了在你面前的项目上取得进展。通过深入研究完成第一个项目的过程,您需要解决许多高度相关的问题:

通过回答这些问题,您将逐渐建立一个全面的从业者技能集。随着时间的推移,你会越来越好地回答这些问题。你解决这些问题的速度和技能将会提高。您将能够解决更大的类似问题,并将创建编程模式和概念洞察力的内部数据库。即使是高级程序员也通过完全相同的过程学习和改进-只是编码项目变得更大、更复杂。

让我们假设您采用了这种基于项目的学习方法。你专注于一个项目,并花了相当长的时间来工作。你现在最大的敌人是什么?你猜对了:复杂性。

复杂性无处不在,在项目的每个阶段都是如此。这种复杂性的隐藏成本是非常明显的:刚刚开始的程序员认输了,项目永远也看不到曙光。这位初学者辩称:“编码对我来说太难了”,他真的相信这一点-尽管没有什么比这更离谱了。

问题的根源是极其复杂和缺乏重点。因此,问题就出现了:

答案很简单,我在这本书“极简主义”中已经强调过几次了。在编码周期的每个阶段寻求简单性和重点。我希望您从书中删除这一概念:在编程空间中遇到的每一个领域都采取激进的极简主义立场。如果这本书能够说服你采取更极端的措施来增加你的关注点,那么它就完成了它的使命!

让我们更深入地研究复杂性的概念,以加深对您的编码效率的最大敌人之一的理解。

在不同的领域,术语复杂性有不同的含义。有时,它是严格定义的,例如在计算机程序的计算复杂性中,该程序提供了一种针对不同输入分析给定代码函数的方法。其他时候,它被松散地定义为系统组件之间的交互量或结构。但在这本书中,我们将以更一般的方式使用它。

梅里亚姆·韦伯斯特词典将复杂性定义为“某种复杂的东西”。术语复合体被定义为“由复杂的[…]组成的整体。部件“。如果您解决了复杂这个术语--“难以分析、理解或解释”--您最终会得到以下粗略的定义:

复杂性:“一个整体,由部分组成,很难分析、理解或解释”。

这就是我们在本书中使用术语复杂性的方式。复杂性描述了整个系统或实体。这很难解释或描述。因为它的艰巨性,复杂性带来了挣扎和困惑。当面对复杂性时,人们发现自己在认知上无法理解“整体”的更深层次的含义、含义或影响。

他们看不到全局-复杂性是清晰度、封闭性和可预见性的敌人,因为复杂的系统以高度不可预测的方式运行。你在哪里发现复杂性?你会发现它无处不在,因为现实世界的系统是杂乱无章的:一个高度相互关联的因果网络,混淆了真实系统的行为,对于被困在这个复杂网络中的个人来说,这是不可能破译的。就像微分方程一样,一个系统的输出反馈给另一个系统的输入,而另一个系统的输入又反馈给第一个系统作为输入。高度复杂的系统的例子有股票市场、社会趋势、新兴的政治观点,以及具有数十万行代码的大型计算机程序,如Windows操作系统。

如果您是一名程序员,您特别容易遇到难以承受的复杂性。让我们深入探讨编程领域的不同复杂性来源:

学习和创造持久价值的最好方式是通过参与或发起一个现实世界的项目。但是,当一个现实世界的项目变得生动起来时,它看起来会是什么样子呢?让我们深入了解项目生命周期的不同阶段:规划、定义、设计、构建、测试和部署(参见图1)。

图1:软件项目的生命周期由六个概念性阶段组成:计划、定义、设计、构建、测试、部署。

图1显示了由六个阶段组成的软件开发生命周期。即使您正在处理一个非常小的软件项目,您也可能要经历软件开发生命周期的所有六个阶段。接下来,您将快速了解所有六个阶段-以及复杂性如何对每个阶段产生重大影响。

软件开发生命周期的第一阶段是规划阶段。从软件工程文献中,您可能知道这就是需求分析。此阶段的目的是确定最终产品的外观。成功的规划阶段会产生一组严格定义的必需功能,以交付给客户或最终用户。

规划阶段解决了一个多维问题,其中不同部门和功能必须协作以确定软件的最佳功能集。必须考虑许多因素:构建功能的成本、无法成功实施功能的风险、最终用户的期望值、市场和销售影响、可维护性、可扩展性、法律限制等等。

这一阶段是至关重要的,因为它可以使你在接下来的阶段避免大量的下游能源浪费。企业主知道,资本配置(或一般而言:资源配置)是CEO最重要的单一职能。规划阶段是杠杆发挥最大作用的阶段:规划错误可能导致价值数百万美元的资源浪费。另一方面,周密的计划有能力为企业在接下来的几年里取得巨大成功奠定基础。计划阶段是一个很好的杠杆点,你可以在这里应用你新学到的80/20思维技能。

为什么?因为我们的主要敌人正潜伏在各处:复杂性。正确地提前评估风险是很复杂的。弄清楚一家公司或一个组织的战略方向也同样复杂。猜测客户对软件项目的反应是很复杂的。权衡不同候选特性的积极影响--考虑包含的特性--是很复杂的。而且,确定给定软件功能的法律含义也很复杂。综上所述,解决这个多维问题的绝对复杂性正在扼杀我们。

与前一阶段相比,这一阶段相对简单。定义阶段包括将前一阶段(需求)的结果转换为适当指定的软件需求。换句话说,它将前一阶段的输出正式化,以获得稍后将使用该产品的客户和最终用户的批准或反馈。

设计阶段的目标是起草系统的体系结构,确定交付已定义功能的模块和组件,并设计用户界面-牢记前两个阶段开发的需求。设计阶段的黄金标准是创建一幅关于最终软件产品的外观和构建方式的清晰图片。

但魔鬼在于细节!伟大的系统设计人员必须了解各种软件工具的优缺点,才能以最有效的方式构建系统。例如,有些库可能非常容易被程序员使用,但是执行速度很慢。构建自己的库对程序员来说比较困难,但可能会带来更高的速度,从而提高最终软件产品的可用性。设计阶段必须修正这些变量,以使收益/成本比最大化-对于组织中特定的成本和收益定义。

这是许多程序员想要花费所有时间的地方。构建阶段是从体系结构草案到软件产品的转换发生的阶段。在这里,你的想法转化为切实的结果-看到你的创意变成现实,感觉很满足。

通过前几个阶段的适当准备,已经消除了很多复杂性。例如,构建器从所有可能的功能中知道要实现哪些功能。他们知道这些功能是什么样子,以及要使用哪些工具来实现它们。

然而,建设阶段总是充满了新的和新出现的问题。会发生一些意想不到的事情,减慢进度,例如外部库中的错误、性能问题、损坏的数据、人为错误等等。构建软件产品是一项非常复杂的工作。要写出优秀的软件,你必须使用一种人工语言,并向愚蠢的机器恰当地解释在任何可能的情况下该怎么做。一个小小的拼写错误就能决定整个软件产品的正确性和可行性。

您仍然必须针对不同的用户输入和使用模式测试软件产品的行为。这似乎是一个次要的细节,但这一阶段往往是最重要的!

事实上,这一点非常重要,以至于现在许多实践者都提倡使用测试驱动开发,在这种情况下,您甚至在没有编写所有测试的情况下就开始实现(在前一阶段)。虽然您可以对此观点提出异议-我还没有看到在实践中严格部署测试驱动开发的方法-但通常情况下,通过创建测试用例并检查软件是否为这些测试用例提供了正确的结果,花大量时间考虑不同的方法来测试您的产品是一个好主意。

例如,如果实现自动驾驶汽车,则必须编写所谓的单元测试,以检查代码中的每个小函数(一个单元)是否为给定的输入生成所需的输出。这通常会发现一些在某些(极端)输入下表现异常的故障函数。但是,即使您的所有单元测试都成功通过,您也还没有完成测试阶段。您必须测试单元之间的正确交互,因为它们正在构建一个更大的整体。你必须设计真实世界的测试,驾驶汽车行驶数千甚至数万英里,才能在奇怪和不可预测的情况下发现奇怪的行为模式。

要考虑的考试太多了,太复杂了,很多人在这里认输了。理论上看起来不错的东西,即使在第一次实现之后,在应用了不同级别的软件测试(如单元测试或实际使用测试)后,在实践中也经常会失败。

您的软件已经通过了严格的测试阶段。现在,是时候发布它并将其投入市场了。

此阶段要求您发布产品、创建营销活动、与产品的早期用户交谈、修复在暴露给用户后肯定会曝光的新错误、协调软件在不同操作系统上的部署、支持和解决不同类型的问题,以及维护代码库以随时间适应和改进。不用说,考虑到产品的各种设计选择(如使用的软件库、所需的计算能力、假定的使用模式)的复杂性和相互依赖性,此阶段可能会变得相当混乱。

你已经气馁了吗?很好,现在你知道敌人是谁了。但是请不要离开我们,因为这本书里有一个解决方案!

虽然围绕软件开发的整个过程非常复杂,但在给定的软件中也存在同样多的复杂性。软件中的复杂性-它是如何定义的?您可能已经对软件产品的复杂性有了直觉(“哇--他们是如何实现这个人脸检测功能的!一定很复杂吧!“)。

但是,软件工程中有许多度量标准,它们以更正式的方式度量软件的复杂性。

例如,有一个精确定义的术语,即算法复杂度。这与您对理解给定代码片段有多容易的直觉无关。相反,算法复杂性讨论的是不同算法的资源需求。它允许您比较解决相同问题的不同算法。例如,假设您已经实现了一个带有高分评分系统的游戏应用程序。您希望得分最高的球员出现在列表的顶部,得分最低的球员出现在列表的底部。

换句话说,您需要对列表进行排序。对100万名玩家来说,对名单进行排序比对100名玩家来说要复杂得多。有数千种算法可以对列表进行排序。随着列表输入大小的增加,一些算法的伸缩性更好;另一些算法的伸缩性更差。只要你的APP服务于几百个用户,你选择哪种算法就无关紧要了。但是随着用户群的增长,列表的运行时复杂性也会超线性增长。很快,您的用户将不得不等待越来越长的时间才能对列表进行排序。他们开始抱怨--你需要更好的算法!

图2举例说明了两种示意性算法的算法复杂性。在x轴上,它显示要排序的列表的大小。Y轴显示算法的运行时间(以时间为单位)。算法1比算法2慢得多,实际上算法1的效率越来越低,需要排序的列表元素越多。因此,你的游戏应用程序变得越来越慢,玩它的用户越多。

这就是为什么算法复杂性是一个经过几十年发展的深入研究领域,无数的计算机科学家不断降低算法的算法复杂度,以便越来越快地解决相同的问题,这就是为什么算法复杂性是一个经过数十年的进步和无数计算机科学家不断降低算法复杂度以更快地解决相同问题的原因。在我看来,在几十年的计算机科学研究中产生的算法是人类最有价值的技术资产之一。这些算法让我们能够用更少的资源解决同样的问题,不是一次,而是一遍又一遍。我们真正站在巨人的肩膀上。

有趣的是,算法复杂度并不是衡量代码复杂性的唯一指标。还有一些实用的度量,比如圈复杂度,这是Thomas McCabe在1976年开发的一种度量,它描述了通过代码的线性独立路径的数量:至少有一条边不在另一条路径中的路径的数量。例如,带有IF语句的代码将导致代码中有两个独立的路径,因此它比没有IF语句的平面代码具有更高的圈复杂度。圈复杂度是许多代码库认知复杂度的可靠度量。

然而,这种复杂性度量忽略了认知复杂性,比方说,嵌套的for循环与扁平for循环不同。在此基础上还有其他改进措施(例如NPath复杂性)。然而,重要的是,复杂性是算法理论和实现实践中的一个重要因素,几十年来,成千上万的研究人员已经对其进行了深入的研究。所有这些努力的目标都是降低复杂性,以减轻其对人类和机器的生产力和资源利用的有害影响。

世界上的信息可以被建模为一个由相互关联的信息块组成的巨大网络-没有一个信息块是独立于其他信息块的。

2012年,谷歌宣布用信息填充一种名为“知识图谱”的数据结构。知识图以网络式结构表示信息--它不是存储愚蠢的独立事实,而是维护不同事实和信息片段之间的相互关系。

然后,谷歌搜索引擎利用事实的这种相互依存关系,用更高层次的知识丰富搜索结果,并自主创建许多答案。

知识图谱的一小部分可能是关于著名计算机科学家艾伦·图灵的。在知识图谱中,艾伦·图灵的概念与他的出生年份(2012年)、他的研究领域(计算机科学、哲学、语言理论)和他的博士生导师(阿隆佐·丘奇)等不同的信息联系在一起。这些信息中的每一条也都与其他事实相联系(例如,阿隆佐·丘奇的研究领域也是计算机科学),形成了一个相互关联的庞大事实网络。您可以使用此网络以编程方式获取新信息和回答用户查询。例如,对“图灵医生父亲的研究领域”的查询会得出“计算机科学”的推断答案。

.