Nintil –关于软件工程的思考

2021-01-29 22:35:01

尽管我现在尚未受聘为软件工程师,但最近几年我一直在编写各种代码(作为数据科学家,ML工程师和软件工程师)。自然地,我就是我,我不仅做过这件事,还反思了这件事。这些问题包括什么是好的软件,什么是好的软件工程师,意味着如何召开会议等等。这是一些想法。

在许多情况下,双方可能都认为分歧不大。我可以认为某些东西是80%好的,而您认为它只是75%,我可以认为您的解决方案的反面。但是我们可以同意,解决分歧的成本可能会抵消实际选择最佳解决方案的收益。所以,我不同意,但让我们继续吧。在这种情况下应该做的事情,如果需要的话可以掷硬币。由共识驱动的决策在某些阶段可能会很好,尤其是如果与会议周围良好的流程配合使用,但可能导致僵局。

我当然不是提出建议的第一个人。不同意并承诺原则,尽管我最近才知道这个名字。

会议可能会很糟糕,会议可能会很棒。人们之所以会召开不好的会议,是因为人们露面时没有考虑会议中要讨论的内容。会议的目的不是思考吗?这是查看会议的一种方式,也许是最常见的方式,而且这是一种懒惰的默认方式,因为它们不会向您施加家庭作业。但是,要进行良好的决策,实时地进行思考,要有许多声音要被顺序地听到,并且时间有限。我多次看到会议永远拖延着分歧不断出现的地方,或者我们只是互相交谈而已。

我也尝试过一种更好的召开会议的方法,就是整理会议之前结果应该是什么。如果是架构设计会议,请进行工作,通常会议所有者可以完成大部分工作,即使他们知道这样做并非100%完美。然后在会议开始前一周征求反馈。将反馈合并到文档中,重复一次。记下分歧;有字面上的"常设分歧"文档中的“关键”部分,以及尚未达成共识的关键点,以及其背后的理由。让所有人复习分歧并有时间这样做,在(数字)纸上为会议设定了一个更清晰的目标:消除特定的问题。在会议期间,主持人将逐一讨论这些问题并要求作出决定,然后以粗体突出显示选择了哪个选项。然后,我们将移至下一点,依此类推。每个人都喜欢这种会议。

缺乏所有权是万恶之源(好吧,这里有些夸张)。每个人的问题都不是问题。在文档中,缺乏所有权意味着找到过时的文档,没有人去修复它。或更糟糕的是,没有适当的系统来强制要求文档是最新的(即,每个文档都可能在文档发布后的6个月内消失,除非上传或创建文档的人在系统发送的电子邮件中另有说明)六个月后,文件便不能免费交房租)。或者拥有一支致力于不懈地汇总信息的团队,以便每个人都可以看到所有内容。在伦敦电动车公司(LEVC)的第一份工作中,我们遇到过类似的事情,我怀疑这在汽车或航空航天中可能比在软件或生物技术中更常见。

苹果公司有一个非常聪明的想法,那就是为所有事情定义直接负责的个人(DRI)。用负责任的名字代替模糊的团队或"流程"使更改变得容易。我认为许多人都不愿意将自己犯的错误归咎于个人,但是适时的责备(反馈有关犯下的错误,可能会导致严重错误等)既可以帮助被指责的个人(他们可以知道如何改进),又可以整个团队取得成功。

我曾经想过的一件事是,许多涉及估计的决策都可以押注,无论是少量资金还是某种代币。如果您说X将在一周内完成,并且需要更长的时间,那么您将输掉。这可以通过高估事情花费多长时间来解决,但类似的事情似乎是正确地做出更好的决策的正确方法。

内部Slack中提出的每个问题都是策略失败。这意味着现有的信息系统无法提供答案,并且用户退回到手动询问蜂巢思维的默认知识。这有很多问题:第一,问题和答案之间的延迟更长,特别是如果知道答案的人在另一个时区。第二,它以分布和不连贯的方式包含隐性知识:如果没有一个正确的答案,那么就会有很多答案,这可能导致分歧和错误的决定。取而代之的是,理想情况下是一个集中的信息存储库,其中每个Q都有一个且只有一个A,并且有一个专门致力于使各种系统的所有者真正地整理其知识的团队。这应该与上面的所有权系统配合良好。

