我们为什么需要面向对象编程?

DinS          Written on 2017/10/30

一、概述

面向对象程序设计(object-oriented programming, OOP)是一门博大精深的学问,本专题仅仅浅尝辄止,简略介绍OOP的各个方面,侧重于思维。实际上本人认为OOP的重点并不在代码,OOP是一种认识世界的方式。

由于主题宏大,所以需要先给出road map。

首先探讨为什么需要OOP,换言之OOP出现的原因是什么?这个问题很有意思,只有理解了OOP存在的意义,才能真正使用OOP。这是本文要解决的事。
其次探讨OOP的三大支柱,在这里摘录C++ Primer里的一段话:

“面向对象程序设计的核心思想是数据抽象、继承和动态绑定。通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。”

——《C++ Primer中文版:第5版》,北京:电子工业出版社,第526页。

自然的,接下来将从数据抽象、继承和动态绑定三方面介绍。对于OOP而言,数据抽象是血肉、继承是骨架、动态绑定是灵魂。这是后面几篇文章要探讨的,阅读后读者应该就掌握了OOP的语法层面的内容。

最后探讨设计模式和系统分析建模,这部分是OOP强大的源泉,即思维方式。一个好的OOP程序必定是从一个好的思维中产生的。但是这部分就是仁者见仁智者见智了,无定论。

二、OOP兴起的原因

面向对象并不是从计算机诞生之初就有的,而是经过了一段时间的积累后才被提出的。实际上OOP是无数人头疼的结果。为什么头疼?因为面向过程编程(function-oriented programming, FOP)。
FOP是初学程序的人自然而然使用的方法,因为这个方法最接近计算机的工作原理,在处理简单问题时,FOP明显有优势,然而随着软件的发展,需求越来越复杂,代码越来越庞大,FOP的弊端开始凸显。到了一定的量后,FOP的程序根本就没法维护了,无数程序员为此头疼,最后才提出了OOP。

理解OOP的最好方法是看看FOP为什么失败。
在这里提出一个水果商人(fruit vendor)的案例,这个案例将贯穿本专题。

假设你是一个水果商人,你看到了互联网的发展给零售业带来的冲击和商机,于是你准备自己办一个网站,线上卖水果。
由于你自己懂编程,所以你想自己写一个后台,把web端交给另一个人去做。这样即节省成本又有乐趣。于是你们两个约定好,web端给用户提供各种选择,其结果用文本文件的方式传给后台,后台根据文件计算水果价格收据,再返回给web端。
这个故事发生在90年代,所以不需要考虑其他复杂的因素,把程序做出来就可以了。

先来试试用FOP如何做这个程序。
第一步,先把读写文件的格式约定好。

每个订单一个文件,每一行是一种水果,依次是水果种类、单价、重量。

返回每种水果的价格和总价。

第二步,简单设计一下程序执行步骤。
主题程序不断循环,每循环一次读入订单,计算,然后写文件。
读文件用一个函数,写文件用一个函数,计算用一个函数。
每一种水果的信息用一个结构体Item表示,整个订单可以用vector表示。
思路很清晰,让我们来实现吧!

三、草创阶段的程序

程序可以满足需求,这里就不演示了。
这个在线水果商店经过一段时间的运行后,效果一般。
经过了仔细的考察,你发现价格与顾客数量密切相关,为了进一步拓展市场,你决定采用打折战略来吸引更多客户。

四、带有打折功能的程序

主体的打折策略是根据购买数量阶梯变化折扣力度。但是具体的数值与购买的水果种类有关,这是商家引导客户的常用手法。
比如说,买苹果的话10斤以上20斤以下打9折,20-27斤打8折,27斤以上打7.5折;买葡萄的话5-10斤打9.5折,10-18斤打8折,18斤以上打7折;买桃的话统一打9.5折。

