《构建之法-现代软件工程》读书笔记(二)

简介: 《构建之法-现代软件工程》读书笔记(二)

请问戴维.帕纳斯( David Lorge Parnas 软件工程专家) 先生: 您认为将来会有什么令人兴奋的软件工程技术出现吗?

戴维·帕纳斯: 最有用的技术不在将来,而是已经出现好些年了,只不过我们没好好用。

很多学生学了一些编程语言,读了一些技术博客,一般都豪情万丈。他们做一个项目恨不得展现自己平生所学。

再加上前沿技术,做一个轰动的创新。这固然值得鼓励,不过实践表明,这些往往都不能成功。

我们来看下成功的例子,他们是怎么做的,例如Linux刚开发的时候:

I'm doing a (free) operating system(just a hobby, won't be big and professional  like gnu) for 386(486) AT clones。

我正在为 386(486) AT clones 编写一个操作系统(只是一个业务爱好而已,没有GNU那么专业和庞大)。

管理学大师彼得·德鲁克 : Those entrepreneurs who start out with the idea that they'll make it big - can be guaranteed  failure.

那些一开始就以为自己会做大的企业家肯定会失败。

前言

开始本篇的时候,我想起我的初中,我的初中是在镇上念的,一周回去一次,我总是信心满满的指定了一个目标,回家把我背的书都看一遍,然后每次我都把所有的书都背了回去。但是你知道小孩子常常抵御不了诱惑,比如睡懒觉,玩游戏。当我睡醒的时候,我的小伙伴就来了,然后我们就去打游戏。再上学的时候,我就把这些书在背到学校。这种重复性工作应该是持续了我的初中时代。想来那个时候,背回来的书一本没看的理由就是我制定的目标太过庞大,让我觉得很难完成,但是那个时候的我显然没有意识到这一点,每次周末,我将书桌中的书全部装进书包的时候,我都是满心欢喜,幻想着自己在家里看书会让家里人开心。

我又想起我上大学的项目比赛,项目组长想做一个美妆社区商城,定位上大致相等于小红书和淘宝的结合体,介绍一下我们当时的技术背景:  Servlet、JSP、JQuery、MySQL(基本SQL语句的编写),当时刚学完这些,信心爆棚。但当时有没有完成小红书加淘宝的结合体的设计目标呢!  应该是没有完成的,大致相当于我们的目标是建一栋楼,最后建了一个摇摇欲坠的茅草屋,只等"八月秋高风怒号",就“卷我屋上三重茅”。

我想倘若目标太过庞大,总会让人产生莫名的畏惧心理,常规的做法就是将庞大的目标拆解为若干个看起来容易实现的小目标。

我想解决大问题固然让人感觉美妙,但是把小问题真正解决好,也不容易。

浅谈软件设计原则

人们在实践中碰到的需求是经常变化的,软件设计的许多原则是从实践而来,这些原则正是为了在不断变化的需求中保证程序的可维护性和效率。我们以两个软件设计原则为例,第一,单一职责原则(Single Responsibility Principle,SRP)指出:

一个模块(类)应该是只有一个导致它变化的原因,一个模块应该完全对某个功能负责。

软件设计的经典著作《敏捷软件开发:原则、模式、实践》分下下面的例子:

一个处理正方形的模块有两个功能:  ① 计算面积 ② 画出这个正方形。

这个设计让一个模块负责两个不同的职责:进行几何运算(与显示图形无关)和图形界面绘出正方形。如果一个集合计算的程序需要使用这个模块,那么它就需要同时包括图形显示的部分(因为是在同一个模块中),这是一种浪费,同时引入了不必要的依赖(因为图形显示和图形底层实现相关),妨碍了可移植性。另外,几何计算需求的改变和图形显示需求的改变都会导致这个模块发生的变化,增加错误发生的风险。

另一个例子描述了一个调制解调器的API界面:

Interface Modem{
 public void dial(String pno); // pho means port number  dial 拨号
 public void hangup();  // hangup 挂断
 public void send(char c); // send 发送
 public void recv(); // recv 接收信息
}

调制解调器这个词有点生僻,但是调制解调器是Modem的义译名,它的音译名大家可能更为熟悉—猫. Modem,其实是Modulator(调制器)与Demodulator(解调器)的合成词。调制是将数字信号转成模拟信号,解调是将模拟信号转成数字信号。

