聊一聊面向对象的三大特性

从最开始接触OOP时,书本和课堂上便一直说着“万物皆对象”,“封装,继承和多态”。我应该也是像大部分初学者一样,被动的接收了这些思想,进而在日后的学习和工作过程中了解了这些特性的实现方式。但在经过这一年多的工作和学习后,我见识到了很多出色的模式实现和项目代码结构,以前的我从未想过代码可以这样写。我开始反思,为什么他们会想到这样写代码?这样一想,我开始怀疑自己对面向对象特性的理解了。所以写这篇文章,长期记录自己对OOP理解的变化。

截至到目前,我可以将自己对封装,继承和多态 总结为以下几个阶段:

第一阶段: 是什么

这是我对OOP三个特性的初步认识,来源与课堂和书本。在这个阶段我对封装的理解是比较粗暴,我觉得封装就是抽象,将事物和处理的过程抽象成一个类,一个类定义好了,封装就完成了。而继承就是子类可以继承父类,然后有了以下的一些特性:

  • 子类拥有父类的部分或全部字段属性和方法属性
  • 子类可以重写父类的方法
  • 子类可以向上转型

至于多态是什么?当时的我觉得,继承中的子类可以向上转型便是多态,一个类可以是自己也可以是它的父类。

现在回过头来看,当时的我理解虽然片面,但是似乎也没有错误,说是一知半解,会比较贴切。对于当时的我,我只恨读书太少,只为了解是什么而去学习,而忽略了更为重要的为什么。

第二阶段: 怎么用

我第二次开始思考面向对象三个特性是在看了《HeadFirst设计模式》这本书之后,这本书讲解了如何使用面向对象特性实现不同的模式,解决不同类型的问题。这是我真正意义上见识到面向对象特性在编程中的强大能力。我开始了解什么是面向接口编程,开始认识到低耦合的代码有着怎样的好处,也认识到了程序设计过程中考虑还未出现的扩展性和后期的可维护性是多么的重要。

在读完这本书的第一章,即策略模式之后。我意识到封装,继承和多态或许不是我想的那么简单。策略模式的实现,通过对调用者暴露一个规范的接口,来告诉调用者该如何使用这些接口的实现。而通过接口不同的实现,我们可以在接口规范之下完成不同的实现。当程序运行时我们可以动态的更改策略,而调用者不会去关心这些实现,他只依赖于接口,接口告诉了它能使用哪些方法,完成了什么功能。

那么对于接口的使用者而言,这是什么呢?这就是多态啊!当我们调用了一个方法之后,这个方法的实现类可能有千千万万种,这个方法的实现也可能不尽相同。这里我对接口有了新的认识,我觉得接口就是一种规范,告诉类的调用者该如何去使用这些实现类。同时也告诉了不同的实现类该去完成什么样的功能。

如下,我定义了一个Pet接口,告诉使用者,所有的Pet都有Bellow方法能够使用,然后这个pet接口有两个不同的实现。

public interface Pet {
  void Bellow();
}
public class Frog implements Pet {
  @Override
  public void Bellow() {
    System.out.println("呱呱!!");
  }
}
public class Dog implements Pet {
  @Override
  public void Bellow() {
    System.out.println("汪汪!!");
  }
}

两种动物的叫法是不同的,对于类的调用者而言,无需关心是哪种动物在叫,只需要它能够叫出来即可。

public class User {
  private Pet pet;

  public User(Pet pet) {
    this.pet = pet;
  }
  
  public void makeBellowing() {
    this.pet.Bellow();
  }
}

这里的叫声可以"呱呱"或者“汪汪”中的任何一个,User不会关心,它只关心接口告诉了它什么。而Dog和Frog也不会关系是谁在用,它只知道接口告诉它需要实现这个功能。接口在这里使得所有的实现类对于调用者而言都是相同的类。但是实际的实现却独立的,各不相同的,这是多态。通过接口实现和继承的方式实现的多态。

之所以说是通过接口实现和继承的方式实现的多态,是因为对于子类而言,我们也可以将其视为其基类,我们可以像调用父类那样使用基类,因为基类可用的方法和字段,子类肯定是有的。那么用接口不就行了吗,为什么还要有继承?代码复用,这是使用继承的主要原因,因为子类之间会用重复的代码,当某个方法代码量比较大,而接口的实现类又比较多的时候,我们就不得不在所有的实现类中复制粘贴这些代码,可以实现功能,但是总会觉得不应该。既然逻辑相同,为什么不只写一个呢?继承便实现了这个功能,当我们将子类中复用的代码放到父类中实现时,当通过子类调用这个方法时会自动地调用父类的实现。同时子类也是对父类的扩展,我们可以在子类中增加子类特有的方法。实际上继承基类很大程度上仅仅是为了代码复用,而且继承会违背迪米特原则,使得子类和父类之间出现强耦合。