另外有一个折扣券商家想与你建立合作关系,他们向你支付一定费用,然后可以给客户发放折扣券,客户可以在这个网站上使用折扣券。如果使用了折扣券则按购买数量打折的策略不再适用。
他们的运营方式不向你透露,你也并不关心。当然为了保证程序运行,他们会提供一个第三方接口ApplyCoupon,传入一个字符串,返回折扣力度,是一个小数,如果返回-1表示无效。

这种需求的增加在一个程序的生命周期内很常见,而且这里提出的内容也不复杂,让我们修改之前的程序吧!

先来梳理一下变化。
现在有两条策略,如果有折扣券,则调用第三方接口;如果没有,则按重量打折。
折扣券信息通过订单文件传入,具体是在最后增加一个字段。
比如这样:

第三方接口用.h和lib提供,不知道实现,按照约定调用即可。

为了使用折扣券,Item结构需要修改:

读订单也要增加一项:

重点是计算价格的函数:

接下来确定重量折扣,单独一个函数:

具体的打折策略。当然这里为了简单直接写的数,真正的程序中应该使用其他的方式获取数值,比如配置文件。
有了打折函数就可以完成价格计算函数了:

至此折扣策略完成,运行结果:

这个结果是正确的,手算确认过。

是时候回顾一下了。
为了适应新的要求,我们修改了Item的内容,读文件的内容,增加了一个函数,修改了原来的计算函数。注意,所有的修改都是直接在原来的代码基础上修改的。记住这一点,非常重要。

五、商业模式与折扣策略变化

经过一段时间的运营,你的网站逐步积累了人气,有些水果商人想加盟这个网站,这涉及到商业模式的变化。
另外之前的折扣券提供方想要取消合作,你找到了另一家,但是具体的函数不同,这是肯定的,函数名、输入参数和返回参数都有变化。
之前的打折策略也发生了变化,由于许多水果是不同加盟商提供的,具体的折扣方法由他们确定。即使是同一种水果,因为加盟方不同折扣方式也会不同。

可以看出这次的需求变化要更加复杂。
为了加入供应商的信息,Item结构体需要添加新字段。
所有涉及到原来折扣券接口的部分都需要改动,以适应新的供应方。
折扣政策也需要变化,要根据水果和供应商来确定策略。
具体的实现就不在FOP里实现了。

需求还可以变得更加复杂,比如增加水果种类和亚种类,并根据某些种类组合打折等等,所有这些都是想说明一个问题:FOP为何失败?

六、FOP为何失败?

麻雀虽小五脏俱全,从这个简单的水果程序可以看到FOP固有的脆弱性:FOP是紧耦合的,需求变化必然修改原来的代码。
这个特性在代码不复杂时看不出有什么问题,毕竟需求变化了,代码肯定是要改的。但是随着计算机行业的发展,需求变得越来越复杂,代码量指数级增长,FOP就扛不住了,为什么扛不住?看这张卡:

一旦动了原来的代码,就会连带着影响许多其他的代码,而这些代码又会影响更多代码,也就是说,局部代码的变化会扩散到整个程序。
试想一个百万行代码的程序,如果改动一处波及了10%的代码,就够整个公司的程序员来几壶了。
使用FOP做出来的程序,随着程序的发展维护代价会越来越高,到达一定阶段后新的程序员会无从下手,老程序员一走整个程序就死了。

光是这种推论可能还不足以说服某些人,最好的方式是自己维护一个庞大的FOP程序,自己感觉到头疼了,就会认真研究OOP了。

实际上OOP就是为了解决FOP的弊端而诞生的。其主要目的是应对复杂变化,效率不是OOP考虑的主要问题。而且良好扩展能力必然要付出一定代价,“天下没有免费的午餐”。代价一方面是效率,但更主要的是设计程序架构。
OOP的难点在于设计,而不是实现,在最后我们还会再讨论这一点,现在让我们走进OOP的大门,先来看看OOP的几大支柱概念,见《数据抽象》、《继承》、《动态绑定/多态》。