谈谈抽象

298 阅读14分钟

Haskell 语言的设计者之一 Paul Hudak 曾说过一句略带夸张的话:编程中最重要的三件事是:抽象,抽象,抽象

abstraction, abstraction, abstraction”are the three most important things in programming。

本文关于抽象的讨论主要集中在软件工程领域。

抽象的定义

百度百科的定义如下:

抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。具体地说,抽象就是人们在实践的基础上,对于丰富的感性素材通过去粗取精、去伪存真、由此及彼、由表及里的加工制作,形成概念、判断、推理等思维形式,以反映事物的本质和规律的方法。

Wikipedia的定义如下:

抽象化(英语:Abstraction)是指缩减一个概念或是一个现象的资讯含量来将其广义化(Generalization)的过程,此时只保存特定的资讯。例如,将一个皮制的足球抽象化成一个球,只保留一般球的属性和行为等资讯。相似地,亦可以将快乐抽象化成一种情绪,情绪以外的资讯量都被剔除。

在计算机科学中,抽象化(英语:Abstraction)是将资料与程序,以它的语义来呈现出它的外观,但是隐藏起它的实现细节。抽象化是用来减少程序的复杂度,使得程序员可以专注在处理少数重要的部分。一个电脑系统可以分割成几个抽象层(Abstraction layer),使得程序员可以将它们分开处理。

抽象的过程就是从“具象”事物中归纳出共同特征,“抽取”得到一般化(Generalization)的概念的过程。

抽象的角度

有一句名言:“事情发生的好坏并不重要,重要的是你从哪个角度切入”。这句话告诉我们,同样的一件事或者一句话,如果你站在不同的角度去诠释,会得到不同的结果,甚至相差甚大。

生活中我们常常说「我的观点是…」其实这里的「观点」就是一个角度问题,从一定的立场或角度出发,对事物或问题所持的看法。以生活中的常见的实物来说(如下图),我们是否能快速的说出其中的相同点和不同点。

抽象是有角度之分的。

image.png

如图中已经标注的,我们从功用的角度对它们定义了椅子、桌子、凳子和柜子这样的区分,但显然很有很多很多角度,比如:物料、文字、高矮等等维度,从不同维度看过去,会有完全不同的相同点和不同点表述,所以,本质是什么?本质是:

  • 抽象角度其实也是分类的角度,角度不同,会导致完全不同建模方向和结果
  • 抽象的角度就是建模的方向和目的(「屁股决定脑袋」)

抽象的层次

Wikipedia 中关于抽象的定义中有一个关于报纸的例子:

  1. 我的 5 月 18 日的《旧金山纪事报》

  2. 5 月 18 日的《旧金山纪事报》

  3. 《旧金山纪事报》

  4. 一份报纸

  5. 一个出版品

这五句话中,我们可以感受到抽象的层次,抽象层次越高,细节越少,普适性越强。再比如下图中关于网络模型的抽象,关于操作系统内核的抽象,我们可以明显的看到不同层次的抽象,就是过滤不同的信息,最终留下来的信息才是当前抽象层次所需要的信息。从系统设计实现上来说,抽象层次越高,越接近设计,越远离实现,同时抽象的模型越不受细节的羁绊,稳定性越高,普适性越强,可重用性就越高

那么这里抽象的划分层次的依据是什么?原则又是什么?我的经验是,划分抽象层次的依据主要包含两个:

  • 以抽象角度分层(可能一层是多角度的聚合)

  • 面对变化分层(用层次隔离变化)

其实这个也不能完全解释如何分层,原则是什么?我觉得这是几个最通用的原则

  • 公用的往下走

  • 个性的往上走

  • 下层可以独立于上层存在

  • 控制下层的变化

考虑抽象层次的好处是不论在哪一个层次上,我们只需要面对有限的复杂度,从而专心考虑这个层次上的抽象是什么,要表达的信息是什么。

抽象的边界

除了角度、层次之外,我们还需要考虑的抽象的边界。如果说层次考虑的是纵向维度的表达,那么边界考虑的是横向维度的表达。如何确定边界,一个总的原则是按照职责进行划分,这里的职责其实也就是分工 一旦职责确定,我们在做建模分析时就不需要把整个业务大局放进来从头到尾去分析一遍,我们只需要考虑当前分工下的上游和下游即可,这样的信息量大大减少,自然的我们面对的领域复杂度也会降低到一定程度。

如果一定要给出边界的定义,我的理解是:边界是在确定抽象角度下,通过寻找核心的业务活动,抽取核心实体,进一步确定实体核心生命周期的结果。可能有一点点绕,关键词是:核心业务活动、核心实体、核心实体生命周期。

以现场娱乐行业为例,如下这张图包含了最高抽象层次下业务的全生命周期,这个抽象层次下的主体是什么,我的理解是票,项目生产的结果是票,分销或电商服务是对票的销售,现场是对票的核验,至此以票为核心实体的生命周期结束。

