AICon全球人工智能与机器学习技术大会8折特惠,购票立减¥960! 了解详情
写点什么

正交设计,OO 与 SOLID

2016 年 8 月 18 日

正交设计,是普遍的设计原则,与粒度无关,与编程范式无关,更与具体的实现语言无关。(虽然确实在不同的编程范式下,或使用不同的编程语言时,具体的解决方法或难易程度不同,这也正是为何我们总是在寻找更适合的编程范式,更高效的编程语言的原因)。

而具体到面向对象范式,我们都知道著名的 SOLID原则。但是:这五个原则是怎么来的?它们的目的何在?它们的关系如何?

为了搞清楚这些疑问,我们再次回到最初的问题:

  1. 软件模块该如何划分?(怎么分)
  2. 模块间 API 该如何定义?(怎么合)

怎样进行分解?

模块的划分,是一个问题分解的过程。

在文章《VBD (Volatility Based Decomposition)》里,引用了一篇70 年代的论文《On the Criteria to be Used in Decomposing Systems into Modules》

在这篇论文里,作者通过一个小例子,清晰的指出了软件模块划分应该以基于信息隐藏为目的,以职责划分为手段,从而封装变化,让软件更加容易修改(即Kent Beck 的理想:局部化影响)。

这篇文章也展示了:基于流程(过程)的分解,是一种极其脆弱的模块划分方式。因而我们应该离基于过程的分解越远越好。

这也是为何 Matt Cochran 将这种分解方法称做:基于易变性的分解Volatility Based Decomposition)。

总而言之,变化应对变化,是软件设计最大的挑战,目的和意义。

面向对象究竟要解决什么问题?

最近几年,我听到太多针对 OO 的批评,其中最为奇葩的说法是:OO 只适合 GUI 编程。因为 GUI 组件概念上更接近于对象,至于其它领域则不太合适。

对提出这样高论的人,我相当确信,他们不仅对于面向对象一无所知,更是对于复杂软件所面临的真正挑战,以及软件设计究竟要解决什么问题一无所知。

前两天偶然从东海陈光剑的文章《函数式编程与面向对象编程 [5]: 编程的本质》读到软件模块化的目的和价值(虽然他用的是结构化,但在我看来也是在谈模块化),非常精彩,深合我意:

(软件设计是一个)层次化分解与重新复合的过程

这个思维过程, 并非是受计算机的限制而产生,它反映的是人类思维的局限性。我们的大脑一次只能处理很少的概念。生物学中被广为引用的 一篇论文指出我们我们的大脑中只能保存 7 ± 2 个信息块。我们对人类短期记忆的认识可能会有变化,但是可以肯定的是它是有限的。底线就是我们不能处理一大堆乱糟糟的对象或像兰州拉面似的代码。

我们需要结构化并非是因为结构化的程序看上去有多么美好,而是我们的大脑无法有效的处理非结构化的东西。我们经常说一些代码片段是优雅的或美观的,实际上那只意味 着它们更容易被人类有限的思维所处理。优雅的代码创造出尺度合理的代码块,它正好与我们的『心智消化系统』能够吸收的数量相符。

那么,对于程序的复合而言,正确的代码块是怎样的?它们的表面积必须要比它们的体积增长的更为缓慢。我喜欢这个比喻,因为几何对象的表面积是以尺寸的平方的速度增长的,而体积是以尺寸的立方的速度增长的,因此表面积的增长速度小于体积。

代码块的表面积是是我们复合代码块时所需要的信息。代码块的体积 是我们为了实现它们所需要的信息。一旦代码块的实现过程结束,我们就可以忘掉它的实现细节,只关心它与其他代码块的相互影响。在面向对象编程中,类或接口的声明就是表面。在函数式编程中,函数的声明就是表面。我把事情简化了一些,但是要点就是这些。

怎样才能做到表面积增长速度小于体积增长速度?当然是分解信息隐藏抽象。而这些也正是面向对象所追求和擅长的。

