软件复杂性简洁之道 - 设计原则篇

2019 年 4 月 14 日 云栖社区

作者:聂晓龙


前言


软件之所以这么有魔力这么繁荣,在于软件的灵活性,也正因为软件的灵活性导致了软件的复杂性。绳子灵活而方便,它能360度无死角花样系东西,但有时为了解开它,相信也没少让你烦心过。


We call it ‘soft’ because we can change it and reshape it easily. 
--- Michael J. Hicks


从我们学会什么是int什么是long时,老师就教会我们什么时候该用int什么时候适合用long。当我们知道什么是面向对象时,我们就一并了解到了抽象、封装、继承和多态。似乎我们在学习如何编写软件的同时,我们就一直伴随着如何设计软件。


那为什么我们要设计软件?软件设计的最大目标,就是降低复杂性。那什么又叫复杂性?


"Complexity is anything that makes software hard to understand or to modify."

--- John Ousterhout 《A Philosophy of Software Design》 


译:所谓复杂性,就是任何使得软件难于理解和修改的因素。


面向过程就可以实现所有的软件需求,但我们依然又衍生出了面向对象。软件设计囊括很多,命名、函数、规范、建模、设计模式、设计原则等等,这里我们重点聊一聊设计原则在降低复杂性上是如何表现的。


复杂性来源


斯坦福的Ousterhout教授指出,复杂性的来源主要有两个:代码的含义模糊和互相依赖。



或者我们可以用这2个词来表示Ousterhout教授的理念,那就是created & spread。 


复杂性不仅在于它本身复杂,并且还会递增。如果做错了一个决定,导致后面的代码都基于前面的错误实现,只会越来越复杂。


复杂性治理


找出易变化的部分,合理抽象。用抽象构建框架、用实现扩展细节。


SRP 单一职责


单一职责原则基于康威定律推论,核心思想是每个软件模块有且只有一个被更改的理由。职责过多,引起它变化的原因就越多,将导致责任依赖增加耦合性



用户信息需要新增“爱好”,UserInfomation负责处理。

 
数据的信息转换与拼装逻辑,UserAssembler职责所在。 


当我们需要调整用户的隐私数据,Privacy能帮到我们。


单一职责让我们在应对变化时更从容,将复杂进行拆解,修改一个功能时,也可以显著降低对其他功能的影响。 


但“单一”其实也有着各种各样的争议,何谓“单一”?Privacy.clear叫单一吗,它专门负责隐私数据的去除,但或许隐私数据的去除又分很多种:个人信息、公司信息、行为信息。那我们再分为Privacy.Personal.clear?如同抽象一样,极度抽象=没有抽象。“单一”也要按我们所面临的需求与场景来看,软件工程没有银弹。


OCP 开闭原则


对扩展开放,对修改关闭。如果有新的需求和变化可以对现有代码进行扩展,以适应新的情况.而不是对原有代码进行修改。 


记得老师给我们上的第一堂课就这么讲过,“软件工程的世界里,不存在没有bug的程序。修复旧bug的同时,就有引入新bug的可能”。“为什么这个会有问题,我这几天都没怎么动它”,我们在日常生活中就有这样的概念,没碰的东西,出问题的概率应该会很小。开闭原则也是如此。


在软件的生命周期内,因为变化,升级和维护等原因需要对软件原有代码进行修改,可能会给旧代码引入错误,也有可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。解决方案就是:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。



装饰者模式,策略模式等都很好的遵循了开闭原则。开闭原则的核心是通过合理的抽象,利用面向对象的多态特性,来完成对扩展开放,对修改关闭。而何时进行抽象、何时不需要抽象、如何抽象也是一个见仁见智的过程,不存在绝对的方法论。


LSP 里氏替换原则


所有引用基类的地方必须能透明地使用其子类的对象。由Barbara Liskov在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。 