我在看到这个例子的时候,将Interface理解为接口了,我在写的时候,还在前面加上了public,如果是接口中的话,上下文就不通顺,因为上面说的是"描述了一个调制解调器的API界面",Modem是调制解调器的意思,Interface如是理解为接口的,那么这说不通。

所以"调制解调器的API界面" 应该是这么理解的,Interface是界面,Modem是调制解调器。花括号中的是API(Application Programing Interface).  接着又说道:

这个界面做了两类紧密相关的事情,连接管理(dial,hangup)和数据通信(send,recv),他们是两类职责,还是一类?

结论是: 根据具体情况分析,要看需求的变化是否导致这些操作同时变化。

后半部分讲的,我的理解是这个单一职责没有普遍的判定准则,要根据需求来去判定。那"这个界面做了两类紧密相关的事情"该怎么理解呢?我理解的界面是下面这样:

image.png

但是上面不是讲的是模块吗?这怎么又讲到界面了呢?"这个界面做了两类密切相关的事情",该怎么理解这句话?

我翻阅了《敏捷软件开发:原则、模式与实践》, 发现我可能过度理解了,这是《构建之法—现代软件工程》作者的笔误,上面的那个Modem的例子在《敏捷软件开发: 原则、模式与实践》就是Java中的接口。

那么连接管理和数据通信应该算两种职责,还是算一种?换种说法,拿吃饭这件事来说,操纵筷子和将食物放到嘴里,从吃饭的角度来说,这两类紧密相关的事情应该是划分到一个模块中。这种理所当然的划分来源于我们对吃饭这件事的准确理解,但是对于其他方向的需求呢?似乎没有绝对的说法,要根据具体情况分析,要看需求的变化是否总是导致这些操作同时变化。

写到这里突然想起微服务,起初的软件比较简单,代码量比较少,业务比较简单,用户量比较少,所以起初的软件大多前后端一体。随着硬件的快速发展,互联网的不断普及,软件逐步的再向复杂的靠近的同时,代码量也在不断的膨胀,软件开发完成又不是一锤子买卖,不像房子卖出去之后,跟开发商就关系不大了。软件开发之后还要考虑维护,这一方面软件的开发建设很像是城市建设,假如城市建设涌入了大量的人口,许多人认为这里在这里能够赚到钱,城市的执政者就会着手对城市进行扩建以适应当前城市的发展。也像是社会的发展,过去的封建社会的县令类比到现在的话是无法类比的,过去的县令身上集合了很多职位,这也许跟过去县的人口比较少有关系,随着人类社会的不断进步,原先集中在县令身上的职位被分解。这也就是对应到了微服务架构上,我们将原本集中在一个服务的应用进行拆解,将其拆解为若干个微服务,每个服务贯彻单一职责,对外提供的服务尽量不存在重复,这也就印证了本文开头引用的戴维.帕纳斯那句话:

最又用的技术不再将来,而是已经出现好些年了,只不过我们没有好好用。

我想起刚学Spring Boot的我,我是在B站看的颜群老师的视频: SpringBoot视频教程(入门篇),在视频的开头就讲到了微服务,说这是目前流行的软件架构,我当时还颇感到新奇,也许是当时并没有开发过大一点的工程,开发过的工程代码量都比较少。当时的想法是微服务架构比较适合大型项目,便于扩容和维护,但是这几年微服务似乎正在成为一种新常态,当初的论断还是正确的吗?以我目前的看法是,微服务架构正在下沉,目前业界是看重微服务架构的扩容和易维护吗?似乎并不全是,我个人的看法是看重服务的重用能力,比如认证服务,开发一次,再次开发新项目的时候就不必再开发了,重用之前的服务就好。

这种单一职责也可以从Web软件工程师的分工可以一见端倪,我们知道许多计算机的硬件能力大致以每两年提高一倍的速度发展。但是软件开发的流程却没有这样的提速过程,开发成本也没有下降,原本前端、后端、运维、DBA集一体的Web工程师被拆成四个职业:

  • 前端工程师
  • 后端工程师(也有称之为后端工程师)
  • 运维

