《A Philosophy of Software Design》心得 2 — 透过模组设计降低软件复杂度,从介面开始
2022年1月25日
在上一篇[《A Philosophy of Software Design 心得 1 — 写出复杂度低的软件》](https://www.explainthis.io/zh-hans/swe/a-philosophy-of-software-design/ part1)中谈到,要能够在程式逐步规模化的同时,让程式好维护、好扩张,必须要降低软件的复杂度。那篇也谈了常见的软件复杂度过高的病征。而这篇将会延续《A Philosophy of Software Design》一书当中提到的观点,进一步讨论如何透过「模组设计」降低软件复杂度,以及要做好软件的模组设计,要先从做好介面设计开始!假如对于「复杂度」这个概念不太熟悉的朋友,建议先回去看上一篇心得,在看这篇会比较清楚唷~
什么是模组设计?
如前面提到,在软件设计上,有一种降低复杂度的方式叫做模组设计(modular design);所谓模组设计就是把庞大的程式拆解成不同的模组,这些模组彼此独立(至少理想上要这样),然后当要执行软件时,再把需要的模组组合起来。这么做每个开发者要面对的,就不是整个系统的复杂度,而是仅用面对单一模组的复杂度。
举例来说,如果要打造一个电商网站,从系统的角度来看是非常复杂的,电商网站需要成列商品、需要有购物车功能,等使用者挑选完商品后,还需要有结帐与金流的功能;甚至在使用者看不到的地方,会需要有库存管理、消费者的会员管理等等不同的功能。可以想像假如要打造这样完整的一个网站,当功能越多时,越有可能让复杂度提高。
具体来说,上一篇谈到让软件复杂度过高的三大病征,以电商网站来看系统变大后很可能会:
- 改动某处时需大量改动其他部分 (例如购物车与结帐有相关联,可能改了一边导致要改另一边)
- 让其他开发者认知负担过重 (网站功能一多就代表要花更多时间理解各个功能与功能间的关系)
- 不知道不知道 (功能越多且彼此有关联时,会有很多这类的陷阱)
这时模组化的设计就能有效处理这些问题。假如上面的各个功能都变成一个个模组,那么购物车功能归购物车模组,结帐与金流功能归结帐与金流模组。负责开发购物车的工程师,不须用担心改了什么导致金流炸掉。这听起来非常的理想 (但多数时候,这会止于理想……)。
因为在现实中模组之间很难做到完全独立,更常见的是模组间相互依赖的问题。就像前面提到,购物车中的产品最终要被结帐的话,那即使拆成模组,两边也很难完全做到独立没关联。这种相互依赖的问题,就是常听到的「相依性」,也是为何资深的工程师前辈们,常常会耳提面命说要注意相依性问题,毕竟当相依性提高,整体系统的复杂度就会提高。
要能降低软件设计的复杂度,其中一个策略就是管理好软件的相依性。要管理模组的相依性,有个一定要记住的原则「介面要窄、功能要深」。这个原则符合在《A Philosophy of Software Design》一书中,又被称为深模组 (deep module) 。
介面要窄
读到这里你可能有点一头雾水,「介面要窄、功能要深」是什么意思? 这边先用自排车、手排车的例子来讲解介面要窄的意思。
对自排车与手排车来说,介面就是车子跟人互动的地方。从这个定义下去讨论,显然自排车的介面比较窄,因为驾驶员要操作的东西与步骤比较少;以换档为例,开自排车基本上不太用做什么事,要开的时候换 D 档,要倒退打 R 档,要停车时用 P 档,在换档的过程中,驾驶不用知道背后发生了什么事,轻松简单就能完成。
然而手排车则不然,手排车的介面就宽了许多,换句话说,在开车时驾驶要做的操作多很多。以换档来说,要踏离合器、要踩煞车踏板,换档后还要加油门,基本上驾驶完全需要手脚并用才能完成。这种对操作者来说,需要碰触到许多东西才能完成任务,就是介面太宽的迹象。
当介面窄 (例如自排车),使用的人可以无脑操作,不需用有太多担心;但介面宽,则需要有非常多动作,要担心的面向也因此变多许多。当介面宽,就容易让人有出错的机会。以车子为例,介面最宽的代表是 F1 赛车,F1 车手在赛车的同时,要在短时间内做非常多的操作;这导致可以从历史上看到,很多车手换车队后,需要适应新的车(适应新的复杂介面),导致刚换队前期都会表现比较差。
在看完车的案例,回到软件设计上也是相同道理,如果你写了某个模组,理想的状况是,要使用该模组的人不太需用做太多事就能用,那就是介面窄;但假如要用该模组的人,要做非常多的操作,那就是介面太宽了。就跟开车一样,软件也是介面太宽操作就容易出问题,介面窄则会像自排车一样,让操作的人更轻松。介面宽就像自排车一样。假如你设计出来的模组,让其他工程师操作时会发生类似打换档忘了踏离合器的状况,那就是介面太宽了。
假如自排车、手排车描述得没办法让你马上懂,希望上面这张比较图能让大家更直觉理解。在介面要窄这个原则,软件模组设计,跟使用者介面 (UI) 设计,是异曲同工之妙。左右两个遥控器可以达到的功能相似,但右边的使用者介面显然太复杂了。假如你设计的软件模组,对其他开发者来说,就像是在用右边的遥控器,那或许你该多想想,如何把模组设计成左边的那样,提供的功能不变,但把复杂度藏起来,简单又好用。
功能要深
还记得上面提到,除了介面要窄外,模组设计的另一个要点是功能要深。最好的模组就是能提供强大的功能,但对接的介面不暴露过多复杂度。在书中,作者提到一个深模组的典范,就是 Unix 作业系统的档案 I/O。
我们一起来看看该模组的介面。该模组的介面基本上就是这五个项目,打开档案(open)、读档案(read)、写入内容到档案(write)、调整档案读写位置(lseek),以及关掉档案(close) 。
int open(const char* path, int flags, mode_t permissions);
ssize_t read(int fd, void* buffer, size_t count);
ssize_t write(int fd, const void* buffer, size_t count);
off_t lseek(int fd, off_t offset, int referencePosition);
int close(int fd);
对于要使用该模组的人来说,要接触到的基本介面就是这五项,是不是非常的精简、介面非常地窄呢? 事实上,这五个操作背后,是靠数千行程码是来达成的!之所以说这是深模组的典范,是因为要用的人,不用管背后许多复杂的问题,只需要简单地用这五个操作即可。
举例来说,你不需用知道档案要如何在硬碟中该怎么被呈现,才能有效率地被读取。你不需要知道阶层路径名如何被处理,让系统能找到对的档案。你不需要档案如何被快取在记忆体,借此降低从硬碟读取的次数。你也不用知道如何把不同的存储装置,例如硬碟、随身碟等与档案系统整合。
上面这些都还只是冰山一角。作业系统的档案 I/O 还有非常多需要考虑、处理的事情。但 Unix 作业系统之所以能被称为好的模组设计,正是因为他让用的人不用去处理那些复杂的事情,只需简单地跟介面互动即可。就像你不需用懂微波炉背后的物理、机械原理,只需用简单按几个按钮就能操做一样。
小结
身为网页与行动装置的前端工程师,在工作中我最常遇到的介面莫过于各种 API (没错 API 的 I 正是介面的意思)。在读模组化设计这个章节时,我不禁想到之前有串过某些 API,让人能清楚且简单地串接,同时提供强大的功能,现在明白这正是符合介面窄、功能深的原则。
不过同时也想起,过去有串过一些第三方 API,在串的过程踩到各种坑,甚至有许多状况是文件中没写的,导致我忽略该注意的点;当时内心咒骂该 API 文件写得不清不楚,但现在重新思考,或许真正的问题出在该 API 的介面设计太宽了。
总结来说,希望大家读完上述摘要与心得,都有掌握深模组的定义。要做好模组设计来降低软件复杂度,就从介面要窄、功能要深开始吧!最后在《A Philosophy of Software Design》当中有这个图示,协助我们视觉化理解,推荐大家多咀嚼咀嚼。
《A Philosophy of Software Design》心得系列文
- 《A Philosophy of Software Design》心得 1 — 写出复杂度低的软件
- [《A Philosophy of Software Design》心得 2 — 透过模组设计降低软件复杂度,从介面开始](https://www.explainthis.io/zh-hans/swe/a-philosophy-of-software- design/part2)
- 《A Philosophy of Software Design》心得 3 — 写程式时该写注解 (comments) 吗? 如果要的话该怎么写?