《代码精进之路:从码农到工匠》——抽象

写本文的原因是,抽象是软件设计中最重要的概念,但抽象这个概念本身又很抽象,值得我们花时间去好好探究一番。

抽象的力量

没有抽象思维,就没有人类光辉灿烂的现代文明。

原始人看到一片树林,不会给它们一个名字,比如叫“松树”。他们会给每一棵树取一个独特的名字,可能叫“silisiba”。原始人只知道某棵具体的树。

随着意识水平的发展,人类开始有意识地将具有相同特征的事物归并到一起,从“silisiba”到“松树”——到“树木”——到“植物”——到“物质”,从具象思维到抽象思维,我们人类花了几万年漫长的时间。

赫拉利在《人类简史》里说,人类之所以成为人类,是因为人类能够想象。这里的想象,作者认为很大程度上也是指抽象能力。正是这样的抽象思维帮助人类能够从具体事物中,抽象出各种概念,然后再用这些概念去构筑种种虚构故事。这些概念,包括政治(例如民族、国家)、经济(例如货币、证券)、文学、艺术和科学等,都是建立在抽象的基础之上。

什么是抽象

那到底什么是抽象呢?“抽”就是抽离,“象”就是具象,字面上理解抽象,就是从具体中抽离出来。英文的抽象——abstract来自拉丁文abstractio,它的原意是排除、抽出。

按照Wikipedia上的解释,抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。例如,一个 皮质的足球 ,我们可以过滤它的质料等信息,得到更一般性的概念,也就是 。从另外一个角度看,抽象就是简化事物,抓住事物本质的过程。

用语言来说明抽象还是太抽象:),我们都知道在绘画里面有一个流派叫抽象主义,最著名的抽象派大师就是毕加索了,下面我们通过他的一幅画,直观的感受一下什么是抽象。
image.png
怎么样? 是不是够抽象,只是几根线条,不过这几根线条是做了高度抽象之后的线条,过滤了绝大部分水牛的细节,只保留了牛的最主要的一些特征,正因为其抽象层次更高,因此,其泛化能力更强,不仅可以表示水牛,什么黄牛、奶牛、野牦牛只要是牛都逃不过这几根线。

抽象是OO的基础

OO(Object Oriented,面向对象)的思考方式,就是万物皆对象,抽象帮助我们将现实世界的对象抽象成类。帮助我们完成从现实世界的概念到计算机世界的模型的映射。比如,这里有一堆苹果,对其进行抽象的话,我们可以得到Apple这个类,通过这个类,我们可以实例化一个红色的苹果:new Apple("red")。此时,如果我们需要把香蕉,橘子等也纳入到考虑范围,那么Apple的抽象层次就不够了,我们需要Fruit这个更高层次的抽象来表达“水果”这个概念。

面向对象的思想主要包括三个方面:OOA(面向对象的分析:Object Oriented Analysis)、OOD(面向对象的设计:Object Oriented Design)、以及我们经常说的OOP(面向对象的编程:Object Oriented Programming)

OOA是根据抽象关键问题域来分解系统。OOD是一种提供符号设计系统的面向对象的实现过程,它用非常接近实际领域术语的方法把系统构造成“现实世界”的抽象。OOP可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。

扫描二维码关注公众号,回复: 11704968 查看本文章

可以看到抽象贯穿于OO的始终,是OO的前提和底层能力,抽象能力差的人是很难做好OO的。

抽象的层次性

对同一个对象的抽象是有不同层次的。层次越往上,抽象程度越高,它所包含的东西就越多,其含义越宽泛,忽略的细节也越多;层次越往下,抽象程度越低,它说包含的东西越少,越细节。也就是我们常说的是,内涵越小外延越大,内涵越大外延越小。不同层次的抽象有不同的用途。

有一次在家,女儿问我,“爸爸你会不会画猫?”,我说会呀,然后她又问,“那你会画房子吗?”,我说也会呀。她有点失落,然后看看外面问,“那你会不会画城市?”,为了让女儿开心,我跟她说,“城市那么大,爸爸不会画”。她还太小,不知道抽象是有层次的,以为画城市,就要把城市的每个细节都画出来。其实城市在地图上,也不过一个点而已。

