分享

深度好文|知乎大佬带你读懂软工经典!

 nysd2012 2023-08-29 发布于北京

特约撰稿人:知乎@大海

中国科学技术大学计算机技术工程硕士。高级工程师、高级程序员,具备多年软件开发经验,目前在高校从事AI算法应用研究与教学相关工作。

最近拿到了人民邮电出版社的新书《现代软件工程:如何高效构建软件》。书不厚,228页,我打算像短视频电影博主一样,带大家快速了解这本书,喜欢的读者可以买来细读。
▲ 

点击封面即可购书,异步社区8周年限时特惠
5折
最近大家都在聊人工智能、大数据。但各垂直领域的具体业务仍然高度依赖于软件工程,大模型很明显不能解决细分领域的精准管理和可溯源管理工作,软件工程的思路和方法对软件开发公司依然非常重要。
我把作者对软件工程的看法进行了浓缩,整本书后期都是围绕这些点再展开讲:

·软件工程不是生产工程,软件工程和计算机科学不一样,是针对软件的工程学科。
·软件工程是对经验主义、科学方法的应用,目的是为软件中的实际问题找到高效率、经济的解决方案。
·软件工程不是要我们成为软件领域的物理学家,不用过高的精度,务实就行了。
·软件工程的专家是学习专家和复杂性管理专家。
·软件工程的5个原则是:迭代、反馈、增量主义、实验、经验主义。
·管理复杂性的5个要素是:模块化、内聚、关注点分离、抽象、松耦合。
·开发的5个理念是:可测试性、可部署性、速度 、控制变量、持续交付。


什么是软件工程?

作者认为软件开发是设计工程,不是生产过程,和造桥等建设工作不同,造出来的桥不能迭代,但数字产品可以。所以软件开发是一项发现、学习和设计的过程,不是生产工程技术。
作者举了4个例子说明软件工程中的一些必要性

· 用步枪可互换零件实例说明标准化和复杂性管理的必要性;

· 用水管的漏洞监听器说明实行精准测试的重要性;

· 用马斯克的星舰选用不锈钢材料说明工程决策是实验性的、不可预料的,需要反复测试、迭代和权衡;

· 用莱特兄弟发明飞机时反复尝试和犯错来说明工程和理论均必不可少,工程和工艺都很重要。

作者在全书中反复强调具备高稳定性和高吞吐量的才是高效能团队:

· 稳定性:变更失败率(修改带来的Bug)低、服务恢复时间快。代表着软件质量提升。
· 吞吐量:变更前置时间短(从想法到有效运行的时间)、部署效率高(代码放到生产环境的时间)。代表着软件开发效率提升。

很明显,稳定性和吞吐量之间需要权衡:

· 比如我们为了增加稳定性,增加了审查和签核流程,吞吐量降低怎么办?
· 比如我们用了手动测试,虽然吞吐量降低了,但稳定性会不会提升?是不是值得?

所以作者的结论是:
具体哪个方案更好,不必纠结,用证据和数据进行决策即可。

优化学习

1

迭代

迭代是AI领域的工作机制:一系列重复性操作产生的结果逐步逼近所期望的结果。
作者对迭代进行了定义:缩小我们的工作范围,以更小的批量进行思考,认真对待模块化和关注点分离;将工作分解得足够小,就可以在一个sprint中完成功能点;在软件开发中,意外、错误和误解正常,快速迭代可以保护我们自己。
迭代做得好,可以让开发做到细粒度、过程可控,常用的测试驱动开发方法是:

· 红:编写测试,运行并看它失败;
· 绿:写足够多的代码让测试通过;
· 重构:提交代码并测试,代码应该清晰、可读、优雅、通用,变更后再进行测试。

2

反馈和经验主义

软件开发是一个不断尝试的过程,不存在一个可以精准预测一切的模型。因此不要弄模型,而要经验主义和实验主义,通过反馈进行持续修改。
如果要把扫帚放在地上,有两个办法,你觉得哪个更实用?

· 精准计算出扫帚的重心一次到位;
· 试试看,不断尝试、反馈、调整、再试试,最终到位。

经验主义也是科学,所有的假设和理论必须通过对自然界的观察来检验,而不是靠先验推理、直觉或者启示。经验主义需要我们组织信息,组合成我们可以通过实验进行测试的理论。不要相信直觉,而要进行量化分析。
我总结一下作者定义的反馈:

编码中的反馈:IDE内进行测试驱动的编码,不断测试,测试过程短,反馈迅速。

集成中的反馈:实际上就是分支合并,一天至少合并一次,有必要每天提交一次变更。

