首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

浅谈面向对象编程

微信限制:不能放置链接、代码;发布后不能更新。。。

Code is by people and for people. —— Kent Beck

1 写在前面

1.1 很多人不关心代码可读性

对于很多人,包括我,最开始写代码的时候,对代码的认识就是:写一些特殊的符号,让机器去执行,从而达到效果。这时候,我们关心的是:机器如何执行我们写的代码。

例如,我曾经会设计这样的函数 —— 在一个循环内完成两件事:一边实现某个操作,一边统计结果并返回。

代码1- 一个循环内完成两件事

随着代码写的越来越多,我们发现:一般读代码的时间比写代码的时间要长。所以,我们在写代码的时候开始关心:如何通过代码来传达我们的想法

对于上边的例子,把查询和修改分开 (Separate Query from Modifier),变为 和 ,更有利于传达我们的想法 —— 因为一般有返回值的方法只作为查询,没有副作用;没有返回值的方法作为修改,才会产生副作用。(见笔记)

1.2 很多人用着面向对象语言,却只写着命令式程序

此外,很多人,包括我,最开始学习的编程语言是命令式(面向过程)语言,在使用面向对象语言编写程序的时候,仍然只用着最简单的命令式控制流 —— 把所有的逻辑写成一个超长的函数,而且只用了条件、循环实现相应功能。

而面向对象语言引入了类和对象,把对外表现的行为(behavior)和用于支持行为的状态(state)封装在一起,实现功能的模块化。(见笔记)

同时,面向对象语言为我们提供了丰富的控制流描述方法,更符合人们直观的想法 —— 循序(sequence),消息(messages),迭代(iteration),条件(conditionals),异常(exceptions)。(见笔记)

1.3 为什么要写这篇文章

2018 年,读了 Martin Fowler 的《重构》、Kent Beck 的《实现模式》,深有感触

分享给大家,并收集反馈

1.4 从一个例子开始

正如 Martin Fowler 所说,抽象的原理不如具体的例子更能说明问题。(见笔记)

假设我们在给一个桌面宠物游戏加入一些功能:

支持两个玩家参与游戏

每个玩家可以养多只宠物(猫 / 狗)

每个玩家可以给自己或对方的宠物喂食、洗澡

需要分别统计猫和狗的数量

根据直观的命令式编程的想法(虽然使用了 C++ 的 STL),我们可以:

定义一组变量,用于存放每个玩家的宠物、猫/狗粮和洗发水

定义一组函数,用于统计猫/狗数量、给宠物喂食、给宠物洗澡

代码2- 命令式编程风格

本文将围绕这个例子,从封装、继承、多态,介绍如何使用面向对象的方法实现上述功能。

2 封装 Encapsulation

面向对象语言引入了类和对象,可以把一组密切相关的逻辑(logic)和对应的数据(data)放在一起,从而实现模块化。(见笔记)

放在同一个类里的逻辑和数据,往往解决了同一领域的问题,变化频率一致—— 理想情况下,当这个领域问题变化时,只需要修改对应的这个类,就可以实现所有的更改。(见笔记)

封装是面向对象编程的核心,有两个重要的优势(见笔记):

对象只需要更少“了解”系统的其他部分,减少对象以外的依赖

当领域问题变化时,只有少数几个对象需要修改,就可以实现这个变化

2.1 例子:状态/数据的封装

代码2中,为了表示 3 类不同的状态,使用 和 构成了 2 组平行的、独立的数据。很容易发现,每一组数据都可以捆绑在一起,组成 ;利用捆绑后的结构,可以很方便的定义多组平行的、结构相同的数据 , 。从而实现对状态/数据的封装。

代码3- 封装状态/数据

2.2 例子:行为/逻辑的封装

进一步的,代码2中的 4 个函数是对 数据进行操作的逻辑,解决了同一领域的问题、变化频率一致。所以,在代码3的基础上,把这些逻辑和数据封装在一起,构成了完整的类 。从而实现对行为/逻辑的封装。

代码4- 封装行为/逻辑

3 继承 Inheritance

在面向对象设计中,继承常用于对领域模型进行分类(例如 / 都属于 )。而在面向对象编程中,继承提供了实现共享逻辑的最简单方法(子类可以使用父类的 字段/方法)。