这种抽象的层次性,基本可以体现在任何事物上,以下是对一份报纸在多个层次上的抽象:

  1. 一个出版品
  2. 一份报纸
  3. 《旧金山纪事报》
  4. 5 月 18 日的《旧金山纪事报》
  5. 我的 5 月 18 日的《旧金山纪事报》

那么我要统计美国有多少个出版品,那么就要用到第一层“出版品”的抽象,如果我要查询旧金山5月18日当天的新闻,就要用到第五层的抽象。

分层抽象,在软件的世界里随处可见,是软件架构的核心,也是我们构建庞大复杂的信息科技的基石。

例如,我们的系统就是分层的。最早的程序是直接运行在硬件上的,开发成本非常高。然后慢慢开始有了操作系统,操作系统提供了资源管理、进程调度、输入输出等所有程序都需要的基础功能,开发程序时调用操作系统的接口就可以了。再后来发现操作系统也不够,于是又有了各种运行环境(如 JVM)。

编程语言也是一种分层的抽象。机器理解的其实是机器语言,即各种二进制的指令。但是使用二进制编程效率极低,所以我们用汇编语言抽象了二进制指令,用C语言抽象了汇编语言,而高级语言Java抽象了低级语言。

在软件开发中,我们应该也都听说过各种分层模型。例如经典的三层模型(展现层、业务逻辑层、数据层),还有 MVC 模型等。有一句名言:“软件领域的任何问题,都可以通过增加一个间接的中间层来解决”。分层架构的核心其实就是抽象的分层,每一层的抽象只需要而且只能关注本层相关的信息,从而简化整个系统的设计。

很难想象,如果没有抽象分层,会有人可以应对软件世界这么大的复杂性。譬如阿里的软件系统其复杂程度绝对不亚于一个城市,若是没有抽象分层,就像要在一张画布上画一整个城市的细节,从摩天大厦到门把手,从公园到街道上的猫,难以想象。

如何进行抽象

归类分组

简单来说,抽象的过程就是归类分组,总结归纳的过程。归类分组就是将有内在逻辑关系,或者叫有共性的事物放在一起。总结归纳就是给这一个分组或者叫分类进行命名,这个名字就代表了这组分类的抽象。

在我们的生活中,无时无刻不在进行这样的抽象,我们的语言作为符号系统,本身就是对现实世界的抽象表达。比如当你说苹果的时候,你就是在运用抽象的概念,用来指称所有的红红的,有水分的,吃起来酸甜的水果。同样花也是一个抽象概念,面对千变万化、多种多样的花,它概括了所有花的本性。

实际上这种归类分组的能力是我们的天性,人类很早以前就认识到,大脑会自动将发现的所有事物以某种秩序组织起来。基本上,大脑会认为同时发生的任何事物之间都存在某种关联,并且会将这些事物按照某种逻辑模式组织起来。

大脑会将其认为具有“共性”的任何事物组织在一起,“共性”指的是具有某种相似的共同点或所处的位置相近。看一下下面这个图
image.png
无论是谁,看到上面的6个黑点,都会认为共有两组黑点,每组3个。这种分类思维是我们与生俱来的天性。

然而,实际工作中的抽象也不会都像上图这么的简单直观。

首先,很多的共性并不是那么容易就能被发现,即使被发现了也不是那么容易就能被归类。

其次,给新抽象命名也不是一件容易的事,这是为什么命名被评为最让程序员头疼的事之首了。stackoverflow的创始人Jeff Atwood也说过:“Creating good names is hard, but it should be hard, because a great name captures essential meaning in just one or two words”。

反过来看,如果你不能很好的给一个类,一个方法,一个变量命名的话,往往意味着你的抽象不够好。这种现象在我的实际工作中屡有发生,每当大家对一个命名纠结有争议时,都会发现概念的混淆,或者抽象的缺失。

提升抽象层次