当前的Web开发领域,DevOps十分流行,DevOps是一个合成词,是Developers(开发者)和Operators的结合,即开发和运维团队一体化。原本从后端仔身上划走的运维,又重新回到了后端仔身上。

  • DBA

这是一种拿着锤子看全世界都是钉子的想法吗?似乎是,又似乎不是。拆分与分工再人类世界可以随处见到,当你开始创业,起初只是小买卖,你大可以不必请会计注册公司,就像农村的小饭店一样,你可以是老板也可以是厨师,同时还可以是服务员和会计、保洁。但是你没想到你的厨艺很不错,好吃不贵,价钱实惠,很快你的饭店就吸引了很多人来,慢慢的你发现,一人身兼多职让你支撑不住,人实在太多了,为了不让你父母受累,你开始招人,将你身上的保洁和服务员的职责分配出去,你仍然不想请会计,你觉得的你的生意还没有那么大,渐渐的你发现一个厨子似乎有点支撑不了当前的用户量,你每天仍然被累的半死,于是你开始琢磨着请厨子、招学徒,让他们负责炒菜,你负责进菜,但是似乎还是要招洗菜的,让厨子洗菜又做菜,厨子不看堪重负,厨子提出了抗议。

于是你开始招洗菜的,很快你就想开分店,你想扩大你生意的规模,但是每天的帐让你有点头疼,你不想将自己的精力太过集中在算账上。所以你请了个账房,这个故事还会发展下去,你会请更多的人,来承接你身上的工作,但是你在分配工作的时候,两个人之间的工作会尽量不出现重合,你不会想让一个人既做厨子又做账房,你希望这个人尽可能的专业。

另一个重要的软件设计原则时开放—封闭原则(Open-Closed Principle,OCP)

软件实体应该是可以扩展的,同时是不可修改的。

具体的说:

  • 允许扩展(Open for extension)。当应用的需求发生改变时,我们可以对模块进行扩展,从而改变模块的功能
  • 不允许修改(Closed for modification)。对模块行为进行扩展时,不必改变的本身

那什么时候该使用这些原则呢?  《敏捷软件开发: 原则、模式与实践》一书也同时指出:

变化的轴线仅当变化实际发生时才具有真正的意义。如果没有征兆,那么去应用SRP,或者其他原则都是不明智的。

遵循OCP的代价也是昂贵的....., 显然,我们希望把OCP的应用限定在可能会发生的变化上。......最终,我们会一直等到变化发生时才采取行动。

我的理解是这些原则应对的是变化,开发者根据自己的经验进行了预测认为这里会发生变化,这需要设计人员具备一些从经验中获得的预测能力,有经验的设计人员希望自己对用户和应用很理解,能够以此来判断各种变化的可能性。然后,他可以设计对于最有可能发生的变化遵循OCP原则。

要预测准确这一点很不容易做到,因为它意味着要根据经验猜测那些应用程序在生长历程中有可能遭受的变化。如果开发人员猜测正确,他们就获得成功。如果他们猜测错误,他们会遭受失败。并且在大多数情况下,他们都会猜测错误。

我们该如何遵循OCP呢?我们来看下面一个例子,以这个例子来说明OCP:

image.png

image.png

image.png

这个例子同样来源于《敏捷软件开发:原则、模式与实践》,书上用的是C++来描述OCP,这里我改装成了Java。我们可以很容易的看出,如果我们再增加一种形状,Shape类中的drawAllShapes方法不用改动,就能画出新的形状。图中打印可以理解为绘制对应图形的行为。

这种设计是符合OCP的,无需改动原本的代码,我们就完成了扩展,还不会引发连锁改动。这让我想起了工厂模式,不懂什么是工厂模式的,可以参看我的文章: 《欢迎光临Spring时代-绪论》,在讲到工厂方法模式的时候,我们也是抽象出了顶层的工厂,具体对象的创建则由具体的工厂去承接,这在某种程度上也是暗合了OCP,但是做到合适的抽象却并不容易,你需要对用户和产品有足够的了解,像是POI的WorkBookFactory一样,xls是Excle03版及之前的版本的excel文件格式,xlsx是07版及之后的excel文件格式,POI的设计者并没有采取工厂方法模式,我们可以说这种设计扩展性不够好吗?

