自学编程,从此开始

上第2学堂,听有趣的编程课

课文: 《你好,对象!封装版(上)》 (点击查看完整内容:视频+评测+讨论+……)

作者:null

沙之为沙,人之为人,车之为车,背后是什么在支撑?

课文题图

 

你好,对象! 封装版(上) ——基本概念

“封”什么?“装”什么?

关于封装,需要关心的第一个问题是:封装什么?

车场上一辆宝马车披着车罩。漂亮的封装是那个车罩吗?不,那辆宝马才是。

“封装”封的是什么?装的又是什么?丁小明回答:“装进进去还要封起来,所以肯定是:见不得人的、隐私的、需要保护的……”

呀!听起来非常有道理!如果将“见不得人”称为“丑陋”的,那么……“老师,我懂了。”丁小明继续回答:

封装丑陋的:宝马车再漂亮,但内部油箱、发动机、变速箱还是丑陋的,没法直接见人;

“老师,我有补充!”二牛回答:

封装隐私的:车其实是一个移动的私人空间,在车里可做的很多(老师你懂的),所以车被设计成一个关起来门来就相对隐闭的空间;

“我也有补充!”,春丽回答:

封装待保护的:车里的人需要被保护嘛,所以有安全气囊等保护模块。

然而,大家都错了!这些正好是多数人学习面向对象的“封装”特性时,最常见的一些似是而非的理解。

曾经我们问过“什么是程序”?《启蒙》篇回答:“计算机程序是一组指令(及指令参数)的组合,这组指令依据既定的逻辑控制计算机的运行”。这个问题的另一个经典回答是:“程序 = 算法 + 数据结构”。新回答更接近人类的思维,启蒙中的回答理强调机器的特性。无论哪一种说法,“程序”总是脱离不了“动作”和“数据”。

对封装的解释,有一种说法就是:“封装是通过类型定义,将数据和动作进行组合”。然而,这种教科书式的定义,通常都无助于大家真正的理解。

假设我用写课程所得的工资,买得一辆BMW,才开半天,就发现以下小毛病:

这完全不是像是德系名车的封装啊!我认真的看了看,才发现字母B写得有点开,果然是不知哪来的“13MW”名车。

在公众的概念里,车就是踩油门要加速踩刹车要减速转方向盘要拐弯按喇叭要叫。做到这几点,就算轮子用驴子代替,方向盘是鞭子,油料是草,排的是粪,那也可以称为车,马车嘛。

踩油门和加速、踩刹车和减速、转方向盘和拐弯、按喇叭和鸣笛,这些叫“关系”。严格点,叫事物的内部逻辑关系。

《白话C++》对“封装 / encapsulation”的解释由此而来:组合“数据”和“动作”不是目的而是手段。真正需要封装不是数据也不是动作,而是关系。数据和数据间的关系、数据和动作的关系、动作和动作的关系。

豪车、普通车、特斯拉、柴油车,世界有很多不同汽车,车里有也许多不同的配件,但只要“车”的抽象概念还没有发生革命性的变化,那么在“车”这个概念所封装的逻辑关系就基本一致。不管什么厂商,在什么流水线,用什么组件,最终封装出来东西(对象)满足以上那些关系(踩油门加速、踩刹车减速、转方向盘拐弯、按喇叭鸣笛),不管外形有多奇怪,都可以称之“车”。

“老师,象棋上那个车,好像没有这些关系?”。

“小明,出去!”

“沙之为沙,人之为人”——不变式

“沙之为沙,人之为人”,一个事物之所以属于某一类事物,那么就得满足某些特有的内部或外部关系。这就叫这类事物的“不变式”。比如我们认为人应该有良心,应该重信用,应该孝敬父母,否则社会就会批评“你还是人(类)吗?”。

先不谈复杂的车,谈一个数学上的小知识:如何使用C++表达二维坐标上的一个点?答案简单:不就是一个x一个y吗?

struct Point
{
      int x, y;
};

我们的问题是:这体现了二维坐标点这类事物的什么“不变式”?看似没有,但其实存在:

再接下来,如何表达二维坐标上45度角斜线上的点呢?也很简单:x和y值相等的点。但是,如何表达一个位于45度角斜线上的点?

许多没有学过编程的人的直觉反应是:反正是“位于45度角斜线上的点,也是点”,所以,可以继续使用Point结构定义。这就立马要出错了。比如,使用Point结构,我们要如何维护 “x和y值相等”这个关系呢?犹豫再三我决定将压箱的多年秘籍“分享”出来以“毒害”各位:

不要低估“秘籍”的流行性。那天我认真阅读“13MW”牌汽车的说明书,就有这么类似一段:

“本款(结构或类型)的车,踩刹车不一定就减速。因此强烈建议驾驶用户踩刹过程中及时打开车门(注1)伸一只脚拖行于地面。”

赶紧找到“注1”处,果不其然写着:

“行驶过程中如需打开车门,建议长按喇叭,车门将智能打开以供驾驶员伸脚刹车或潇洒跳车。”

团队编程约定和代码注释很重要,但只有“管”和“求”并不保险。程序员更应该做好封装工作,将x和y值恒等的关系,封装成类的不变式。以下是做法之一:

假设 “45度斜线上的二维坐标点”的结构名为 “PointOnAngle45”, 那么这个结构该如何定义呢? 有人又想了,既然x和y一定相等,那干脆就只留下一个成员数据吧:

struct PointOnAngle45
{
    int x;    
};

首先,这下子,所谓的 “45度线上的点” 这个类型的点也非常的不像一个点:居然只有一个坐标轴?更重要的是,哪怕你坚持“做自己的设计,让别人说去吧”,但你内心也得承认:只有x的点,并不一定是45度线上的点,90度的垂直线,也是这么表达啊。因为只提供X坐标的位置,并没有表达出Y一定和X同值这个内部关系,也可以理解为是在表达Y值可以是任意值的意思。而所谓“X和Y同值”的这内部关系,就是对“45度线上的点”的约束,就是“45度线上的点”的不变式

我们不在这节课给出PointAngle45的结构该如何写,因为它需要下一节课,也就是“你好,对象!封装版(下)”的课程知识。

类型即封装

其实,当我们通过struct来定义一个新的数据类型时,就是一种“封装”。

struct Person

{

    string name;

};

想像我们还想关注“人类”的年龄信息,那么我们可以Person类再添加一个成员数据。

struct Person
{
    string name;

    int age;
};

看,和“人”有关的数据,被我们统一包装在Person这个类型信息中。假设我们要在程序中管理到2个人,那么使用Person这个类型,代码如下:

Person xiaA;
Person zhiLing;

2个人,对应定义两个对象,直观,轻松。如果不使用自定义类型来封装人的信息,代码该如何写呢?

string nameOfXiaA;
string nameOfZhiLing;
int ageOfXiaoA;
int ageOfZhiLing;

是不是有一种所有数据被“拆散”得“支离破碎”的感觉?如果有,恭喜你,说明你具备程序员的气质。

如果没有,也恭喜你,说明你拥有机器一般的超强能力的大脑。 不过,当前Person只有两个属性,如果是10个属性20个人呢?人脑一定会记不过来。所以,将属性组合在一起形成一个新的复合类型,就是封装。现实生活中,无论是牙膏还是手机或者一只麻雀,都是结构化的数据,而非离散的数据——你把一只麻雀大卸八块它就不再被人们认识为“一只麻雀”。人类认识一个“Object/物体/对象/东西”的天生习惯,是将它当作一个完整的“结构/struct”对待,而不是离散的一堆的数据。

有了结构之后,还得区分内外——这正是下一节的第一个知识点。