当发现有些东西就是不能归到一个类别中是,我们应该怎么办呢?此时,我们可以通过拔高一个抽象层次的方式,让它们在更高抽象层次上产生逻辑关系。

比如,你可以合乎逻辑地将苹果和梨归类概括为水果,也可以将桌子和椅子归类概括为家具。但是怎样才能将苹果和椅子放在同一组中呢?仅仅提高一个抽象层次是不够的,因为上一个抽象层次是水果和家具的范畴。因此,你必须提高到更高的抽象层次,比如将其概括为“商品”,如果要把大肠杆菌也纳入其中呢?那么“商品”这个抽象=也不够了,需要再提高一个抽象层次,比如叫“物质”,但是这样的抽象太过于宽泛,难以说明思想之间的逻辑关系。类似于我们在Java里面的顶层父类对象(Object),万物皆对象。
image.png

在我们的开发工作中,很多时候就需要通过抽象层次的提升,来提高代码的可读性和通用性。

举一个关于提升抽象层次的例子,这样大家会更有体感。假如在客户管理系统中,有一个Customer父类,现在有一个子类ChannelACustomer代表是通过渠道A过来的客户,他有一些自己特有的属性,比如用来标识这个渠道的id——channelAId,可以通过getChannelAId()方法来获取。而这个方法是没有在Customer上声明的,这样在使用Customer作为参数的地方,如果要想使用getChannelAId()方法,就必须强转成ChannelACustomer才可以用。

此时,我们就应该考虑一下,是否应该对这个channelAId进行一下抽象提升,首先分析一下这个id的用途,它是用来关联外部系统的,如果再过来一个新渠道叫ChannelB,还会有一个channelBId。这些id对当前系统来说,都是外部id,我们可以将它从具体的渠道中抽象出来,重新命名为externalId,这样就可以将其纳入Customer的范畴。用一张图来表示,就是下面的过程:
image.png

通过上面抽象提升,我们让Customer拥有了getExternalId(),从而变得更通用,消除了本来必须对ChannelACustomer进行强制类型转换,才能使用子类方法getChannelAId()的情况,提升了代码的通用性,也让程序更好的满足了LSP(里式替换原则)。因此,每当我们有强制类型转换,或者使用instanceof的时候,都值得停下来,思考一下,我们是否需要做抽象层次的提升。

值得注意的是,在我们向父类型,特别是接口上添加新的方法(也就是抽象)时,会波及到现有的代码实现,意味着所有的实现类都要被改动。为了避免这样的大范围改动,方便向后兼容,Java8引入了default关键字,让实现类可以不实现接口中声明为default的方法,从而将添加新抽象的影响降到最小。

金字塔结构

《金字塔原理》是一本教人如何进行结构化思考和表达的书,核心思想是通过归类分组搭建金字塔结构,让我们可以更加全面的思考,表达观点时更加清晰。是一种非常有用的思维框架,我们平时说这个人逻辑很清晰,正是因为他具备结构化思维,可以把一件事情说的很有条理,很清晰。

书里说我们要自下而上的思考,总结概括;自上而下的表达,结论先行。其中自下而上的总结概括的过程就是抽象的过程,构建金字塔的过程就是寻找逻辑关系,抽象概括的过程。经常锻炼用结构化的方式去处理问题,搭建自己的金字塔,可以帮助我们理清问题的脉络,提升我们的抽象能力。

金字塔结构让我们把混乱无序的信息,通过抽象概括形成不同的抽象层次,从而方便我们理解和记忆,这个方法论值得我们每个人好好掌握。

举个例子,你出门买报纸,你老婆让你带点东西,你老婆给你列了一个清单,里面有葡萄,橘子,咸鸭蛋,土豆,鸡蛋、牛奶和胡萝卜,当你准备出门的时候,你妻子说,顺便还带点苹果和酸奶吧。

如果不用纸记下来,你还能记住妻子让你买的9样东西吗?大部分人应该都不可以,因为我们的大脑短期记忆无法一次容纳7个以上的记忆项目,超过5个时,我们就会开始讲起归类到不同的逻辑范畴,以便于记忆。

