程序员必备的思维能力:笼统思维

作者 : 开心源码 本文共9716个字,预计阅读时间需要25分钟 发布时间: 2022-05-14 共192人阅读

若想捉大鱼,就得潜入深渊。深渊里的鱼更有力,也更纯净。硕大而笼统,且非常美丽。——大卫·林奇

笼统思维是我们工程师最重要的思维能力。由于软件技术 本质上就是一门笼统的艺术。我们的工作是存思维的“游戏”,尽管我们在使用键盘、显示器,打开电脑可以看到主板、硬盘等硬件。但我们即看不到程序如何被执行,也看不到0101是如何被CPU解决的。

我们工程师每天都要动用笼统思维,对问题域进行分析、归纳、综合、判断、推理。从而笼统出各种概念,挖掘概念和概念之间的关系,对问题域进行建模,而后通过编程语言实现业务功能。所以,我们大部分的时间并不是在写代码,而是在梳理需求,理清概念,当然,也包括尝试看懂那些“该死的、别人写的”代码。

在我接触的工程师中,能深入了解笼统概念的并不多,能把笼统和面向对象、架构设计进行有机结合,能用笼统思维进行问题分析、化繁为简的同学更是凤毛麟角。

对于我本人而言,每当我对笼统有进一步的了解和认知,我都能切身感受到它给我在编码和设计上带来的质的变化。同时感慨之前对笼统的了解为什么如此肤浅。假如时间可以倒流的话,我希望我在我职业生涯的早期,就能充分意识到笼统的重要性,能多花时间认真的研究它,深刻的了解它,这样应该可以少走很多弯路。

一、什么是笼统

关于笼统的定义,百度百科是这样说的:

笼统是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。具体地说,笼统就是人们在实践的基础上,对于丰富的感性材料通过去粗取精、去伪存真、由此及彼、由表及里的加工制作,形成概念、判断、推理等思维形式,以反映事物的本质和规律的方法。
实际上,笼统是与具体相对应的概念,具体是事物的多种属性的总和,因此笼统亦可了解为由具体事物的多种属性中舍弃了若干属性而固定了另少量属性的思维活动。

Wikipedia的解释是:

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

简单而言,“抽”就是抽离,“象”就是具象,字面上了解笼统,笼统的过程就是从“具象”事物中归纳出共同特征,“抽取”得到一般化(Generalization)的概念的过程。英文的笼统——abstract来自拉丁文abstractio,它的原意是排除、抽出。

为了更好的方便你了解笼统,让我们先来看一幅毕加索的画,如下图所示,图的左边是一头水牛,是具象的,右边是毕加索画,是笼统的。怎样样,是不是感觉自己一下子了解了笼统画的含义。

可以看到,笼统牛只有几根线条,不过这几根线条是做了高度笼统之后的线条,过滤了水牛的绝大部分细节,保留了牛最本质特征,比方牛角,牛头,牛鞭、牛尾巴等等。这种对细节的舍弃使得“笼统牛”具备更好的泛化(Generalization)能力。可以说,笼统更接近问题的本质。也就是说所有的牛都逃不过这几根线条。

二、笼统和语言是一体的

关于笼统思维,我们在百度百科上可以看到如下的定义:

笼统思维,又称词(概念)的思维或者者逻辑思维,是指用词(概念)进行判断、推理并得出结论的过程。笼统思维以词(概念)为中介来反映现实。这是思维的最本质特征,也是人的思维和动物心理的根本区别。

之所以把笼统思维称为词思维或者者概念思维,是由于语言和笼统是一体的。当我们说“牛”的时候,说的就是“牛”的笼统,他代表了所有牛共有的特征。同样,当你在程序中创立Cow这个类的时候,道理也是一样。在生活中,我们只见过一头一头具象的牛,“牛”作为笼统的存在,即看不见也摸不着。

这种把笼统概念作为世界本真的看法,也是古希腊哲学家柏拉图的最重要哲学思想。柏拉图认为,我们所有用感觉感知到的事物,都源于相应的理念。他认为具体事物的“名”,也就是他说的“理念世界”才是本真的东西,具体的一头牛,有大有小,有公有母,颜色、性情、外形各自不同。因而我们不好用个体感觉加以概括,但是这些牛既然都被统称为“牛”,则说明它们必然都源于同一个“理念”,即所谓“牛的理念”或者者“理念的牛”,所以它们可以用“牛”加以概括。尚且不管“理念世界”能否真的存在,这是一个哲学识题,但有一点可以确定,我们的思考,对概念的表达都离不开语言。

这也是为什么,我在做设计和代码审查(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)。

笼统的这种层次性,是除了笼统概念之外,另一个我们必需要深入了解的概念,由于小到一个方法要怎样写,大到 一个系统要如何架构,以及我们后面第三章要详情的结构化思维,都离不开笼统层次的概念。

在进一步详情笼统层次之前,我们先来了解一下外延和内涵的意思:

笼统是以概念(词语)来反映现实的过程,每一个概念都有肯定的外延和内涵.概念的外延就是适合这个概念的一切对象的范围,而概念的内涵就是这个概念所反映的对象的本质属性的总和.例如“平行四边形”这个概念,它的外延包含着一切正方形、菱形、矩形以及一般的平行四边形,而它的内涵包含着一切平行四边形所共有的“有四条边,两组对边互相平行”这两个本质属性。