有了抽象就一定对应着具象,抽象与具象则是继承与派生的关系。我们知道继承有很多好处,它帮我们提高了代码的复用性,但继承也是侵入的,继承意味着必须拥有父类的所有属性和方法,降低了代码的灵活性,有时会形成对子类的一种约束。不合理的继承与派生关系,不仅没有解决复杂性,反而可能会无端增加新的复杂性。



现在有一个方法birdLetGo,统一处理鸟儿去北京的行为



很明显,这里birdLetGo的代码违反了开闭原则,代码中会充斥着各种instanceof。开闭原则的核心是基于多态、基于抽象,而里氏替换原则保障了这一点“所有引用基类的地方必须能透明地使用其子类的对象”。如果我们不能基于多态来构建我们的架构,那我们将没有架构,而父类此时如果不能安全放心的替换起子类,那我们的多态也将毫无意义。LSP是使OCP成为可能的主要原则之一,正是因为子类的可替换性,才使得父类模块无须修改的情况就得以扩展。


复杂性隔离


说完复杂性治理,我们再谈谈复杂性隔离。既然我们都将复杂性治理了,为什么还存在隔离一说?用户在使用微信时,觉得很轻松,但微信的工程师们的工作可不轻松且很复杂。这就是复杂性隔离的一种。



这段代码出自Java.Lang.Integer。很明显,这些代码是复杂的,不结合上下文, 不弄懂这些数字的含义,很难了解它的作用于价值。但我们在使用Integer的时候,似乎没有印象我们会用到这里的代码。这,就是复杂性隔离。


Isolating complexity in places that are rarely interacted with is roughly equivalent to eliminating complexity.

-- John Ousterhout 《A Philosophy of Software Design》 


译:如果能把复杂性隔离在一个模块,不与其他模块互动,就达到了消除复杂性的目的。


接口和实现



模块分成接口和实现。接口要简单,实现可以复杂。好的class应该是"小接口,大功能",糟糕的class是"大接口,小功能"。好的设计是,大量的功能隐藏在简单接口之下,对用户不可见,用户感觉不到这是一个复杂的class。最好的例子就是Unix的文件读写接口,只暴露了5个方法,就囊括了所有的读写行为。


DIP 依赖倒置原则


高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。 


依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。


依赖倒置告诉了我们模块与模块间正确的依赖方向,实现类是复杂的,对外而言我们不需要知道这些复杂性,如同上面unix-IO的例子。我们将复杂性隔离在实现类,通过抽象,建立新的依赖关系。由于抽象将高层和细节彼此隔离,所以代码也更容易维护。


ISP 接口隔离原则


即使开放接口也不能随意开放,接口隔离原则认为“多个特定客户端接口要好于一个宽泛用途的接口”。不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。


根据接口隔离原则拆分接口时,必须首先满足单一职责原则。ISP最大的好处是可以将外部依赖减到最少,你只需要依赖你需要的东西,这样可以降低模块之间的耦合,达到复杂性治理的效果。


抽象原则三剑客


上述所有的设计原则,都离不开抽象,而代码抽象也有三个原则(PS.突然想起这句话“如何得出方法论,也是有一套方法论的”)。不抽象、盲目抽象与随意抽象,都可能会让我们在解决旧的复杂度上,引入新的复杂度,甚至更甚。


Don't repeat yourself


DRY原则出

自 Andrew Hunt 《The Pragmatic Programmer》,特指在程序设计以及计算中避免重复代码,因为这样会降低灵活性、简洁性,并且可能导致代码之间的矛盾。


系统的每一个功能都应该有唯一的实现。也就是说,如果多次遇到同样的问题,就应该抽象出一个共同的解决方法,不要重复开发同样的功能。


You Ain’t Gonna Need It


出自 Ron Jeffries 《Extreme programming》 


YAGNI原则指的是你自以为有用的功能,实际上都是用不到的。因此,除了最核心的功能,其他功能一概不要部署,这样可以大大加快开发。 


但是,这里出现了一个问题。仔细推敲的话,你会发现DRY原则和YAGNI原则并非完全兼容。前者追求"抽象化",要求找到通用的解决方法;后者追求"快和省",意味着不要把精力放在抽象化上面,因为很可能"你不会需要它"。所以,就有了第三个原则。