好的软件是可读,快速,灵活和可扩展的代码。在这些唯一的速度中,有一个关于如何测量的普遍同意的速度。其余的都是模糊的,就像生活中的大多数事物一样。

可读代码在很大程度上取决于编写者。 Dyalog在我看来就像Brainfuck,并且Lisps中的许多括号会使非lisper的人难以阅读代码(我做了实验;我花了一些时间学习基本的Clojure,而当我仍然不是Clojurian时,括号变成了更少的问题)。直到人们知道它们是如何工作的,Rust的生命才显得晦涩难懂。

灵活的代码是更易于扩展的代码。这很难量化,但是任何编码的人在看到它时都知道。给定的一段代码只需两行更改即可轻松完成一些新工作,或者可能需要一千行更改才能再次工作。前者比后者更灵活。而且,这种灵活性不应该以可读性为代价,尽管有时候是这种情况。

可伸缩代码是无论输入大小都适用的代码,可以在一台或多台机器上实现。

这些问题的程度取决于开发人员(可读代码取决于个人喜好),如果最终结果或多或少是固定的,则实际上并不需要灵活性。但是在不断适应的初创公司中确实很需要。在那种环境下,为了牺牲额外的灵活性可能会牺牲速度。

在没有任何约束的情况下应该编写的代码是好的代码,但是现实生活中的情况意味着正确的做法是权衡取舍并向前发展。这些决定是软件工程经验的核心。

静态类型很棒。早在Aiden.ai的Python代码库中,我们就决定尽可能多地使用近视进行静态类型化。所以不要写像

甚至更进一步,在某些情况下,我们将使用新类型来使这些类型注释更有意义,使用数据类将数据捆绑在一起,并使用详尽的枚举来确保处理枚举的所有变体,例如:

class Operation(Enum):Multiply ="相乘"添加="添加" Value = Union [int,float] def assert_never(x:NoReturn)-> NoReturn:引发AssertionError(f"无效值:{x!r}")def do_the_op(a:Value,b:Value,op:Operation)->值:如果op是Operation.Multiply:返回a * b否则如果op是Operation.Add:返回a + b else:assert_never(op)

因此,如果我们说删除一个操作或添加一个新操作,则mypy将迫使我们进行处理。这是动态执行的,但最重要的是也是静态执行的,因此,如果缺少变体,代码将无法通过测试。

所有这些键入(加上我编写的自定义熊猫typechecker,但这是另一个故事)使重构相对容易,并在需要时添加了新功能。没有类型的盲目飞行将是巨大的痛苦。当您最不期望Python时,Python会把它炸开。从第一天的类型开始,我就不会后悔,它并没有使编码变慢并且节省了大量时间(或者我们可以想象!)。

现在似乎到处都有类型的趋势。 Javascript死了(对任何认真的开发人员而言),让Typescript崛起,而Ruby仍在Sorbet类型检查器中,获得了Crystal。

就像一个聪明人曾经说过的那样,Python及其后果对人类来说是一场灾难。即使进行所有键入操作,Python也会使您的脸部膨胀。类型检查器可能很高兴,但是并不能保证如果它认为某事是类型T,那么实际上,也许您认为是数字的实际上是一个字符串,并且由于鸭子输入而需要花费一些时间。函数要求该错误显示出来。

我相信在编写大量代码和编写正确的代码之间要进行权衡。一天之内,您可以编写X行代码,也可以编写X / 2。程序员B的两天时间将与程序员A的一日时间相同,但后者可能会引入较少的错误。您可以编写或多或少的测试,或多或少可以确保刚编写的内容是正确的。

您可以从这里的错别字数中看出,我是一个YOLO程序员,而不是一个冷酷的程序员。我是曾经在东京的一家饭店吃晚饭的时候曾经用我的手机从一个GitHub修补程序生产产品的人(那个人确实工作了!)。