面向对象主要目的是为了模块化。虽然在一些数学家看来:由于面向对象没有很好的数学理论基础,因而必然是一个错误的方法论。可对于如何编写一个易于应对变化的软件,并不是一个纯数学理论问题(或许确实有数学家也可以抽象出一套数学理论),而更多的是一个实践问题。作为长期处于实践一线的我们,不应把几个数学家的看法当作金科玉律(对于那些没有实践经验,却对如何实践指手画脚的纯理论派,每次看到他们的不负责任的言论,考虑到他们的影响力和对新手的误导,就禁不住想对他们竖中指)(参见《学习的逻辑3: 三人行必有我徒》)。

软件工业最近20 年来,能够构建如此大规模的需求频繁变化的软件系统,很大程度上得益于面向对象对于模块化的良好支持。

而现在风头正劲的微服务化,无非是把模块化的思想,从进程内模块(类),变为进程间而已。

OO 和 FP

面向对象函数式编程的最大区别在于数据是否是强制不变性。这个前提,导致了一系列其它的差异。

因为可变性,面向对象可以将算法和数据放在一起,当数据是一种实现细节时,可对其进行信息隐藏封装。但在 Pure FP 里,数据和算法是必须分离的。这种分离,在很多场景下,对于信息隐藏相当不利(在这里我们先不谈性能)。因而,当系统规模足够复杂时,FP 对于构造易于维护软件的能力比面向对象要弱。

因而,FP 为了实用,要么部分放弃对不变性的坚持(如 LISP 所做的那样),从而允许模拟面向对象范式(参见 SICP );要么通过 Existential Quantification,来模拟 OO 的运行时多态,以达到信息隐藏,隔离变化的目的;要么使用轻量级进程轻量很关键):让每个轻量级进程承担一个很小的职责,从进程外部看,每个轻量级进程都可以有可修改的数据,以及基于消息的行为驱动(如 erlang 或 akka 的 Actor 模型),而这正是对于 smalltalk 的对象模拟,从而缓解了不变性带来的尴尬。进而也说明了基于高内聚,低耦合原则进行的模块化是超越范式的。

因而,一些 FPer 对于 OO 的盲目批评,和认为面向对象只适合 GUI 领域一样,都并不真正明白一个复杂软件的关键挑战,以及解决方案何在。

当然,具体到编程语言,即便都是 OO 语言,差别也巨大。但这是另外一个宏大的话题,这里暂且不谈。只重点说一句:不要把某种 OO 语言,当作 OO 本身

关于 FP 和 OO 的话题,值得专门写一篇文章全面论述,而本文的目的在于介绍 SOLID,因而不再赘述。

正交原则与 SOLID 的关系

一个好的面向对象设计,自然是符合高内聚,低耦合原则的对象划分和协作方式。

单一职责开放封闭,更多的在强调类划分时的高内聚;而里氏替换依赖倒置接口隔离则更多的强调类与类之间协作接口(即 API)定义的低耦合

高内聚(怎么分)

单一职责,通过对变化原因的识别,将一个承担多重职责的类,不断分割为更小的,只具备单一变化原因的类。而单一变化原因指的是:一个变化,会引起整个类都发生变化。只有关联极其紧密的情况,才会导致这样的局面。因而,单一职责高内聚某种程度是同义词。

单一职责原则本身,并没有明确指示我们该如何判定一个类属于单一职责的,以及如何达到单一职责的状态。而策略消除重复分离不同变化方向,正是让类达到单一职责的策略与途径。

低耦合 (怎么合)

开放封闭原则,正是通过将不同变化方向进行分离,从而达到对于已经出现的变化方向,对于修改是封闭的,对于扩展是开放的

里氏替换原则强调的是,一个子类不应该破坏其父类客户之间的契约。唯有如此,才能保证:客户与其父类所暴露的接口(即 API)所产生的依赖关系是稳定的。子类只应该成为隐藏在 API 背后的某种具体实现方式。

依赖倒置原则则强调:为了让依赖关系是稳定的,不应该由实现侧根据自己的技术实现方式定义接口,然后强迫上层(即客户)依赖这种不稳定的 API 定义,而是应该站在上层(即客户)的角度去定义 API(正所谓依赖倒置)。

