计算机基础原来可以如此好懂!——『面向抽象编程』

From Unsplash

“面向抽象编程,面向接口编程”这句话流传甚广,它像一面旗帜插在每个人前进的道路上,引导大家前行。每个程序员都免不了和抽象打交道,差距可能在于能否更好地提炼。这句话包含两部分含义:“面向抽象编程”本质上是对数据的抽象化,“面向接口编程”本质上是对行为的抽象化。

我们先来谈谈数据的抽象化——面向抽象编程。

抽象最讨厌的敌人:new

因为直接讲什么是抽象不太好讲,容易描述的话那就不是抽象了,所以我们换个角度,先聊聊抽象的反面:什么是具体。在具体里,有个先锋人物,就是我们都熟悉的 new。大家知道,new 是最简单和最常见的关键字,用来创建对象。但被创建出来的一定是具体的对象,所以new 代表着具体,它是抽象最讨厌的敌人。

大家要有这种敏感:什么时机创建对象,在哪里创建,是很有讲究的。为了阐述这个话题,我们先看下面这行代码:

Animalanimal= new Tiger(); //Animal是抽象类

我曾经对这句简单的赋值语句思考很久:左边抽象,右边具体,感觉不对等,这样写好不好?答案不简单啊。

接下来,我们分成两个方向细细讨论。

假设一:如果它是某个类的成员变量的定义。例如:

privateAnimal animal =newTiger();

先下结论:如果类里其他地方没有对 animal 这个变量的赋值操作,此后再没有更改它的逻辑了,那么它基本不是好写法(有少许例外)。那么,什么是好写法?

哈,这里先卖个关子。

这里需要注意的是,我们讨论的是左边是抽象,右边是具体的 new。如果 new 的两边是平级概念的类,例如:

Tiger tiger= new Tiger();

它左右两边没有抽象之分,那么不在我们讨论范围之内。

假设二:如果它是某个函数内部的变量定义语句。示例如下:

voidShow() {Animal animal =newTiger();......// 出场前的准备活动ShowAnimal(animal);}

我曾经疑惑:为何不直接定义成子类类型?就这样写:

Tiger tiger= new Tiger();

根据继承原理,子类能调用抽象类的方法。所以也不会影响接下来的函数调用。例如:所有的animal.Eat 替换为 tiger.Eat 一定成立。

同时根据里氏替换原则,但凡出现animal 的地方,都可以把tiger 代替进去,所以也不会影响我的参数传递。例如:ShowAnimal(animal)替换为ShowAnimal(tiger)也一定成立。

可一旦把 Tiger 类型上溯转为抽象的 Animal 类型,那么 Tiger 自身的特殊能力(例如Hunt)在“出场前的准备活动”那部分就用不了,例如:

tiger.Hunt();// 老虎进行狩猎animal.Hunt();// 不能通过编译

也就是说,Animal animal = new Tiger();里Animal 的抽象定义,只有限制我自由的作用,而没有带来任何实质的好处!这种写法不是很糟糕吗?

你会有一天顿悟:这种对自由的限制,恰恰是最珍贵的!大部分时候,我们缺的不是自由,而是自律。任何人的自由,都不能以损害别人的利益为代价。

ShowAnimal(animal);之前的那段“出场前的准备活动”代码,将来很有可能是别人来维护的。在架构设计上,一定要考虑“时间”这个变量带来的不确定性。如果你定义成:

Tiger tiger= new Tiger();

这看起来更灵活,但你没法阻止这只老虎被别人将来使用Hunt 函数滥杀无辜。

一旦定义为:

Animal animal= new Tiger();

那么,这只老虎将会是一只温顺的老虎,只遵循普通的动物准则。

所以如果“出场前的准备活动”这部分的业务需求里只用到Animal 的基本功能函数,那么:

Animal animal= new Tiger();

要优于

Tiger tiger= new Tiger();

好了,等号左边的抽象问题解决了,但等号右边的 new 呢?这个场景里,Animal animal =new Tiger();是函数的局部变量,也没有传导到全局变量中。到目前为止,这个new 是完全可以接受的。面向抽象,是要在关键且合适的地方去抽象,如果处处都抽象,代价会非常大,得不偿失。如果满分是 100 分的话,目前能得 95 分,已经很好了,这也是我们大多数时候的写法。

但你还是要知道:一旦接受了这个 new,好比是和魔鬼做了契约,会付出潜在代价的。此处的代价是这段代码不能再升级成框架性的抽象代码了。想要完美得到 100 分,则需要消灭这个new,怎么办呢?

消灭 new 的两件武器

上面站在理论高度“批判”了new,其实并不是说 new 真的不好,而是说很多人会滥用。就好比火是人类文明的起源,好东西,但是滥用就会造成火灾。把火源限定在特定工具才能点火,隔离开,用起来才安全。new 其实也一样,下面讲的本质上不是消灭 new,而是隔离 new 的两件武器。

控制反转——脏活让别人去干

还记得前面卖的关子吗?如果 animal 是类成员变量:

privateAnimal animal =newTiger();

这并不是好写法,那么什么是好写法呢?这种情况下,比较简单的是对它进行参数化改造:

void setAnimal(Animalanimal) { this.animal=animal;}

然后让客户去调用注入:

Tigertiger =newTiger();obj.setAnimal(tiger);

有了上面的注入代码,private Animal animal = new Tiger();这句话反而变得可以接受了。因为等号右边的 Tiger 仅仅是默认值,默认值当然是具体的。

上面的参数化改造手法,我们可以称为“依赖注入”,其核心思想是:不要调我,我会去调你!依赖注入分为属性注入、构造函数注入和普通函数注入。很明显,上面的例子是属性注入。

依赖注入和标题的“控制反转”还不能完全划等号。确切地说,“依赖注入”是实现“控制反转”的方式之一。

这种干脆把创建对象的任务甩手不干的事情,反而是个好写法,境界高!这样,你不知不觉把自己的代码完全变成了只负责数据流转的框架性代码,具备了通用性。

在通往架构师的道路上,你要培养出一种感觉:要创建一个跨作用域的实体对象(不是值对象)是一件很谨慎的事情(越接触大型项目,你对这点的体会就越深),不要随便创建。最好不要自己创建,让别人去创建,传给你去调用。那么问题来了:都不愿意去创建,谁去创建?这个丢手绢的游戏最终到底要丢给谁呢?

先把问题揣着,我们接着往下看。

工厂模式——抽象的基础设施

我们回到这段Show 代码:

voidShow() { Animal animal =newTiger();// 上面说过,这里的 new 目前是可以接受的......// 出场前的准备活动ShowAnimal(animal);}

但如果Show 方法里创建动物的需求变得复杂,new 会变得猖狂起来:

voidShow(stringname) { Animal animal;if(name =="Tiger") animal =newTiger();elseif(name =="Lion") animal =newLion();......// 其他种类ShowAnimal(animal);}

此时将变得不可接受了。对付这么多同质的 new(都是创建Animal),一般会将它们封装进专门生产 animal 的工厂里:

Animal ProvideAnimal(stringname) { Animal animal;if(name =="Tiger") animal =newTiger();elseif(name =="Lion") animal =newLion();......// 其他种类}

进而优化了 Show 代码:

voidShow(stringname){ Animal animal = ProvideAnimal(name);// 等号两边都是同级别的抽象,这下彻底舒服了ShowAnimal(animal);}

因此,依赖注入和工厂模式是消灭 new 的两种武器。此外,它们也经常结合使用。

上面的 ProvideAnimal 函数采用的是简单工厂模式。由于工厂模式是每个人都会遇到的基本设计模式,所以这里会对它进行更深入的阐述,让大家能更深入地理解它。工厂模式严格说来有简单工厂模式和抽象工厂模式之分,但真正算得上设计模式的,是抽象工厂模式。简单工厂模式仅仅是比较自然的简单封装,有点配不上一种设计模式的称呼。因此,很多教科书会大篇幅地介绍抽象工厂,而有意无意地忽略了简单工厂。但实际情况正好相反,抽象工厂大部分人一辈子都用不上一次(它的出现要依赖于对多个相关类族创建对象的复杂需求场景),而简单工厂几乎每个人都用得上。

和一般的设计模式不一样,有些设计模式的代码结构哪怕你已经烂熟于心,却依然很难想象它们的具体使用场景。工厂模式是面向抽象编程,数据的创建需求变复杂之后很自然的产物,很多人都能无师自通地去使用它。将面向抽象编程坚持到底,会自然地把创建对象的任务外包出去,丢给专门的工厂去创建。

可见,工厂模式在整个可扩展的架构中扮演的不是先锋队角色,而是强有力的支持“面向抽象编程”的基础设施之一。

最后调侃一下,我面试候选人的时候,很喜欢问他们一个问题:“你最常用的设计模式有哪些?”

排第一的是“单例模式”,而“工厂模式”是当之无愧的第二名,排第三的是“观察者模式”。这侧面说明这三种模式应该是广大程序员最容易用到的设计模式。大家学习设计模式时,首先应该仔细研究这三种模式及其变种。

new 去哪里了呢

这里回到最开始也是最关键的问题:如果大家都不去创建,那么谁去创建呢?把脏活丢给别人,那别人是谁呢?下面我们从两个方面阐述。

■ 局部变量。局部变量是指在函数内部生产又在函数内部消失的变量,外部并不知晓它的存在。在函数内部创建它们就好,这也是我们遇到的大多数情况。例如:

voidShow() { Animal animal =newTiger();......// 出场前的准备活动ShowAnimal(animal);}

前面说过,这段代码里的 new 能得95 分,没有问题。

■ 跨作用域变量。对这类对象的创建,总是要小心一些的。

○ 如果是零散的创建,就让各个客户端自己去创建。这里的客户端是泛指的概念,不是服务器对应的客户端。凡是调用核心模块的发起方,均属于客户端。每个客户端是知道自身具体细节的,在它内部创建无可厚非。

○ 如果写的是框架性代码,是基于总体规则的创建,那就在核心模块里采用专门的工厂去创建。

抽象到什么程度

前面说过,完全具体肯定不行,缺乏弹性。但紧接着另一个问题来了:越抽象就越好吗?不见得。我们对抽象的态度没必要过分崇拜,下面就专门讨论一下抽象和具体之间如何平衡。比如Java 语言,根上的 Object 类最抽象了,但 Object 定义满天飞显然不是我们想要的,例如:

Objectobj =newTiger();

那样你会被迫不停地进行下溯转换:

Animalanimal= (Animal)obj;

所以不是越抽象越好。抽象是有等级之分的,要抽象到什么程度呢?有一句描述美女魔鬼身材的语句是“该瘦的地方瘦,该肥的地方肥”。那么,这句话可改编一下,即可成为抽象编程的原则,即“该实的地方实,该虚的地方虚”。也就是说,抽象和具体之间一定有个平衡点,这个平衡点正是应该时刻存在程序员大脑里的一件东西:用户需求!

你需要做的是精确把握用户需求,提供给用户的是满足用户需求的最根上的那层数据。什么意思呢?我们通过下面这个例子详细阐述。

村里的家家户户都要提供一种动物去参加跑步比赛,于是每家都要实现一个ProvideAnimal函数。你家里今年养了一只老虎,老虎属于猫科。三层继承关系如下:

publicabstractclassAnimal{publicvoidRun();}publicclassCat:Animal{publicintJump();}publicclassTiger:Cat{publicvoidHunt(Animal animal);}

现在有个问题:ProvideAnimal 函数的返回类型定义为什么好呢?Animal、Cat 还是Tiger?这就要看用户需求了。

如果此时是举行跑步比赛,那么只需要你的动物有跑步能力即可,此时返回Animal 类型是最好的:

publicAnimalProvideAnimal(){returnnewTiger();}

如果要举办跳高比赛,是Cat 层级才有的功能,那么返回Cat 类型是最好的:

publicCatProvideAnimal(){returnnewTiger();}

切记,你返回的类型,是客户需求对应的最根上的那个类型节点。这是双赢!

如果函数返回值是最底下的 Tiger 子类型:

publicTigerProvideAnimal(){returnnewTiger();}

这会带来如下两个潜在的问题。

问题1:给别人造成滥用的可能

这给了组织者额外的杂乱信息。本来呢,对于跑步比赛,每一个参赛者只有一个 Run 函数便清晰明了,但在老虎身上,有 Run 的同时,还附带了跳高 Jump 和捕猎 Hunt 的功能。这样组织者需要思考一下到底应该用哪个功能。所以提供太多无用功能,反而给别人造成了困扰。

同时也给了组织者犯错误的机会。万一,他一旦好奇,或者错误操作,比赛时调用了 Hunt方法,那这只老虎就不是去参加跑步比赛,而是追捕别的小动物吃了。

问题2:丧失了解耦子对象的机会

一旦对方在等号两边傻傻地按照你的子类型去定义,例如:

Tiger tiger= ProvideAnimal();

从此组织者就指名道姓地要你家的老虎了。如果比赛当天,你的老虎生病了,你本可以换一头猎豹去参加比赛,但因为别人预定了看你家的老虎,所以非去不可。结果便丧失了宝贵的解耦机会。

如果是Animal 类型,那么你并不知道是哪一种动物会出现,但你知道它一定会动起来,跑成什么样子,你并不知道。这样的交流,是比较高级的交流。绘画艺术上有个高级术语叫“留白”,咱们编程玩“抽象”也算是“留白”。我先保留一些东西,一开始没必要先确定的细节就不先确定了。那这个“留白”留多少呢?根据用户需求而定!

总结

多态这门特技,成就了人们大量采用抽象去沟通,用接口去沟通。而抽象也不负众望地让沟通变得更加简洁、高效;抽象也让相互间依赖更少,架构更灵活。

参数化和工厂模式是消灭或隔离new 的两种武器。

用户需求是决定抽象到何种程度的决定因素。

——本文选自《代码里的世界观:通往架构师之路》


编程中有很多通用的知识点,它们是10年甚至20年都不会淘汰的编程技术,市面上也极少有将它们综合起来并讲得有意思的书。

上面这本书是一位IBM架构师结合了自己13年编程经验,结合自己的理解和领悟,把许多知识点汇入到了这本书里。它们并不是潮流的知识点,而是厚重的基础知识。

点击这里,看图书目录。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容