假设再增加了一种Excel类型,这个WorkBookFactory还需要改动,我想并不能这么说,我想是POI的开发者认为Excel的变化不会那么强吧,不会每年都增加一种文件格式,这来源于POI的设计者对Excel的了解,这是一种预测,同时也是在减少开发者使用POI的心智负担,过度的设计导致软件趋于复杂,增加维护和使用成本。

上面的Shape是完美的符合开闭原则的吗?似乎并不全是,我们完全可以提出一个需求,让drawAllShapes方法不得不修改,《敏捷软件开发:原则、模式与实践》 给出的变化是要求所有的圆必须在正方形之前绘制,在这种情况下,DrawAllShapes似乎无法不对这种变化做到封闭,如果你说你可以在传入List参数的时候,将圆放入正方形之前,不就可以了吗?那我这里就再提出一个需求,要求将正方形和圆形绘制在一起。除非你是穿越过来的,那么总有你没有预测到的情况,无论模块你做的是多么的"封闭",都会存在一些无法对之封闭的变化,没有一种模型可以应对所有的变化。

既然无法做到绝对封闭,那么就必须做到有策略地对待这个问题。也就是说,设计人员必须对于他设计的模块应该对那种变化做出选择。

开发者必须先猜测出哪种变化是最有可能的,然后构造抽象来隔离那些变化。

这让我想起了设计模式,大家认为这个很有用,国内的面试也很看重,这在某种程度上导致了设计模式的滥用,开发者在未对具体的业务比较熟悉之前,就开始展开了预测,开始应用设计模式,这很容易造成项目种的设计模式满天飞,我想起我前公司的资深架构师对我说的话,那天他看见我在看设计模式,便对我说,对于我这种程度的人来说,设计模式无助于我代码功力的提升,他这么多年的开发,也没用过几次。

总结一下

对业务要熟悉,这可以让你洞悉变化,预测变化,构建抽象应对变化,可以避免改动产生连锁反应,你总不希望,之前开发完成的东西,再改一遍Bug吧。对于开闭原则我之前的理解是,假如这个部分的代码已经上线平稳运行了,那么就尽可能的不要去改动他,那么这里又补充了 一下开闭原则,即根据经验预测变化,然后构建抽象,应对变化。但是应当谨慎的引入,这回增加阅读成本和维护成本,但是我常常收到的反驳意见是,我引入又不会出什么事,又怎么啦。

单一职责我之前的理解是方便重用,假如一个方法完成了A和B这两件事,我假设因为某种需要需要用到A,不需要用到B,那么这个方法我就永不了,同时也让阅读变得简单,其实我当初的想法是可以涵盖在变化中,我预测将来我会用到A,我做出的应对策略是将方法做的尽量效一点。

计划与估计

当你能够度量你所说的,并且能够用数字表达它时,就表示你了解它,若你不能度量它,不能用数字去表达它,那说明你的知识时匮乏的、不能令人满意的。—开尔文勋爵(英国物理学家)

做项目是会有工期的,常常会让开发者去估计工时,这是让我颇为头疼的一个问题,我项目经理让我估计的时候,我的脑袋是一片空白,我当时的想法是这要估计少了要加班,估计多了项目经理又不满意,于是我就请教当时公司中的前辈,我把我的担心如实说了,前辈表示没事,时间不够再要。但是我还是想估计一下,我想对自己的开发能有一个衡量。

"估计"这一技术看似容易,其实大有学问,当约翰·巴克斯启动FORTRAN项目后,他的上司会定期询问完成日期,他总是给出同样的答复:"六个月"。但实际上,项目一共花了近三年的时间。

再开始估计之前,我们需要弄清楚几个概念:目标、估计和决心。单独拎出来这三个词,我们是不会混淆的,但是在实际应用中,我们却很容易混淆这三个词。

  • 目标: 表明一个希望达到的状态。比如你希望追上一个女孩子,但你应该清楚这是一个目标,不一定能实现。
  • 估计: 以当前了解的情况和掌握的资源,要花费多少人力物力实践才能实现某事。

注意前半句以当前了解的情况和掌握的资源,我想起我上初中的时候,比较喜欢看网络小说,印象很深的一部小说是《斗破苍穹》,吞噬异火的把握,这也是一种估计。我当时很想学会这种估计,比如我们上初中的时候,有一次是初中周四放假了,