当然,值得一提的还有一个抽象类,介于接口和基类之间。抽象类中的方法可以有定义,也可以仅仅是关于一个方法的声明。从目前我了解和见过的代码来看,它主要适用的场景是,某个方法,子类会复用这个方法中大量的代码,但是在某些实现上又有些许差异。这种情况下,就可以将差异的部分单独抽象成一个方法,又不同的子类来实现。这种方式被称之为模板方法模式。举个例子,现在某一健将要参加铁人三项(游泳,自行车和长跑)比赛,对于此次比赛教练制定了三种不同的策略,而这三个策略只有游泳的方式不一样,其他都一样,代码实现如下:

public abstract class AbstractStrategy {
  public void start() {
    swimming();
    System.out.println("开始自行车比赛...");
    System.out.println("开始长跑比赛...");
  }

  abstract void swimming();
}

这个类的子类都可以参加比赛,但是比赛的过程不一样,因为游泳的方式可以各自实现,但是比赛的过程都是一样的,先游泳,再自行车比赛,最后长跑。实现了一个过程中部分代码的复用。

public class BreastStrokeStrategy extends AbstractStrategy {
  @Override
  void swimming() {
    System.out.println("使用蛙泳...");
  }
}
public class ButterflyStrokeStrategy extends AbstractStrategy {
  @Override
  void swimming() {
    System.out.println("使用蝶泳...");
  }
}

以上,我觉得继承和多态密不可分,或者二者本就是同一概念不同角度的描述。通过继承(宽泛概念上的继承,包括实现)来实现多态。

在这个阶段,我对封装的理解就是“内聚+private"。尽量明确一个类的职责,实现一个类该完成的功能,如果外部不需要的字段和方法就通过private限定权限。

第三阶段: 解决了什么问题

在我第二次思考关于OOP的三个特性时,见识到他们的部分使用场景时,才知自己前路漫漫。冰山一角已经让我收获颇多,如果是深耕下去,肯定会大有裨益。所以,最近我还是会时不时的看看这方面的博文,了解大佬们对于这三个特性的理解。读的越多,就越发觉自己眼界之狭隘,目光之短浅。我一直着眼于这三个特性的使用,却一直忽略了其对于程序设计的战略意义。

我们进行程序设计的目的在于编写出一个可扩展易维护的系统。这便是对于系统中每个模块的实现提出了要求,我们希望模块与模块之间具有易交互(有着良好的接口规范),低耦合(单个模块实现对其他模块有着较小的影响),可扩展等特性。对于模块的划分我们且不讨论,我们单单考虑一个类的实现。

一个类的实现免不了需要经历抽象定义。回归到我之前认为比较容易理解的特性封装,封装真的仅仅是"内聚+private"吗?诚然,我无法否认封装是通过private实现的,但是如果你封装的东西在日后的扩展和修改中需要被重新拿出来,这样的封装是否还有意义,是否外部类现在不需要的属性就可以封装起来,或者封装是为了让别人不使用,还是因为别人不使用? 进行封装时,难点在于我们很难考虑到一个类在未来的设计之中是否需要被外部类调用,而外部类的操作是否会对自身的实现产生影响。如果只是为了实现封装而盲目的使用private,很可能导致日后系统扩展或维护的过程中出现尴尬的局面。封装的确是约束和隔离的一个过程,通过接口制定规范,隔离外部调用和内部实现。其实,在很长的一段时间里,我都用着面向对象的编程语言写着面向开发者的应用程序。当我写一个类时,我知道有哪些字段和方法我该调用,哪些不该,我不会在其他的类里去使用我不该使用的东西。这个过程是通过我的主观判断实现的。那所谓的封装与这样开发的我和这样开发出来的程序便是没有任何关系了。封装和隔离应该体现在类与类之间,当我们设计编写一个类时,就应该站在这个类的角度考虑,即使它依赖的类也是我们实现的。设计时我们需要宏观考虑,实现时我们要从类的角度出发。

至于继承和多态,二者其实实现了归一化(基于接口或基类实现的类,那对与其使用者而言,他们都是一样的)。通过归一化,接口(宽泛理解,包括父类)的使用者就可以兼容所有的实现类。这样就可以用同一种方式使用所有的实现类,这极大简化了调用者的使用难度。

咦,不对啊!这跟之前的认识有什么不一样吗?

的确就是这样,这个地方我们用回归到之前思考的东西,接口是用来干嘛的?接口就是为了实现归一化,就是规范和约束。

这是认知的问题。从三个特性的角度出发我们很容易理解它们的实现,我觉得我各个阶段的理解都是没有毛病的。只是从软件设计过程来看,这三个特性占据着非常重要的地位,通过不同的使用方式我们可以更好的工程化我们的项目。

其实没必要一直拘泥于面向对象这个概念,而忽略是实现的基础。很多时候我们的代码里充斥了各种设计模式和多态,这样的代码并不见得是好代码。模式也是长期经验总结出来的解决方案,继承和多态也只是模式实现的一种方式。而且模式也意味着相同的功能,可能需要更多的小类,这不见得是好事。一些问题本身就可以通过编程语言本身的特性完成简单的实现,一味地追求模式化,很容易导致我们忽略其实现的基础。

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