设计中的反馈:要先编写测试用例,实现模块化、关注点分离、高内聚低耦合和信息隐藏。

架构中的反馈:持续交付,构建单体系统(紧耦合)或者使用微服务(需要分别测试)。

倾向于早期的反馈:使用强类型和编译查错、进行自动化测试和验收测试(早期发现问题叫测试左移)。

产品设计中的反馈:从生产系统中收取信息,获得反馈以帮助设计下一代产品和服务。

组织和文化中的反馈:用一些合理的规则来减轻评测的主观性。

3

增量设计实验性

增量设计主要使用模块化设计,组件经过改进可以自由替换以确保更好的性能。开发过程就是在迭代中进行细化和改进,一部分一部分地提交增量,持续构建并发布,系统交付更快。
作者认为增量主义和模块化适合小团队,可以减少团队间协作,每个团队可以增量快速实现交付。当团队人员数量在8人以下时非常适合这种模式,人员和团队有更大的自主权。
增量和实验性密不可分。实验是为了支持、反驳或者验证一个假设而开展的一系列操作或者活动。
实验性有4个要素:反馈、假设、度量和控制变量
为了实现增量设计,自动化测试必不可少,也就是必须进行测试驱动开发,比如验收测试驱动开发、行为驱动开发等。虽然测试会带来一些时间成本,但总体来看可以快速增加生产力。

优化管理复杂性

1

模块化

模块化的特点是每个模块中代码要少,接口不能太复杂,代码要简单清晰。
模块化组成的复杂系统要可测试。我们要设定测量点,要将测试数据注入被测系统,要有桩模块和伪外部系统。其中与第三方系统的交互和集成点都变为测量点,测量点丰富就便于开展自动化测试。
模块化可以提高可部署性并形成部署流水线:在生产线的一端提交,在生产线的另一端发布。
团队也要模块化,规模也不能太大,那么团队多大比较合适呢?
如果2个比萨不足以喂饱一个团队,那这个团队就太大了。
——亚马逊·贝索斯

一项历时9个月的研究表明,5人以下的团队比20人的团队创建100 000行代码只多花了1个月时间,也就是说小团队的生产力几乎是大团队的4倍。
所以作者的建议是:建立协作程度最低的团队,解耦系统,提升效率,增加可伸缩性。

2

内聚

内聚就是把不相关的东西进一步拉开,把相关的东西更紧密地放在一起
——肯特·贝克。

内聚就是把不相关的东西进一步拉开,把相关的东西更紧密地放在一起——肯特·贝克。
内聚是每个计算机专业学生都懂的术语,就是不要把代码堆到一起,比如以下代码的内聚性就很差,这叫代码堆砌:

· 读入文件代码

· 排序代码

· 写入文件代码

如果把类似功能代码模块化,内聚性就明显增加了。

· 读入文件函数

· 排序代码函数

· 文件写入代码函数

作者认为,写代码的主要目的是把想法传递给他人。在软件开发领域,并非越复杂的代码效率越高,简单清晰的代码可能更好。
如何提升代码的内聚度?可以专注于代码外部的API和接口设计,测试驱动可以帮我们找到内聚的最佳点。
图片
以上面这段代码为例,它的内聚就有问题。第3行的存储和外部系统有关系,读写出错会影响正常的程序逻辑,但是细节确实已经在一定程度上屏蔽了。

3

关注点分离

关注点分离是将程序分成不同部分,每个部分处理一个单独的关注点的设计原则。
写代码的时候,做到一个类、一个事、一个方法、一件事情(帮助实现高内聚、低耦合及模块化)
关注点分离以后,代码可读性更好,业务变动时代码量变化就少。
怎么做到关注点分离?模块化当然是一种做法,另一种做法是依赖注入。
依赖注入,即把一段代码的依赖项作为参数提供给它。
讲得直白一点就是参数化实现逻辑上的接口并实现业务分离。比如给函数传一个文件名,或给函数传一个监听器,这样可以兼顾耦合度和内聚性。

4

信息隐藏和抽象