Rule Of Three


出自 Martin Fowler 《Refactoring》


第一次用到某个功能时,你写一个特定的解决方法;第二次又用到的时候,你拷贝上一次的代码;第三次出现的时候,你才着手"抽象化",写出通用的解决方法。"三次原则"是DRY原则和YAGNI原则的折衷,是代码冗余和开发成本的平衡点,值得我们在"抽象化"时遵循。


可以看到,哪怕是这些大家,在面对抽象问题时,也没有一个非黑即白的答案。抽象非常简单,因为哪怕是没学过软件工程的人同样会抽象。但抽象又很难,优秀的抽象模型与抽象经验,是很难从书本上快速获取的,不仅需要你有优秀的软件工程的知识,还需要你又贴近业务的理解与经验,甚至在某种程度上,属于哲学的一种。


优秀的设计原则


除了上述所说大名鼎鼎的SOLID原则、抽象原则等,还有很多优秀的设计原则。了解它们能帮助我们更有效率的编写代码,帮助我们成为一名更优秀的程序员。


KISS 简单原则


keep it simple, stupid

--- Robert Kaplan


KISS原则全称为

(Keep It Simple and Stupid ),有趣的是它并不是由软件工程师开创的,KISS原则最早由卡普兰在著名的平衡计分卡理论中提出。把事情变复杂很简单,把事情变简单很复杂。好的目标不是越复杂越好,反而是越简洁越好。这就是KISS原则。 


最容易的莫过于忙忙碌碌,最困难的莫过于卓有成效。化繁为简,善于将复杂的问题简明化、简单化,是防止忙乱、事半功倍的法宝。不能将问题简单化,迷惑于纷繁复杂的现象,甚至让复杂的问题更加复杂,只能陷在问题的泥潭里走不出来,结果工作忙乱被动,办事效率低下。只有善于把复杂的事物简明化,办事才能又快又好。


POLA 最小惊讶原则


"If a necessary feature has a high astonishment factor, it may be necessary to redesign the feature.


译:如果必要的特征具有较高的惊人因素,则可能需要重新设计该特征。


--- Michael Frederic Cowlishaw"


Principle of least astonishment,最小惊讶原则通常是在用户界面方面引用,但同样适用于编写的代码。代码应该尽可能减少让读者惊喜。也就是说,你编写的代码只需按照项目的要求来编写。其他华丽的功能就不必了,以免弄巧成拙。


WCFM 代码维护原则


"Always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live."


译:如果一个维护者不再继续维护你的代码,很可能他就有想杀了你的冲动。


Write Code for the Maintainer,代码维护原则阐述,一个优秀的代码,应当使本人或是他人在将来都能够对它继续编写或维护。代码维护时,或许本人会比较容易,但对他人却比较麻烦。因此你写的代码要尽可能保证他人能够容易维护。 


这里应用了它的原始出处,但我还想再引用一个大牛的经典语录。


写下一行只要1分钟,但未来会被一代代工程师读很多次、改很多次。代码的可读性与可维护性,是我心目中好代码的第一标准。


--- 鲁肃


APO 避免过早优化原则


"Premature optimization is the root of all evil."

-- DonaldKnuth


译:过早的优化是一切罪恶的根源


Avoid Premature Optimization,避免过早优化可能是很多程序员(优秀)的通病,如果有这样的需求,我的设计也可以怎样支撑,如果发生错误,我的代码有怎样的容错性等等。很明显只有优秀的程序员才会有这样的前瞻性与对代码的追求。但优化是永无止境的,我们的解决方案也得是要在解决问题的基础上产生。突然想起一句笑话“有的困难,解决困难,没有困难,创造困难也要解决困难”


结尾


设计原则有很多,这里并没有一一罗列。其实细心的小伙伴可能发现,有些设计原则之间相互甚至是互斥的,其实世界不也一直就是这样的吗,兔子不吃窝边草,同样也有近水楼台先得月。世界本就是两面,软件设计的最大目标,就是降低复杂性,万物不为我所有,但万物皆为我用。追求好代码,追求优雅,追求卓越,追求简单。


