共计 9783 个字符,预计需要花费 25 分钟才能阅读完成。
若想捉大鱼,就得潜入深渊。深渊里的鱼更无力,也更污浊。硕大而形象,且十分漂亮。——大卫·林奇
作者:张建飞
前言
抽象思维是咱们工程师最重要的思维能力。因为软件技术 实质上就是一门形象的艺术。咱们的工作是存思维的“游戏”,尽管咱们在应用键盘、显示器,关上电脑能够看到主板、硬盘等硬件。但咱们即看不到程序如何被执行,也看不到 0101 是如何被 CPU 解决的。
咱们工程师每天都要动用抽象思维,对问题域进行剖析、演绎、综合、判断、推理。从而形象出各种概念,开掘概念和概念之间的关系,对问题域进行建模,而后通过编程语言实现业务性能。所以,咱们大部分的工夫并不是在写代码,而是在梳理需要,理清概念。当然,也包含尝试看懂那些“该死的、他人写的”代码。
在我接触的工程师中,能深刻了解抽象概念的并不多,能把形象和面向对象、架构设计进行有机联合,能用抽象思维进行问题剖析、化繁为简的同学更是百里挑一。
对于我自己而言,每当我对形象有进一步的了解和认知,我都能切身感受到它给我在编码和设计上带来的质的变动。同时,感叹之前对形象的了解为什么如此浮浅。如果工夫能够倒流的话,我心愿我在我职业生涯的晚期,就能充沛意识到形象的重要性,能多花工夫认真的钻研它,粗浅的了解它,这样应该能够少走很多弯路。
什么是形象
对于形象的定义,百度百科是这样说的:
形象是从泛滥的事物中抽取出独特的、本质性的特色,而舍弃其非本质的特色的过程。具体地说,形象就是人们在实际的根底上,对于丰盛的理性资料通过去粗取精、去伪存真、由此及彼、由表及里的加工制作,造成概念、判断、推理等思维模式,以反映事物的实质和法则的办法。
实际上,形象是与具体绝对应的概念,具体是事物的多种属性的总和,因此形象亦可了解为由具体事物的多种属性中舍弃了若干属性而固定了另一些属性的思维流动。[1]
Wikipedia 的解释是:
形象是指为了某种目标,对一个概念或一种景象蕴含的信息进行过滤,移除不相干的信息,只保留与某种最终目标相干的信息。例如,一个皮质的足球,咱们能够过滤它的质料等信息,失去更一般性的概念,也就是球。从另外一个角度看,形象就是简化事物,抓住事物本质的过程。[2]
简略而言,“抽”就是抽离,“象”就是具象,字面上了解形象,形象的过程就是从“具象”事物中演绎出独特特色,“抽取”失去一般化(Generalization)的概念的过程。英文的形象——abstract 来自拉丁文 abstractio,它的原意是排除、抽出。
为了更好的不便你了解形象,让咱们先来看一幅毕加索的画,如下图所示,图的右边是一头水牛,是具象的;左边是毕加索画,是形象的。怎么样,是不是感觉本人一下子了解了抽象画的含意。
能够看到,形象牛只有几根线条,不过这几根线条是做了高度形象之后的线条,过滤了水牛的绝大部分细节,保留了牛最本质特征,比方牛角,牛头,牛鞭、牛尾巴等等。这种对细节的舍弃使得“形象牛”具备更好的泛化(Generalization)能力。
能够说,形象更靠近问题的实质,也就是说所有的牛都逃不过这几根线条。
=
形象和语言是一体的
对于抽象思维,咱们在百度百科上能够看到如下的定义:
抽象思维,又称词(概念)的思维或者逻辑思维,是指用词(概念)进行判断、推理并得出结论的过程。抽象思维以词(概念)为中介来反映事实。这是思维的最本质特征,也是人的思维和动物心理的基本区别。[3]
之所以把抽象思维称为词思维或者概念思维,是因为语言和形象是一体的。当咱们说“牛”的时候,说的就是“牛”的形象,他代表了所有牛共有的特色。同样,当你在程序中创立 Cow 这个类的时候,情理也是一样。在生活中,咱们只见过一头一头具象的牛,“牛”作为形象的存在,即看不见也摸不着。
这种把抽象概念作为世界本真的认识,也是古希腊哲学家柏拉图的最重要哲学思想。柏拉图认为,咱们所有用感觉感知到的事物,都源于相应的理念。他认为具体事物的“名”,也就是他说的“理念世界”才是本真的货色,具体的一头牛,有大有小,有私有母,色彩、性格、形状各自不同。因而咱们不好用个体感觉加以概括,然而这些牛既然都被统称为“牛”,则阐明它们必然都源于同一个“理念”,即所谓“牛的理念”或者“理念的牛”,所以它们能够用“牛”加以概括。尚且不管“理念世界”是否真的存在,这是一个哲学问题,但有一点能够确定,咱们的思考和对概念的表白都离不开语言。[4]
这也是为什么,我在做设计和代码审查(Code Review)的时候,会特地关注命名是否正当的起因。因为命名的好坏,在很大水平上反映了咱们对一个概念的思考是否清晰,咱们的形象是否正当,反馈在代码上就是,代码的可读性、可了解性是不是良好,以及咱们的设计是不是到位。
有人做过一个考察,问程序员最头痛的事件是什么,通过 Quora 和 Ubuntu Forum 的考察结果显示,程序员最头疼的事件是命名。如果你已经为了一个命名而搜索枯肠,就不会对这个后果感到意外。
就像 Stack Overflow 的创始人 Joel Spolsky 所说的:“起一个好名字应该很难,因为,一个好名字须要把要义稀释在一到两个词。(Creating good names is hard, but it should be hard, because a great name captures essential meaning in just one or two words)。”
是的,这个稀释的过程就是形象的过程。我不止一次的发现,当我感觉一个中央的命名有些顺当的时候,往往就意味着要么这个中央我没有思考分明,要么是我的形象弄错了。
对于如何命名,我在《代码精进之路》里曾经有比拟详尽的论述,这里就不赘述了。
我想强调的是,语言是清晰概念的根底,也是抽象思维的根底,在构建一个零碎时,值得咱们花很多工夫去斟酌、去斟酌语言。在我做过的一个我的项目中,就曾为一个要害实体探讨了两天,因为那是一个新概念,尝试了很多名字,始终感觉到顺当、不好了解。随着咱们探讨的深刻,对问题域了解的深刻,咱们最终找到了一个绝对比拟适合的名字,才肯罢休。
这样的斟酌是有意义的,因为清晰要害概念,是咱们设计中的重要工作。尽管不合理的命名、不合理的形象也能实现业务性能。但其代价就是保护零碎时须要极高的认知负荷。随着工夫的推移,就没人能搞懂零碎的设计了。
=
形象的层次性
回到毕加索的抽象画,如下图所示,如果映射到面向对象编程,形象牛就是抽象类(Abstract Class),代表了所有牛的形象。形象牛能够泛化成更多的牛,比方水牛、奶牛、牦牛等。每一种牛都代表了一类(Class)牛,对于每一类牛,咱们能够通过实例化,失去一头具体的牛实例(Instance)。
从这个简略的案例中,咱们能够到形象的三个特点:
1. 形象是疏忽细节的。抽象类是最形象的,疏忽的细节也最多,就像形象牛,只是几根线条而已。在代码中,这种形象能够是 Abstract Class,也能够是 Interface。
2. 形象代表了独特性质。类(Class)代表了一组实例(Instance)的独特性质,抽象类(Abstract Class)代表了一组类的独特性质。对于咱们下面的案例来说,这些独特性质就是形象牛的那几根线条。
3. 形象具备层次性。抽象层次越高,外延越小,内涵越大,也就是说它的涵义越小,泛化能力越强。比方,牛就要比水牛更形象,因为它能够表白所有的牛,水牛只是牛的一个品种(Class)。
形象的这种层次性,是除了抽象概念之外,另一个咱们必须要深刻了解的概念,因为小到一个办法要怎么写,大到 一个零碎要如何架构,以及咱们前面第三章要介绍的结构化思维,都离不开抽象层次的概念。
在进一步介绍抽象层次之前,咱们先来了解一下内涵和外延的意思:
形象是以概念(词语)来反映事实的过程,每一个概念都有肯定的内涵和外延。概念的内涵就是适宜这个概念的所有对象的范畴,而 概念的外延就是这个概念所反映的对象的本质属性的总和。例如“平行四边形”这个概念,它的内涵蕴含着所有正方形、菱形、矩形以及个别的平行四边形,而它的外延蕴含着所有平行四边形所共有的“有四条边,两组对边相互平行”这两个本质属性。
一个概念的外延愈广,则其内涵愈狭;反之,外延愈狭,则其内涵愈广。例如,“平行四边形”的外延是“有四条边,两组对边相互平行”,而“菱形”的外延除了这两条本质属性外,还蕴含着“四边相等”这一本质属性。“菱形”的外延比“平行四边形”的外延广,而“菱形”的内涵要比“平行四边形”的内涵狭。
所谓的抽象层次就体现在概念的内涵和外延上,这种层次性,根本能够体现在任何事物上,比方一份报纸就存在多个档次上的形象,“出版品”最形象,其外延最小,但内涵最大,“出版品”能够是报纸也能够是期刊杂志等。
- 一个出版品
- 一份报纸
- 《旧金山纪事报》
- 5 月 18 日的《旧金山纪事报》
当我要统计美国有多少个出版品,那么就要用到最下面第一层“出版品”的形象,如果我要查问旧金山 5 月 18 日当天的新闻,就要用到最上面第四层的形象。
每一个抽象层次都有它的用处,对于咱们工程师来说,如何拿捏这个抽象层次是对咱们设计能力的考验,抽象层次太高和太低都不行。
比方,当初要写一个水果程序,咱们须要对水果进行形象,因为水果外面有红色的苹果,咱们当然能够建一个 RedApple 的类,然而这个抽象层次有点低,只能用来表白“红色的苹果”。来一个绿色的苹果,你还得新建一个 GreenApple 类。
为了晋升抽象层次,咱们能够把 RedApple 类改成 Apple 类,让色彩变成 Apple 的属性,这样红色和绿色的苹果就都能表白了。再持续往上形象,咱们还能够失去水果类、动物类等。再往上形象就是生物、物质了。
你能够看到,抽象层次越高,外延越小,内涵越大,泛化能力越强。然而,其代价就是业务语义表达能力越弱。
具体要形象到哪个档次,要视具体的状况而定了,比方这个程序是专门钻研苹果的可能到 Apple 就够了,如果是卖水果的可能须要到 Fruit,如果是动物钻研的可能要到 Plant,但很少须要到 Object。
我常常开玩笑说,你把所有的类都叫 Object,把所有的参数都叫 Map 的零碎最通用,因为 Object 和 Map 的外延最小,其延展性最强,能够适配所有的扩大。从原理上来说,这种形象也是对的,万物皆对象嘛。然而这种形象又有什么意义呢?它没有表白出任何想表白的货色,只是一句正确的废话而已。
越形象,越通用,可扩展性越强,然而其语义的表达能力越弱。越具体,越不好延展,然而其语义表达能力很强。所以,对于抽象层次的衡量,是咱们零碎设计的关键所在,也是辨别一般程序员和优良程序员的关键所在。
软件中的分层形象无处不在
越是简单的问题越须要分层形象,分层是分而治之,形象是问题域的正当划分和概念语义的表白。不同档次提供不同的形象,上层对下层暗藏实现细节,通过这种层次结构,咱们才有可能应答像网络通信、云计算等超级简单的问题。
网络通信是互联网最重要的根底施行,但同时它又是一个很简单的过程,你要晓得把数据包传给谁——IP 协定,你要晓得在这个不牢靠的网络上呈现情况要怎么办——TCP 协定。有这么多的事件须要解决,咱们可不可以在一个档次中都做掉呢?当然是能够的,但显然不迷信。因而,ISO 制订了网络通信的七层参考模型,每一层只解决一件事件,低层为下层提供服务,直到应用层把 HTTP、FTP 等不便了解和应用的协定裸露给用户。
编程语言的发展史也是一个典型的分层形象的演化史。
机器能了解的只有机器语言,即各种二进制的 01 指令。如果咱们采纳 01 的输出形式,其编程效率极低(学过数字电路的同学,领会下用开关实现加减法)。所以咱们用汇编语言形象了二进制指令。
然而汇编还是很底层,于是咱们用 C 语言形象了汇编语言。而高级语言 Java 是相似于 C 这样低级语言的进一步形象,这种逐层形象极大的晋升了咱们的编程效率。
=
反复代码是形象的缺失
如果说形象的实质是共性的话,那么咱们代码中的反复代码,是不是就意味着形象的缺失呢?
是这样的,反复代码是典型的代码坏滋味,其本质问题就是形象的缺失。因为咱们 Ctrl+C 加 Ctrl+V 的工作习惯,导致没有对共性代码进行抽取;或者尽管抽取了,只是简略的用了一个 Util 名字,没有给到一个适合的名字,没有正确的反馈这段代码所体现的抽象概念,都属于形象不到位。
有一次,我在 Review 团队代码的时候,发现有一段组装搜寻条件的代码,在几十个中央都有反复。这个搜寻条件还比较复杂,是以元数据的模式存在数据库中,因而组装的过程是这样的:
- 首先,咱们要从缓存中把搜寻条件列表取出来;
- 而后,遍历这些条件,将搜寻的值填充进去;
// 取默认搜寻条件
List<String> defaultConditions = searchConditionCacheTunnel.getJsonQueryByLabelKey(labelKey);
for(String jsonQuery : defaultConditions){jsonQuery = jsonQuery.replaceAll(SearchConstants.SEARCH_DEFAULT_PUBLICSEA_ENABLE_TIME, String.valueOf(System.currentTimeMillis() / 1000));
jsonQueryList.add(jsonQuery);
}
// 取主搜寻框的搜寻条件
if(StringUtils.isNotEmpty(cmd.getContent())){List<String> jsonValues = searchConditionCacheTunnel.getJsonQueryByLabelKey(SearchConstants.ICBU_SALES_MAIN_SEARCH);
for (String value : jsonValues) {String content = StringUtil.transferQuotation(cmd.getContent());
value = StringUtil.replaceAll(value, SearchConstants.SEARCH_DEFAULT_MAIN, content);
jsonQueryList.add(value);
}
}
简略的重构无外乎就是把这段代码提取进去,放到一个 Util 类外面给大家复用。然而我认为这样的重构只是实现了工作的一半,咱们只是做了简略的归类,并没有做形象提炼。
简略剖析,不难发现,此处咱们是缺失了两个概念:一个是用来表白搜寻条件的类——SearchCondition;另一个是用来组装搜寻条件的类——SearchConditionAssembler。只有配合命名,显性化的将这两个概念表达出来,才是一个残缺的重构。
重构后,搜寻条件的组装会变成一种十分简洁的模式,几十处的复用只须要援用 SearchConditionAssembler 就好了。
public class SearchConditionAssembler {public static SearchCondition assemble(String labelKey){String jsonSearchCondition = getJsonSearchConditionFromCache(labelKey);
SearchCondition sc = assembleSearchCondition(jsonSearchCondition);
return sc;
}
}
由此可见,提取反复代码只是咱们重构工作的第一步。对反复代码进行概念形象,寻找有意义的命名才是咱们工作的重点。
因而,每一次遇到反复代码的时候,你都应该感到兴奋,想着这是一次锤炼形象能力的绝佳机会,当然,测试代码除外。
强制类型转换是抽象层次有问题
面向对象设计外面有一个驰名的 SOLID 准则是由 Bob 大叔(Robert Martin)提出来的,其中的 L 代表 LSP,就是 Liskov Substitution Principle(里氏替换准则)。简略来说,里氏替换准则就是子类应该能够替换任何父类可能呈现的中央,并且通过替换当前,代码还能失常工作。
思考一下,咱们在写代码的过程中,什么时候会用到强制类型转换呢?当然是 LSP 不能被满足的时候,也就是说子类的办法超出了父类的类型定义范畴,为了能应用到子类的办法,只能应用类型强制转换将类型转成子类类型。
举个例子,在苹果(Apple)类上,有一个 isSweet() 办法是用来判断水果甜不甜的;西瓜(Watermelon)类上,有一个 isJuicy() 是来判断水分是否短缺的;同时,它们都独特继承一个水果(Fruit)类。
此时,咱们须要挑选出甜的水果和有水分的习惯,咱们会写一个如下的程序:
public class FruitPicker {
public List<Fruit> pickGood(List<Fruit> fruits){return fruits.stream().filter(e -> check(e)).
collect(Collectors.toList());
}
private boolean check(Fruit e) {if(e instanceof Apple){if(((Apple) e).isSweet()){return true;}
}
if(e instanceof Watermelon){if(((Watermelon) e).isJuicy()){return true;}
}
return false;
}
}
因为 pick 办法的入参的类型是 Fruit,所以为了取得 Apple 和 Watermelon 上的特有办法,咱们不得不应用 instanceof 做一个类型判断,而后应用强制类型转换转成子类类型,以便取得他们的专有办法,很显然,这是违反了里式替换准则的。
这里问题出在哪里?对于这样的代码咱们要如何去优化呢?仔细分析一下,咱们能够发现,根本原因是因为 isSweet 和 isJuicy 的抽象层次不够,站在更高抽象层次也就是 Fruit 的视角看,咱们筛选的就是可口的水果,只是具体到苹果咱们看甜度,具体到西瓜咱们看水分而已。
因而,解决办法就是对 isSweet 和 isJuicy 进行形象,并晋升一个档次,在 Fruit 上创立一个 isTasty() 的形象办法,而后让苹果和西瓜类别离去实现这个形象办法就好了。
上面是重构后的代码,通过抽象层次的晋升咱们打消了 instanceof 判断和强制类型转换,让代码从新满足了里式替换准则。抽象层次的晋升使得代码从新变得优雅了。
public class FruitPicker {
public List<Fruit> pickGood(List<Fruit> fruits){return fruits.stream().filter(e -> check(e)).
collect(Collectors.toList());
}
// 不再须要 instanceof 和强制类型转换
private boolean check(Fruit e) {return e.isTasty();
}
}
所以,每当咱们在程序中筹备应用 instanceof 做类型判断,或者用 cast 做强制类型转换的时候。每当咱们的程序不满足 LSP 的时候。你都应该警醒一下,好家伙,这又是一次锤炼形象能力的绝佳机会。
=
如何晋升抽象思维能力
抽象思维能力是咱们人类特有的、与生俱来的能力,除了下面说的在编码过程中能够锤炼形象能力之外,咱们还能够通过一些其余的练习,一直的晋升咱们的形象能力。
多浏览
为什么浏览书籍比看电视更好呢?因为图像比文字更加具象,浏览的过程能够锤炼咱们的形象能力、设想能力,而看画面的时候会将你的大脑铺满,较少须要形象和设想。
这也是为什么咱们不提倡让小孩子过多的裸露在电视或手机屏幕前的起因,因为这样不利于他抽象思维的锤炼。
抽象思维的差异让孩子们的学习成绩从初中开始分化,许多不能适应这种形象层面训练的,就去读技校了,因为技校比大学会更加具象:车铣刨磨、零部件都能看得见摸得着。体力劳动要比脑力劳动来的简略。
多总结积淀
小时候不了解,语文老师为什么总是要求咱们总结段落粗心、中心思想什么的。当初回想起来,这种思维训练在基础教育中是十分必要的,其实质就是帮忙学生晋升抽象思维能力。
记录也是很好的总结习惯。就拿读书笔记来说,最好不要原文摘录书中的内容,而是要用本人的话总结演绎书中的内容,这样不仅能够加深了解,而且还能够晋升本人的抽象思维能力。
我从四年前开始零碎的记录笔记,做总结积淀,构建本人的常识体系。这种思维训练的益处不言而喻,能够说我之前写的《从码农到工匠》和当初正在写的《程序员必备的思维能力》都离不开我总结积淀的习惯。
命名训练
每一次的变量命名、办法命名、类命名都是一次难得的抽象思维训练机会,后面曾经说过了,语言和形象是一体的,命名的好坏间接反馈了咱们的问题域思考的是否清晰,反映了咱们形象的是否正当。
现实情况是,咱们很多的工程师经常疏忽了命名的重要性,只有能实现业务性能,名字素来就不是重点。
实际上,这是对系统的不负责任,也是对本人的不负责任,更是对前期保护零碎的人不负责任。写程序和写文章有很大的相似性,实质上都是在用语言论述一件事件。试想下,如果文章中用的都是些词不达意的句子,这样的文章谁能看得懂,谁又违心去看呢。
同样,我始终强调代码要显性化的表白业务语义,其中命名在这个过程中表演了极其重要的角色。为了代码的可读性,为了零碎的长期可维护性,为了咱们本身抽象思维的训练,咱们都不应该放过任何一个带有歧义、表白含糊、意不清的命名。
领域建模训练
对于技术同学,咱们还有一个十分好的晋升形象能力的伎俩——领域建模。当咱们对问题域进行剖析、整顿和形象的时候,当咱们对畛域进行划分和建模的时候,实际上也是在锤炼咱们的形象能力。
咱们能够对本人工作中的问题域进行建模,当然也能够通过浏览一些优良源码背地的模型设计来学习如何形象、如何建模。比方,咱们晓得 Spring 的外围性能是 Bean 容器,那么在看 Spring 源码的时候,咱们能够着重去看它是如何进行 Bean 治理的?它应用的外围形象是什么?不难发现,Spring 是应用了 BeanDefinition、BeanFactory、BeanDefinitionRegistry、BeanDefinitionReader 等外围形象实现了 Bean 的定义、获取和创立。抓住了这些外围形象,咱们就抓住了 Spring 设计主脉。
除此之外,咱们还能够进一步深刻思考,它为什么要这么形象?这样形象的益处是什么?以及它是如何反对 XML 和 Annotation(注解)这两种对于 Bean 的定义的。
这样的抽象思维锤炼和思考,对晋升咱们的形象能力和建模能力十分重要。对于这一点,我深有感触,初入职场的时候,当我尝试对问题域进行形象和建模的时候,会感觉无从下手,建进去的模型也感觉很顺当。
然而,通过长期的、刻意的学习和锤炼之后,很显著能够感觉到我的建模能力和形象能力都有很大的晋升。岂但剖析问题的速度更快了,而且建进去的模型也更加优雅了。
小结
- 抽象思维是程序员最重要的思维能力,形象的过程就是寻找共性、演绎总结、综合剖析,提炼出相干概念的过程。
- 语言和形象是一体的,抽象思维也叫词思维,因为形象的概念只能通过语言能力表达出来。
- 形象是有层次性的,抽象层次越高,外延越小,内涵越大,扩展性越好;反之,抽象层次越低,外延越大,内涵越小,扩展性越差,但语义表达能力越强。
- 对抽象层次的拿捏,体现了咱们的设计功力,视具体情况而定,抽象层次既不能太高,也不能太低。
- 反复代码意味着形象缺失,强制类型转换意味着抽象层次有问题,咱们能够利用这些信号来重构代码,让代码从新变的优雅。
- 咱们能够通过刻意练习来晋升形象能力,这些练习包含浏览、总结、命名训练、建模训练等。
参考文献:
[1] https://baike.baidu.com/item/…
[2] https://zh.wikipedia.org/wiki…
[3] https://baike.baidu.com/item/…
[4] https://www.sohu.com/a/359915…