一个概念的内涵愈广,则其外延愈狭;反之,内涵愈狭,则其外延愈广。例如,“平行四边形”的内涵是“有四条边,两组对边互相平行”,而“菱形”的内涵除了这两条本质属性外,还包含着“四边相等”这一本质属性。“菱形”的内涵比“平行四边形”的内涵广,而“菱形”的外延要比“平行四边形”的外延狭。

所谓的笼统层次就表现在概念的外延和内涵上,这种层次性,基本可以表现在任何事物上,比方一份报纸就存在多个层次上的笼统,“出版品”最笼统,其内涵最小,但外延最大,“出版品”可以是报纸也可以是期刊杂志等。

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

当我要统计美国有多少个出版品,那么就要用到最上面第一层“出版品”的笼统,假如我要查询旧金山5月18日当天的新闻,就要用到最下面第四层的笼统。

每一个笼统层次都有它的用途,对于我们工程师来说,如何拿捏这个笼统层次是对我们设计能力的考验,笼统层次太高和太低都不行。

比方,现在要写一个水果程序,我们需要对水果进行笼统,由于水果里面有红色的苹果,我们当然可以建一个RedApple的类,但是这个笼统层次有点低,只能用来表达“红色的苹果”。来一个绿色的苹果,你还得新建一个GreenApple类。

为了提升笼统层次,我们可以把RedApple类改成Apple类,让颜色变成Apple的属性,这样红色和绿色的苹果就都能表达了。再继续往上笼统,我们还可以得到水果类、植物类等。再往上笼统就是生物、物质了。

你可以看到,笼统层次越高,内涵越小,外延越大,泛化能力越强。然而,其代价就是业务语义表达能力越弱。

具体要笼统到哪个层次,要视具体的情况而定了,比方这个程序是专门研究苹果的可能到Apple就够了,假如是卖水果的可能需要到Fruit,假如是植物研究的可能要到Plant,但很少需要到Object。

我经常开玩笑说,你把所有的类都叫Object,把所有的参数都叫Map的系统最通用,由于Object和Map的内涵最小,其延展性最强,可以适配所有的扩展。从原理上来说,这种笼统也是对的,万物皆对象嘛。但是这种笼统又有什么意义呢?它没有表达出任何想表达的东西,只是一句正确的废话而已。

越笼统,越通用,可扩展性越强,然而其语义的表达能力越弱。越具体,越不好延展,然而其语义表达能力很强。所以,对于笼统层次的权衡,是我们系统设计的关键所在,也是区分普通程序员和优秀程序员的关键所在。

四、软件中的分层笼统无处不在

越是复杂的问题越需要分层笼统,分层是分而治之,笼统是问题域的正当划分和概念语义的表达。不同层次提供不同的笼统,下层对上层隐藏实现细节,通过这种层次结构,我们才有可能应对像网络通信、云计算等超级复杂的问题。

网络通信是互联网最重要的基础设备,但同时它又是一个很复杂的过程,你要知道把数据包传给谁——IP协议,你要知道在这个不可靠的网络上出现状况要怎样办——TCP协议。有这么多的事情需要解决,我们可不可以在一个层次中都做掉呢?当然是可以的,但显然不科学。因而,ISO制定了网络通信的七层参考模型,每一层只解决一件事情,低层为上层提供服务,直到应用层把HTTP,FTP等方便了解和使用的协议暴露给客户。

编程语言的发展史也是一个典型的分层笼统的演化史。

机器能了解的只有机器语言,即各种二进制的01指令。假如我们采用O1的输入方式,其编程效率极低(学过数字电路的同学,体会下用开关实现加减法)。所以我们用汇编语言笼统了二进制指令。然而汇编还是很底层,于是我们用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的定义的。

这样的笼统思维锻炼和思考,对提升我们的笼统能力和建模能力非常重要。关于这一点,我深有感触,初入职场的时候,当我尝试对问题域进行笼统和建模的时候,会觉得无从下手,建出来的模型也感觉很别扭。然而,经过长期的、刻意的学习和锻炼之后,很显著可以感觉到我的建模能力和笼统能力都有很大的提升。不但分析问题的速度更快了,而且建出来的模型也更加优雅了。

八 小结

  • 笼统思维是程序员最重要的思维能力,笼统的过程就是寻觅共性、归纳总结、综合分析,提炼出相关概念的过程。
  • 语言和笼统是一体的,笼统思维也叫词思维,由于笼统的概念只能通过语言才能表达出来。
  • 笼统是有层次性的,笼统层次越高,内涵越小,外延越大,扩展性越好;反之,笼统层次越低,内涵越大,外延越小,扩展性越差,但语义表达能力越强。
  • 对笼统层次的拿捏,表现了我们的设计功力,视具体情况而定,笼统层次既不能太高,也不能太低。
  • 重复代码意味着笼统缺失,强制类型转换意味着笼统层次有问题,我们可以利用这些信号来重构代码,让代码重新变得优雅。
  • 我们可以通过刻意练习来提升笼统能力,这些练习包括阅读、总结、命名训练、建模训练等。

作者:技术匠人
链接:https://www.toutiao.com/a6959815066481000973/?log_from=f1334f2adb0ef_1627027904124

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 程序员必备的思维能力:笼统思维

发表回复