下周我就满怀期望的也希望周四也放假,我当时的估计是当时的天气,就像模像样的估计了一个八成概率。但是最后还是没有放假。

  • 决心: 保证某个时间之前完成预先规定的功能和质量。如果下的决心没有做好估计,那么大概率是实现不了的。

这种情况在软件项目中也可以看到,软件项目中的延迟更是比比皆是—为什么我们估计得不准呢?因为难么?为什么软件估计这么难呢?其实所有的估计都难,如果你只是瞎估计,没有找出估计后面的假设的话。不信的话,我们做一些估计的练习,不用搜索引擎,你估计一下下面的数目(数量级正确就行)

  • 中国的陆地边界长度
  • 非洲人口密度
  • 长江一年的流量
  • 2013年亚洲货币流通的总量

怎么样?你的估计和实际情况差几个数量级? 但是如果你把你做出估计的依据,也就是假设列出来,那这是不是会让你对自己的判断更加自信呢。软件工程专家Paul Rook说: 我们其实并不是不会估计,我们真正不会的,是把估计后面藏着的假设全部列举出来。我们平时的估计还有一个维度就是参考前人的经验。软件工程师在长期的实践中,也摸索出一套经验公式:实际时间花费主要取决于两个因素—对某件事的估计时间X,以及他做过类似开发的次数N。

Y = X ± X ➗N // Y是实际时间花费、中间的±表示加上或减去。

例如刚毕业的你被分派到了一个用户管理模块的任务,你估计需要三天,但是你从来没有做过用户管理模块,所以N就是0,高中数学告诉我们,0不能当除数,但是大学数学告诉我们极限情况下就可以,所以你花费的时间是3+无穷大?也就是说在项目给定的时间内根本无法完成,或者是3-无穷大?不但没有完成任务,还写了很多bug来,让团队花更多的时间来处理,把项目拖垮。

注意这是个经验公式,所以跟实际花费时间存在误差是非常正常的事情,这也就需要你在完成需求之后,分析原因,以便减少误差,假如一直做相同的项目,估计值会越来越准确,因为越来越熟练。但是也不会有人一直愿意做重复的是事情,这会让人厌倦。

写在最后

写到最后的时候突然觉得本文不完全像是《构建之法—现代软件工程》的读书笔记了,在写软件设计原则的时候就翻下《敏捷软件开发: 原则、模式与实践》,这在《构建之法—现代软件工程》种只花了两页来介绍,但是主要内容还是来自于它,也糅合了自己的若干感悟,希望会对诸君有所帮助。封面是在海南度假的时候,别人拍的,感觉很漂亮。

相关文章
|
1月前
|
监控 数据可视化 数据建模
软件工程之设计分析(2)
软件工程之设计分析(2)
25 0
软件工程之设计分析(2)
|
1月前
|
设计模式 关系型数据库 UED
软件工程之设计分析(1)
软件工程之设计分析(1)
29 0
|
8月前
|
算法 数据挖掘 数据库
[软件工程导论(第六版)]第5章 总体设计(复习笔记)
[软件工程导论(第六版)]第5章 总体设计(复习笔记)
|
6月前
|
存储 监控 测试技术
读书笔记第五讲:《卓有成效的工程师》
读书笔记第五讲:《卓有成效的工程师》
|
8月前
|
存储 算法 测试技术
[软件工程导论(第六版)]第9章 面向对象方法学引论(复习笔记)
[软件工程导论(第六版)]第9章 面向对象方法学引论(复习笔记)
|
9月前
|
测试技术 程序员 开发者
【软工视频】第六章软件测试
【软工视频】第六章软件测试
|
敏捷开发 架构师 数据建模
|
敏捷开发 监控 安全
|
敏捷开发 算法 架构师
【备战软考架构师系列笔记 · 001】软件工程篇 —— 软件开发方法的主要分类 ⭐
# 软件开发方法### 分类- 按功能 - 水平原型(针对界面) - 垂直原型(针对复杂算法)
196 0
【备战软考架构师系列笔记 · 001】软件工程篇 —— 软件开发方法的主要分类 ⭐
|
人工智能 测试技术
《软件测试-基础理论》- 前言
《软件测试-基础理论》- 前言
《软件测试-基础理论》- 前言