如果我们往下 Down 一层,从项目生产这一个业务活动去看,整个业务流程是这样:

项目管理->场馆座位分销->票房预测->场次管理->配额管理->绘座->票房规划

从生产这个视角去看,核心的实体不是票,而是场次(确定时间、确定地点、确定内容的一场演出或赛事),所有的关键业务活动都是以场次为维度,生产领域里需要考虑的主要就是场次的核心生命周期。

所以,在不同的抽象角度、不同的抽象层次,根据分工的不同会有不同的核心业务活动、不同的核心实体、边界的确定关键在寻找核心的生命周期。寻找生命周期的过程,就是发现内聚的过程;将所有关于生命周期的业务活动累积,就可以提升领域或模块的内聚性。

抽象的泄漏

所有非平凡的抽象,在某种程度上,都是有泄漏的。

早在 2002 年,程序员 Joel Spolsky 就敏锐地发现了这类现象,并将它们总结为:“抽象泄露法则”。它的含义是:任何试图减少或隐藏复杂性的抽象,其实都并不能完全屏蔽细节;试图被隐藏的复杂细节总是可能会从抽象层级中“泄漏”出来。举几个抽象泄漏的例子。

  • 即使是遍历一个大的二维数组这样简单的事情,如果你水平遍历而不是垂直遍历,性能可能会有天壤之别,这取决于“木纹的方向”——一个方向可能会导致比另一个方向多得多的页面错误,而页面错误是很慢的。即使是汇编程序员也被允许假装他们有一个大的平坦地址空间,但虚拟内存意味着它实际上只是一个抽象,当发生页面错误时,某些内存访问会比其他的多花很多纳秒,这时抽象就出现了泄漏。

  • SQL语言旨在抽象出查询数据库所需的过程步骤,而是让你只需定义你想要的内容,并让数据库自行找出查询的过程步骤。但在某些情况下,某些SQL查询比其他逻辑上等效的查询慢数千倍。一个著名的例子是,在某些SQL服务器上,如果你指定“where a=b and b=c and a=c”,查询速度会比只指定“where a=b and b=c”快得多,尽管结果集是相同的。你本不应该关心过程,只需关心规范。但有时抽象会泄漏,导致性能极差,你不得不拿出查询计划分析器,研究它哪里出了问题,并找出如何让你的查询运行得更快。

  • 尽管像NFS和SMB这样的网络库让你可以像对待本地文件一样对待远程机器上的文件,但有时连接会变得非常慢或中断,文件就不再表现得像本地文件一样,作为程序员,你必须编写代码来处理这种情况。“远程文件与本地文件相同”的抽象出现了泄漏。这里有一个针对Unix系统管理员的具体例子。如果你将用户的主目录放在NFS挂载的驱动器上(一种抽象),而用户创建了.forward文件以将所有电子邮件转发到其他地方(另一种抽象),当新邮件到达时,如果NFS服务器宕机,邮件将不会被转发,因为.forward文件将无法找到。抽象中的泄漏实际上导致了一些邮件被丢弃。

但是这个规则为什么会存在?我们为什么不能建立完美的抽象层级?

这个问题在于,虽然抽象存在的意义是为了屏蔽细节,但抽象 (abstraction) 的价值也正是在于它所屏蔽或隐藏的细节当中。一个好的抽象应该做减法,也就需要将一些细节隐藏在调用者视线之外。但原 API 的设计范畴是有限的,其复杂度和操作支持范围必定是它更下层抽象的一个子集。

The value of an abstraction is in the details that it hides.

抽象泄漏的原因

  • 接口 (interface) 暴露的细节太多。在接口层面将底层的实现细节暴露出来,并且这些实现细节在后续的迭代过程中可能发生变化。这就要求接口的使用者学习底层的实现细节,才能正确使用接口。
  • 接口 (interface) 所屏蔽的细节太多。细节屏蔽的太多,接口范围太窄,开发者真正想实现接口不支持的功能时,关注内部实现细节,基于屏蔽的细节进行功能扩展。
  • 抽象层级设计缺乏一致性 (consistency)。当接口存在不一致的使用方式,或者和业内规范不一致时,使用者需要了解内部实现实现来解答困惑。
  • 抽象层级缺乏完好的注解。由于缺失完善的说明文档,调用者从抽象层级外观察难以低成本理解功能。

抽象的时机