在研究对象或者系统时,去除物理的、空间的或者时间的细节或者属性,把注意力集中到更重要的细节上的过程。
作者认为,程序员不能单纯谈代码量,还要高质量交付,不能逃避测试和重构。其实软件开发的速度和质量之间没有权衡:代码质量不好,软件开发也不会快。因为成本包括重构、测试、设计和修Bug。
也就是说这些所谓的“杂活”都是必须有的,它们是软件开发的基础,不是在浪费时间或者精力。
作者明确反对各种高大上的设计。他认为对于软件工程而言,实用和能解决问题才是王道,着眼现在、迅速出结果才是正道。
如果一定要说面向未来,那至多可以增加编码的灵活性,修改起来轻松一些就行。
如何实现抽象?先写测试(规范),这也叫轻量级的契约式设计 (DEISGN BY CONTRACT),测试用例实际就是一种契约。
抽象不可能完美。有些本来应该隐藏的细节没有抽象好,露出来了,这叫抽象的泄露,要避免。
在一个要求低延迟系统里,做了Java的垃圾回收,延迟时间就不确定了,这就是一种泄露。
在一个证券业务系统出错时,返回的是HTML标准错误代码,这和领域业务就无关了,也是一种泄露。
此外,抽象要看场合
比如城市的地铁线路图并不精确显示两点之间的实际地理情况和实际距离,仅体现一个逻辑概念。但是步行地图却刚好相反。
因此如果在2个地铁站点通勤,地铁线路图极好;如果要去某个饭店吃饭,那还是步行地图更适合。
作者举了一个例子:
图片
这个代码是有抽象泄露的——在存商品的时候如果内存不足,存储就会失败。
可以进行如下的修改:
图片
这段代码就比较好,可以根据错误情况在逻辑上进行回退。同时,这里也没有用一些第三方库的代码进行数据库连接和数据库读写,比如conn.open()之类的函数,这样就隔离了第三方系统和代码。

5

管理耦合

耦合指软件模块之间的相互依赖程度,对2个例程或者模块之间紧密联系程度的度量标准,模块之间关系的强度。
作者倾向于松耦合,但也要理解在做出选择时的权衡:松耦合会带来复杂性(微服务),也会增加代码量。
在开发大规模软件的时候,紧耦合是非常必要的。因为大型团队之间的协调很重要——有人修改了代码,别人必须知道。怎么做到开发时的紧耦合呢?持续集成基础的设施很重要,有集中的存储库、编译库、持续集成和自动测试机制等,比如大家都用GitHub或vsts等。
目前最好的策略是微服务。这种小的、专注单一服务、自主、可独立部署的松耦合架构很理想。但是微服务实现非常复杂,而且微服务互相独立,打破了我们想要的开发上的耦合,因此微服务也不是万能的。
这里提一个问题大家想想:
对于重复代码,是不是要实现共用?
大家会说:这还用说?代码难道复制N遍么?对的,这叫DRY(Don't Repeat Youself)。但有时候耦合的成本会超过复制的成本。比如对于不同的微服务,是不是不要用复杂的机制在微服务之间共享代码呢?还是复制一份代码呢?

支持软件工程的工具

工程学科的工具
▮ 工具1:可测试性
因为可测试的代码是内聚的、关注点分离的、适当耦合的,所以适合软件工程。
比如有一个car.start() 要测试,如果什么东西都不暴露,怎么测试?
图片
解决办法是做依赖注入,加个汽车引擎:
图片
对engine而言,可以用:
图片
通过在测量点上注入依赖进行测试,灵活且清晰。
▮ 工具2:边缘测试
当系统与现实世界交互,比如显示、打印的时候,测试会比较困难。现在居然真的有公司拿摄像头对准屏幕进行识别,但更好的办法是用 showStuff() 之类的方法,把输出临时放到字串里,和具体设备分离以供测试。
工具3:提升可测试性
从测试中得到快速、高效和清晰的反馈。做法是划分系统,做小的用例。

现代软件工程师

作者认为,工程学是对经验主义和科学方法的应用,是为实际问题找到高效的、经济的解决方案。所以我们是在信息不完全的情况下做决定,基于科学推理不断尝试新方法以提升效率。
所以软件开发机构的组织应该怎么架构?大多数公司是“OBAP”架构,即组织、业务、架构和过程。先有组织,再决定业务。
而更合理的公司应该是“BAPO”架构,即业务、架构、过程和组织,先有业务。这样的组织架构适用于业务。
这里我们不妨思考一下,是好的结果重要,还是组织机构和机制重要?

结语

老外的书相对来说话比较多,一个观点前后反复讲,对新人非常友好,对熟手而言就需要提炼。
书中高度强调模块化、关注点分离、高内聚、低耦合、持续交付,极其强调测试先行,喜欢先写测试用例再编码,多次快速迭代,达到软件开发高效率和高质量的理想状态。
推荐大家阅读这本《现代软件工程:如何高效构建软件》,对于提升对软件工程的理解,大有裨益。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多