但是,Alan Snyder 在Encapsulation and inheritance in object-oriented languages一文中,提出了继承破坏封装(inheritance breaks encapsulation)的观点 —— 子类使用了父类的 字段/方法,父类的被依赖部分一旦发生改变,子类也被迫需要修改。

所以,人们常说应该面向接口编程,而不是面向实现(code to interfaces, not implementations)(见笔记)—— 子类尽可能避免过度依赖于父类的实现,尤其是两者变化不可控的时候。(在 Java/C# 语言中,专门引入了 的概念)

3.1 例子:继承实现

为了避免重复,我们可以把两个类的共享逻辑抽出到一个父类里,然后这两个类继承于该父类。例如,猫和狗吃东西的逻辑有共同之处 —— 吃了食物后,需要一段时间消化食物,然后饥饿值下降。消化食物的逻辑 、饥饿值的状态 是猫和狗共有的,可以提到父类 里,从而避免分别在 和 里重复代码。

代码5- 继承实现

但是,代码5中的 一旦有修改, 和 可能会被动的受到这个修改的困扰(例如 要求传入参数, 和 需要修改为传入参数的形式)。

4 多态 Polymorphism

多态,很难顾名思义,是指通过重写函数(overriding)/实现接口(implementing)的方式,让同一个消息(messages)实现不同的逻辑。

使用消息控制流,是面向对象编程的一个重要特性:

两个对象之间用消息调用,能更丰富的表达我们的想法

基于多态,消息对未来不可预见的扩展是开放的 —— 改变消息的接收者,不需要改变消息的发送者 ——发送者只关心消息的意图,而接收者才需要关心消息的实现(见笔记)

利用多态,我们可以更灵活的设计逻辑策略的切换(见笔记):

命令式编程中,我们只能通过条件(conditionals)实现逻辑的切换

基于多态,我们可以通过派生(subclasses)和委托(delegation)实现

派生一般通过继承实现;而委托一般通过组合实现

派生切换的逻辑,在对象生命周期内不能再次改变;而委托可以多次修改

4.1 例子:条件逻辑切换

很多人,包括我,喜欢使用命令式语言里的测试条件(testing conditionals)实现逻辑策略切换。例如,在实现喂食功能 ,而对于不同的动物有着不同的喂食逻辑时,我们会使用 语句先判断 的类型,然后针对不同类型进行处理。

代码6- 条件逻辑切换

这段代码并不是一个良好的设计:

在函数中, 作为消息的发送者, 是消息的接收者 —— 操作 的对象实现喂食逻辑

破坏了封装性

消息的发送者不仅需要关心消息的意图,还需要关心消息处理逻辑的实现

一个类过度访问另一个类 的数据/实现,在 里实现了应该在 里实现的功能,类的职责划分不恰当,是重构的一个信号(见笔记)

不易于扩展

当我们需要引入一个新的宠物类型(例如,兔子)的时候,就需要修改消息的发送者的 实现,即加入 分支

4.2 例子:派生逻辑切换

利用面向对象的方法,我们可以实现基于多态的派生逻辑切换。例如,同样是实现喂食功能 ,我们只需要给 定义一个统一的接口 ,接收 发送的消息;对于不同动物的不同逻辑,我们可以通过重写函数(overriding)/实现接口(implementing)实现。

代码7- 派生逻辑切换

相对于代码6,这个设计有着极大的优势:

在函数中, 作为消息的发送者, 是消息的接收者 —— 操作 的对象实现喂食逻辑

有良好的封装性

消息的发送者只需要关心消息(简单的 接口)的意图,不需要关心处理逻辑的实现

消息的接收者只负责处理消息,和发送者没有过多的耦合,尽职尽责

易于扩展

当我们需要引入一个新的宠物类型(例如,兔子)的时候,只需要在 类里实现 接口即可;消息发送者代码不需要修改

4.3 例子:委托逻辑切换

类似于代码7,把 委托到 函数:

对于 的消息调用是透明的,和派生相比,仅仅是实现上的不同

相对于派生实现的优势在于,可以在 对象生命周期内切换逻辑—— 将变量 赋值为 对象时,可以实现猫的逻辑;赋值为 对象时,就可以动态切换为狗的逻辑(虽然在这个业务场景下没有实际意义,猫生出来不会变成狗。。。)

代码8- 委托逻辑切换

写在最后

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180325G1A76200?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券