YOLO编程在编程语言无法胜任时会遇到更多问题,相反,它会从强类型语言(如Rust;也可能是函数式编程)中受益匪浅。如果"可以编译,则可以正常工作"这个主意不是引用正确的,它比"如果进行类型检查是否正确"更正确。在Python中。在Python的情况下,代码似乎足够好了。以及(不足的)测试和(不完美的)类型检查器对您的影响,所有让错误流过的缺陷是代码审查并不完美。在Rust案中,对我来说似乎没什么问题。将使rustc开心。许多错误(逻辑错误)是很难避免的,但是在无数次的时间内,我已经看到可以通过适当类型捕获通过这些错误的错误。

更多的代码是错误的代码(平均而言),因此进行更安全的YOLO编程的另一种方法是设计代码,以便随着时间的推移,只需添加更少的代码行。这个怎么做?如果可以实现,则为它们提供DSL和编译器。 DSL是(如果做得很好的话)一种简洁,可读,模块化的方式,用于描述关注领域的业务逻辑,并使尽可能多的无效可能性无法表示。回到Aiden.ai,而不是编写大量数据管道,我最终编写了一个通用的数据转换系统,该系统将在运行时获取配置并任意转换数据。这样,我们就可以以相对较少的工作将新的数据源插入到通用框架中。

长函数有多长时间?一个程序可以是一个很长的函数。也可能是许多微小的功能,每个功能都做一件非常简单的事情。最好的是什么?您是否应该大量内联?您是否应该遵循“单一责任原则”其最终结果。我个人喜欢可以自上而下阅读的更长的功能;即使模式在函数中重复出现,而不是在主函数之外定义一个新函数,我还是愿意使用闭包或将此新函数写在原始函数中。您可以抓住的功能越少越好,这使得查找所需内容变得更加容易。至少对我来说,拥有许多较小的功能使跟踪更大的代码主体正在做的事情变得更加困难,并且在删除旧代码时,很难错过这些辅助功能。缺少检测未被调用的死语的工具,即使它们不再有用,它们也可以在那儿徘徊并得到维护。较短的函数也可以隐藏代码的实际作用。如果无辜的getPersonFromId正在执行数据库调用怎么办?您要映射到数据库并向其发送垃圾邮件吗?还是...写一个可以调用一个新函数?使用较少的抽象层,就更容易看到。

但是功能短小的捍卫者会争辩说,他们使代码更具可读性。我看到在某些情况下可能会怎样,这使我意识到这可能与认知差异和各个层面的价值观有关。您有多少工作内存,或者偏爱具体性还是抽象性,或者您信任正在阅读的代码有多少?最终,答案是正确的函数长度是团队在给定代码库上的功能。

据说过早的优化是万恶之源。但是,后期优化也可能有害。想象一下,您到了感觉一切缓慢的地步。您找出原因并找出原因:数据库未正确索引,查询未优化,查询反复执行或编写了较慢的算法或函数。在那个阶段可能很诱人,只是在问题上扔下云,然后复制数据库并启动另外4台服务器。现在,您必须支付所有费用并处理数据库同步。我认为在设计时要考虑到未来的性能需求,这是一个合理的中间立场,而不是将其作为一个完整的事后思考方法。

我已经读了许多有关软件的书。软件设计,简洁代码等的哲学。我还观看了许多有关软件的讨论。由于某种原因,我最终非常欣赏游戏开发人员(乔纳森·布洛,凯西·穆拉托里,约翰·卡马克)的观点。我想其中一部分是因为他们的情况而定解决其中许多问题的方法。可以认识到函数式编程的优点,或者在这里或那里拥有类,或者编写单元测试而不将所有内容都变成纯函数,进行完整的OOP或花费数天的时间来测试可以用较短的测试套件完成的代码的优点。如果您说听起来很合理的SOLID,则OCD方式的“单一责任原则”只能由一元函数满足。其他所有事情都要做的不只是一件事。在编程中看似硬性规定的事情与编程艺术的模糊性不符。

我们对编程真正了解什么?好吧,基于证据的软件工程就是一回事。有关于它的书。有谈论它。但这不是一件大事。软件工程就像教育一样,它是现代社会的重要组成部分,但是很少有资源致力于使其变得更好,这与生命科学说的不同。 Twitter最初是用Ruby编写的,然后过渡到Scala。如今,许多人使用的是Kubernetes支持的微服务架构,而不是整体架构。那是件好事儿吗?谁知道。我的猜测是,巨石可能被低估了。