对于葡萄,橘子,牛奶,咸鸭蛋,土豆,鸡蛋、胡萝、苹果,酸奶。我们可以按照逻辑关系进行分类,然后搭建一个如下的金字塔结构:
image.png
分类的作用不只是将一组9个概念,分成每组各有4个、3个和2个概念的3组概念,因为这样还是9个概念,你所要做的是提高一个抽象层次,将大脑需要处理的9个项目变成3个项目。

这样做以后,你无须再记忆9个概念中的每一个概念,仅需记忆9个概念所属的3个组。这样,你的思维的抽象程度就提高了一层。由于处于较高层次的思想总是能够提示其下面一个层次的思想,因而更容易理解和记忆。

在程序设计中,也是一样,如果在一个类或者一个函数中涉及过多的内容和概念,我们大脑也会显得不知所措,会觉得很复杂,不能理解。其实有时候,只要按照上面给日用品分类的方法,将一个大方法,按照逻辑关系,整理成一组更高层次的小而内聚的子程序的集合,那么整个代码逻辑就会呈现出完全不一样的风貌,显得干净、容易理解的多。

在这方面,Spring里面核心类AbstractApplicationContext做上下文初始化的refresh方法,给我们做了一个很好的示范:

	public void refresh() throws BeansException, IllegalStateException {
    
    
		synchronized (this.startupShutdownMonitor) {
    
    
			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			try {
    
    
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}

			catch (BeansException ex) {
    
    
				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

			finally {
    
    
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
			}
		}
	}

试想下,上面的逻辑,如果是平铺在refresh方法中的,其结果会怎样。

如何提升抽象思维

说了这么多关于抽象的概念,抽象思维又是如此重要。那我们有没有办法来锻炼和提升我们的抽象思维呢?

是的,抽象思维也是可以习得的。婴幼儿是没有抽象思维的,你和他玩躲猫猫游戏,把他眼睛蒙上,他就以为你消失了,再放开,他看到你就会很高兴,咯咯笑。因为他只能意识到你这个具象的人,他的意识还到不了抽象的程度。理解具象的内容要比抽象更加简单容易,而对抽象内容的理解则要困难和复杂很多。

多阅读

为什么阅读书籍比看电视更好呢?因为图像比文字更加具象,阅读的过程可以锻炼我们的抽象能力、想象能力,而看画面的时候会将你的大脑铺满,较少需要抽象和想象。

这也是为什么我们不提倡让小孩子过多的暴露在电视或手机屏幕前的原因,因为这样不利于他抽象思维的锻炼。

抽象思维的差别让孩子们的学习成绩从初中开始分化,许多不能适应这种抽象层面训练的,就去读技校,这里比大学更加具象:车铣刨磨、零件部件都能看到能摸到。

多总结

小时候不理解,语文老师为什么总是要求我们总结段落大意、中心思想什么的。现在回想起来,这种思维训练在基础教育中是非常必要的,其实质就是帮助学生提升抽象思维能力。

记录也是很好的总结习惯。就拿读书笔记来说,最好不要原文摘录书中的内容,而是要用自己的话总结归纳书中的内容,这样不仅可以加深理解,而且还可以提升自己的抽象思维能力。

我们的现象世界繁杂复杂,只有具备较强的抽象思维能力才能够具备抓住事物本质的能力。

多做领域建模

对于技术同学,我们还有一个非常好的提升抽象能力的手段——领域建模。当我们对问题域进行分析、整理和抽象的时候,当我们对领域进行划分和建模的时候,实际上也是在锻炼我们的抽象能力。

关于这一点,作者深有感触,当我一开始使用上一章介绍的建模方法论进行建模的时候,会觉得无从下手,建出来的模型也感觉很别扭。然而,经过几次锻炼之后,很明显可以感觉到我的建模能力和抽象能力都有很大的提升。不但分析问题的速度更快了,而且建出来的模型也更加优雅了。

猜你喜欢

转载自blog.csdn.net/significantfrank/article/details/93173386