Programs must be written for people to read, and only incidentally for machines to execute


--- Harold Abelson《Structure and Interpretation of Computer Programs》


Ousterhout教授讲座:

https://www.youtube.com/watch?v=bmSAYlu0NcY


更多精彩


如何准备阿里技术面试?终面官现身说法!


对话Ruby创始人松本行弘、阿里高级技术专家朴灵!


【资料合集】2019阿里云峰会•北京回顾合集:PDF下载

如果觉得本文还不错,点击好看一下!
登录查看更多
0

相关内容

软件(中国大陆及香港用语,台湾作软体,英文:Software)是一系列按照特定顺序组织的计算机数据和指令的集合。一般来讲软件被划分为编程语言、系统软件、应用软件和介于这两者之间的中间件。软件就是程序加文档的集合体。
【2020新书】实战R语言4,323页pdf
专知会员服务
98+阅读 · 2020年7月1日
【2020新书】C++20 特性 第二版,A Problem-Solution Approach
专知会员服务
56+阅读 · 2020年4月26日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
223+阅读 · 2020年3月22日
【Google】利用AUTOML实现加速感知神经网络设计
专知会员服务
28+阅读 · 2020年3月5日
自动驾驶技术解读——自动驾驶汽车决策控制系统
智能交通技术
29+阅读 · 2019年7月7日
想让页面更简明,你需要学会设计信息层级
互联网er的早读课
3+阅读 · 2019年2月3日
【APS】PCB企业如何实现APS自动排程系统
产业智能官
12+阅读 · 2018年9月24日
【知识图谱】 一个有效的知识图谱是如何构建的?
产业智能官
56+阅读 · 2018年4月5日
用于数学的 10 个优秀编程语言
算法与数据结构
11+阅读 · 2018年1月5日
4个方面,系统总结个性化推荐系统
人人都是产品经理
7+阅读 · 2017年12月10日
手把手教你安装深度学习软件环境(附代码)
数据派THU
4+阅读 · 2017年10月4日
33款可用来抓数据的开源爬虫软件工具 (推荐收藏)
数据科学浅谈
6+阅读 · 2017年7月29日
A Survey on Bayesian Deep Learning
Arxiv
60+阅读 · 2020年7月2日
A Survey on Edge Intelligence
Arxiv
49+阅读 · 2020年3月26日
Arxiv
22+阅读 · 2019年11月24日
Arxiv
34+阅读 · 2019年11月7日
Arxiv
3+阅读 · 2018年4月11日
VIP会员
相关资讯
自动驾驶技术解读——自动驾驶汽车决策控制系统
智能交通技术
29+阅读 · 2019年7月7日
想让页面更简明,你需要学会设计信息层级
互联网er的早读课
3+阅读 · 2019年2月3日
【APS】PCB企业如何实现APS自动排程系统
产业智能官
12+阅读 · 2018年9月24日
【知识图谱】 一个有效的知识图谱是如何构建的?
产业智能官
56+阅读 · 2018年4月5日
用于数学的 10 个优秀编程语言
算法与数据结构
11+阅读 · 2018年1月5日
4个方面,系统总结个性化推荐系统
人人都是产品经理
7+阅读 · 2017年12月10日
手把手教你安装深度学习软件环境(附代码)
数据派THU
4+阅读 · 2017年10月4日
33款可用来抓数据的开源爬虫软件工具 (推荐收藏)
数据科学浅谈
6+阅读 · 2017年7月29日
相关论文
A Survey on Bayesian Deep Learning
Arxiv
60+阅读 · 2020年7月2日
A Survey on Edge Intelligence
Arxiv
49+阅读 · 2020年3月26日
Arxiv
22+阅读 · 2019年11月24日
Arxiv
34+阅读 · 2019年11月7日
Arxiv
3+阅读 · 2018年4月11日
Top
微信扫码咨询专知VIP会员