在什么时候进行抽象是最优的,并无明确答案,这里列举了3个原则,可在具体实施时根据情况灵活应用。

  • DRY原则。DRY是 Don't repeat yourself 的缩写,意思是"不要重复自己"。软件工程名著《The Pragmatic Programmer》首先提出了这个原则。它的涵义是,系统的每一个功能都应该有唯一的实现。也就是说,如果多次遇到同样的问题,就应该抽象出一个共同的解决方法,不要重复开发同样的功能。这个原则有时也称为"一次且仅一次"原则(Once and Only Once)。
  • YAGNI原则。YAGNI是 You aren't gonna need it 的缩写,意思是"你不会需要它"。这是"极限编程"提倡的原则,指的是你自以为有用的功能,实际上都是用不到的。因此,除了最核心的功能,其他功能一概不要部署,这样可以大大加快开发。它背后的指导思想,就是尽可能快、尽可能简单地让软件运行起来(do the simplest thing that could possibly work)。 但是,这里出现了一个问题。仔细推敲的话,你会发现DRY原则和YAGNI原则并非完全兼容。前者追求"抽象化",要求找到通用的解决方法;后者追求"快和省",意味着不要把精力放在抽象化上面,因为很可能"你不会需要它"。所以,就有了第三个原则。
  • Rule Of Three原则。Rule of three 称为"三次原则",指的是当某个功能第三次出现时,才进行"抽象化"。这是软件开发大家Martin Fowler在《Refactoring》一书中提出的。 它的涵义是,第一次用到某个功能时,你写一个特定的解决方法;第二次又用到的时候,你拷贝上一次的代码;第三次出现的时候,你才着手"抽象化",写出通用的解决方法。 这样做有几个理由: (1)省事。如果一种功能只有一到两个地方会用到,就不需要在"抽象化"上面耗费时间了。 (2)容易发现模式。"抽象化"需要找到问题的模式,问题出现的场合越多,就越容易看出模式,从而可以更准确地"抽象化"。 比如,对于一个数列来说,两个元素不足以判断出规律:

  1, 2, _, _, _, _,

第三个元素出现后,规律就变得较清晰了:

  1, 2, 4, _, _, _,

(3)防止过度冗余。如果一种功能同时有多个实现,管理起来非常麻烦,修改的时候需要修改多处。在实际工作中,重复实现最多可以容忍出现一次,再多就无法接受了。

综上所述,"三次原则"是DRY原则和YAGNI原则的折衷,是代码冗余和开发成本的平衡点,值得我们在"抽象化"时遵循。

抽象的评估

高内聚低耦合,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高,耦合度是否低。目的是使程序模块的可重用性、移植性大大增强。通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低,当模块内聚高耦合低的情况下,其内部的腐化问题不容易扩散,从而带给系统本身的好处就是复杂度的降低。

内聚是从功能角度来度量模块内的联系,好的内聚模块应当做好一件事情,它描述了模块内部的功能联系;而耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的依赖程度

Manny Lehman 教授在软件演进法则中首次系统性提出了软件复杂度:

软件(程序)复杂度是软件的一组特征,它由软件内部的相互关联引起。随着软件的实体(模块)的增加,软件内部的相互关联会指数式增长,直至无法被全部掌握和理解。

软件的高复杂度,会导致在修改软件时引入非主观意图的变更的概率上升,最终在做变更的时候更容易引入缺陷。在更极端的情况下,软件复杂到几乎无法修改。

在软件的演化过程中,不断涌现了诸多理论用于对软件复杂度进行度量,比如,Halstead 复杂度、圈复杂度、John Ousterhout 复杂度等等。对于这几种复杂度的计算方式不再展开介绍。

如何进行抽象

总结前面说的所有关于抽象的内容,形成*抽象的方法论(套路) *:

  • 抽象有两种方法,一种是自顶向下,另一种是自底向上

  • 业务建模,是从小到大,从局部到整体,自底向上的归纳、演绎的抽象过程

  • 系统建模,是从大到小,从整体到局部,自顶向下的拆解、切分的抽象过程

  • 但不绝对,自上而下和自下而上,往往在过程中是随意切换的

下面这张图来自于《Thinking in UML》,我觉得这个循环的过程可以表达上面这四个点,供大家参考。

参考文档

www.infoq.cn/article/txe…

zh.wikipedia.org/wiki/%E6%8A…

zhuanlan.zhihu.com/p/32563505

blog.csdn.net/significant…

zh.wikipedia.org/wiki/%E6%8A…

www.51cto.com/article/669…

www.ruanyifeng.com/blog/2013/0…

lostechies.com/derickbaile…

www.51cto.com/article/669…

juejin.cn/post/739224…

lanlingzi.cn/post/techni…

www.cnblogs.com/peida/p/120…

blog.csdn.net/significant…

blog.csdn.net/significant…

www.pathsensitive.com/2022/03/abs…

corner.buka.sh/understandi…

blog.csdn.net/ShawGolden/…

blog.csdn.net/ShawGolden/…

www.cnblogs.com/CareySon/p/… redscarf.me/code-leaky-… rickbsr.medium.com/%E6%B7%BA%E…

corner.buka.sh/understandi…