To程序员:要写出好代码,你需要懂点儿“底层思维”
👆点击“博文视点Broadview”,获取更多书讯
在阿里巴巴的晋升会议上,评委经常会问:“你的成功可以复制吗?”
我最初做评委时基本不会问这样的问题,因为我认为这样的问题很虚,工作完成就行了,不需要那么多道理。
然而随着时间的推移,我发现这的确是一个好问题。
因为它可以区分出你是碰巧把事情做对了,还是你具备了一直做对事情的能力,二者是有本质区别的。碰巧做对,说明你的能力可能还不足,换一种情景,你就不一定能应付。
因此,好的晋升制度不仅要考查成绩,更重要的是考查能力。对从事脑力劳动的技术人员来说,“能力”主要指的是“思维能力”。
例如,我们都知道编程的时候命名很重要,也很难,可为什么会这样呢?
如果要深挖其背后的原因,将是一个非常有趣的话题,甚至可以和哲学有关。
命名工作中暗含了抽象思维能力和语言哲学,语言本身是抽象的符号,比如当你说“花”的时候,指的并不是某一朵具体的玫瑰花、郁金香,而是花的抽象概念。一朵具体的花虽然看得见、摸得着,但总会有凋零消亡的时候,而“花”这个字作为精神实体将永不会消亡。所以,抽象的花和具体的花到底哪个才是本真呢?这是一个哲学问题。
抛开哲学争论,就“花”这个字而言,它是提取了所有花的共性的抽象符号。命名之所以难,是因为你要经历一个提取共性、归纳要义,并赋予恰当名称的抽象思维过程。
因此,要想真正做好命名,除了要掌握一些命名技法,还需要更深层次的修炼——提升抽象思维能力。
在《西方哲学史》中,奥古斯丁说:“至于什么是时间,在没人问我时,我非常清楚;可一旦要向别人解释,我就有点糊涂了。”
对于抽象的概念也是如此,我们都知道,抽象思维是工程师最重要的思维能力。因为软件设计是纯思维的创造活动,软件技术本质上就是一门抽象的艺术。程序员每天都要动用抽象思维,对问题域进行分析、归纳、综合、判断、推理等。
但到底什么是抽象思维?什么是抽象的层次性?如何运用抽象思维解决软件中的问题?如何提升抽象思维能力?对于这些问题,并不是每个人都能说的清楚。
简而言之,“抽”就是抽离,“象”就是具象。从字面上理解抽象,抽象的过程就是从“具象”事物中归纳共同特征,“抽取”得到一般化的概念的过程。英文的抽象——abstract,来自拉丁文abstractio,它的原意是排除、抽出。
为了更直观地理解抽象,让我们先来看一幅毕加索的画。图的左边是一头水牛,是具象的;右边是毕加索的画,是抽象的。
抽象牛只有几根线条,不过这几根线条是做了高度抽象之后的线条,过滤了水牛的绝大部分细节,只保留了牛最本质的特征,比如牛角、牛头、牛鞭、牛尾巴等。这种对细节的舍弃使得“抽象牛”具有更好的泛化(Generalization)能力。可以说,抽象概念更接近问题的本质。也就是说,所有的牛都逃不过这几根线条。
回到毕加索的抽象画,如果映射到面向对象编程,抽象牛就是抽象类(Abstract Class),代表了所有牛的抽象。抽象牛可以被泛化成更多的牛,比如水牛、奶牛、牦牛等。每一种牛都代表了一类(Class)牛,对于每一类牛,我们可以通过实例化,得到一个具体的牛实例(Instance)。
从这个简单的案例中,我们可以总结出抽象的3个特点。
第一,抽象是忽略细节的。抽象类是最抽象的,忽略的细节也最多,就像抽象牛,只是几根线条而已。在代码中,这种抽象既可以是抽象类,也可以是接口(Interface)。
第二,抽象代表了共同性质。类代表了一组实例的共同性质,抽象类代表了一组类的共同性质。对于上面的案例来说,共同性质就是抽象牛的那几根线条。
第三,抽象具有层次性。抽象层次越高,其内涵越小、外延越大,也就是说它的含义越小、泛化能力越强。比如,牛就要比水牛的抽象层次更高,因为它可以表达所有的牛,水牛只是牛的一个种类。
对于程序员来说,对抽象层次的权衡是对我们设计能力的考验,要根据业务的需要,选择合理的抽象层次,既不能太高,也不能太低。
例如,现在要写一个关于水果的程序,我们需要对水果进行抽象,因为水果里面有红色的苹果,我们当然可以建一个RedApple的类,但是这个抽象层次有点低,只能用来表达“红色的苹果”。假如来一个绿色的苹果,你还得新建一个GreenApple类。
为了提升抽象层次,我们可以把RedApple类改成Apple类,让颜色变成Apple的属性,这样红色和绿色的苹果就都能用Apple表达了。再继续往上抽象,我们还可以得到水果类、植物类等。
可以看出,抽象层次越高,内涵越小,外延越大,泛化能力越强。然而,其代价就是业务语义表达能力越弱。
我经常开玩笑说:“为了通用性,把所有的类都设置为Object,把所有的参数都设置为Map的系统,是最通用的。”因为Object和Map的内涵最小,其泛化能力最强,可以适配所有的扩展。从原理上来说,这种抽象也是对的,万物皆对象嘛!但我们为什么不这么做呢?就是因为越抽象,越空洞,虽然通用,但也“无用”。
思考一下,我们在写代码的过程中,什么时候会用到强制类型转换呢?当然是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;
}
}
对于代码中的强制类型转换,仔细分析一下,可以发现,根本原因在于isSweet()和isJuicy()的抽象层次不够。站在更高的抽象层次,也就是Fruit的视角看,我们挑选的就是可口的水果,只是具体到苹果时,我们看甜度;具体到西瓜时,我们看水分而已。
解决方法是对isSweet( )和isJuicy( )进行抽象层次提升,我们可以在Fruit上创建一个isTasty( )的抽象方法。
注意:tasty这个词本身就要比sweet和juicy要抽象。
因此,我们在做抽象提升的时候,一定是伴随着语言的变化的。
通过提升抽象层次,我们消除了instanceof判断和强制类型转换,让代码重新满足了里氏替换原则,利用面向对象的多态,也使代码重新变得优雅可扩展了。
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) {
return e.isTasty(); //不再需要instanceof和强制类型转换
}
}
所以,每当我们在程序中准备使用instanceof做类型判断,或者用cast做强制类型转换的时候,都有可能是一次做提升抽象层次的机会。
本文节选自《程序员的底层思维》一书,想要了解更多相关内容,欢迎阅读本书!
▊ 《程序员的底层思维》
张建飞 著
这是一本超越具体编程技法的技术书:职场晋升不仅需要技术能力,更重要的是思维能力。本书带你学会用底层思维解决复杂技术问题,突破职场“天花板”。
这也是一本培养思维能力的通用技能书:打破认知局限,培养通用的思维能力。本书帮你跳出思维定势,轻松解决生活及工作中遇到的问题。
本书涵盖程序员应知应会的16种思维能力,共18章,分为三部分。
第一部分主要介绍抽象思维、逻辑思维、结构化思维、批判性思维、维度思维、分类思维、分治思维、简单思维,以及成长型思维等解决日常问题的基础思维能力。
第二部分结合软件行业的特点,主要介绍解耦思维、契约思维、模型思维、工具化思维、量化思维、数据思维,以及产品思维等专业思维能力。
第三部分主要是对上述思维能力的综合运用实践。
粉丝专享六折优惠,扫码即购!
热文推荐
▼点击阅读原文,了解本书详情~