但是,虽然接口由上层定义,但最终接口的实现却依然由下层完成,因此依赖倒置描述为:上层不依赖下层,下层也不依赖上层,双方共同依赖于抽象

最后,接口隔离原则强调的是:不应该强迫客户依赖它不需要的东西。显然,这是缩小依赖范围策略在面向对象范式下的产物。

结论

正交设计是一种与范式,语言无关的设计原则。为了解决在模块化的过程中,如何让软件在长期范围内更容易应对变化。

面向对象是一种对模块化支持良好的范式。通过高内聚,低耦合原则,或正交策略的运用,面向对象范式下 SOLID 原则会自然浮现。

我们耳熟能详的软件设计相关原则,模式与实践的关系如下:

关于正交设计的更多细节,请参阅《变化驱动:正交设计》


感谢张凯峰对本文的策划,丁晓昀对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2016 年 8 月 18 日 17:552434

评论

发布
暂无评论
发现更多内容

专治小学生作业拖沓

Ian哥

28天写作

Kafka.07 - 性能优化介绍

insight

kafka 2月春节不断更

产品0期 - 第五周作业

曾烧麦

产品训练营

科大讯飞发布全新一代智能办公本X2

Lucien

工作多年,如何找到自己更好的职业方向

一笑

28天写作

揭秘京东城市时空数据引擎—JUST如何助力交通流量预测

京东科技开发者

JUST 流量预测

伊卡洛斯象征了什么?「Day 5」

道伟

文化 28天写作

感性赢了理性那一面——浅谈峰终定律

Justin

心理学 28天写作

批量下载,我有妙解~

Viktor

JavaScript iframe 跨域

火山翻译:工业级应用与研究

DataFunTalk

【LeetCode】转置矩阵Java题解

HQ数字卡

算法 LeetCode 28天写作 2月春节不断更

处理XML数据应用实践

华为云开发者社区

xml 数据库 数据 XML文档 GaussDB(DWS)

工作日志2-23

技术骨干

新思科技静态应用安全测试帮助Cryptsoft公司提高软件安全和质量水平

InfoQ_434670063458

阿里粗排技术体系与最新进展

DataFunTalk

优雅编程 | javascript代码优化的4个小技巧

devpoint

递归 命名空间 闭包 函数绑定

28天瞎写的第二百四十三天:正念冥想可以解决什么问题?

树上

冥想 28天写作 正念

别再这么写代码了,这几个方法不香吗?

楼下小黑哥

Java 重构

Linux 入门篇 —— 重定向与管道符

若尘

Linux 管道符 linux开发

厘清 I/O 模型

sakila

网络编程 I/O

揭开《钢铁侠》AI管家贾维斯神秘面纱的扛鼎之作!

博文视点Broadview

如何有效改变别人的认知和行为?

数列科技杨德华

28天写作

(28DW-S8-Day5) 区块链如何防伪

mtfelix

比特币 区块链 非对称加密 28天写作 防伪技术

程序员成长第十二篇:做好项目计划

石云升

项目管理 程序员成长 28天写作 2月春节不断更

移除数组中的数字,不用额外空间, 实战RxSwift中的Observable, subscribe, dispose, 吴军老师态度读后感 John 易筋 ARTS 打卡 Week 39

John(易筋)

ARTS 打卡计划 吴军的态度 态度读后感

【管理笔记12】行销

俊毅

28天写作

翻译:《实用的Python编程》02_05_Collections

codists

Python

为您收录的操作系统系列 - 进程管理(下篇)

Arvin

方法论 操作系统 进程

腾讯位置服务开发应用

魔王哪吒

28天写作 2月春节不断更 腾讯地图 腾讯位置服务开发应用 腾讯位置

高手来啦!十八般武艺保护你的Web应用

浪潮云

云计算

谁手握账本?趣讲 ZK 的内存模型

HelloGitHub

Java zookeeper ZooKeeper原理

正交设计,OO与SOLID-InfoQ