面向对象概述

什么面向对象编程

面向对象编程(OOP,Object Oriented Programming),是一种编程范式或编程风格。以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。

面向对象编程语言(OOPL,Object Oriented Programming Language),是支持类或对象,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。

一般通过面向对象编程语言来进行面向对象编程,但不用面向对象编程语言,同样可以进行面向对象编程。反过来,使用面向对象编程语言,写出来的代码不一定是面向对象编程风格的,也有可能是面向过程编程风格的。

四大特性

封装、抽象、继承、多态是面向对象的四大特性,也有另外一种说法,即只包含封装、继承和多态的三大特性,不包含抽象。

封装(Encapsulation)

封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。

封装这个特性,需要编程语言本身提供一定的语法机制(访问权限控制)来支持,如 Java 中 private、public 等关键字。

如果没有封装,代码会过于灵活和不可控,属性能以各种方式被修改,散落在各处,影响可读性和可维护性。另外,通过有限的方法暴露必要的操作,也能提高类的易用性,给调用者减负,无需了解细节。

抽象(Abstraction)

抽象即隐藏方法具体实现,让调用者只需关心方法提供的功能,而不需了解功能呢的实现细节。

在面向对象编程中,通常借助编程语言提供的接口类(如 Java 中的 interface 关键字)或者抽象类(如 Java 中的 abstract 关键字)这两种语法机制,来实现抽象这一特性。

实际上,抽象这个特性非常容易实现,并非只能依靠接口类或者抽象类这些特殊语法机制来支持。具体实现逻辑被函数包裹,本身就是一种抽象。

抽象是一个非常通用的设计思想,不仅可以用在 OOP 中,也可以用来指导架构设计等。同时抽象又不需要编程语言提供特殊的语法机制来支持,没有很强的特异性,所以有时不被看作 OOP 的特性之一。

抽象一方面能够提高代码的可扩展性、维护性,修改实现不需要改变定义,减少代码的改动范围;另一方面,也是处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息。

在定义(命名)类的方法时,也要有抽象思维,不要在方法定义中暴露太多的实现细节。

继承(Inheritance)

继承用来表示类之间的 is-a 关系,从关系上可以分为单继承和多继承两种模式。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类(有菱形继承的副作用)。

继承这个特性,需要编程语言提供特殊的语法机制来支持,如 Java 中的 extends 关键字。

继承最大的一个好处是代码复用(组合也可以解决复用问题),将不同类的相同属性和方法,抽取到父类中,让子类继承父类。另外通过继承表示 is-a 关系,非常符合人类的认知。

过度使用继承,继承层次过深过复杂,会导致代码可读性、可维护性变差。很多人觉得继承是一种反模式。

多态(Polymorphism)

多态,即在实际的代码运行过程中,子类可以替换父类,调用子类的方法实现。

多态需要编程语言提供特殊的语法机制来实现。编程语言需要支持父类对象可以引用子类对象,支持继承,支持子类可以重写(override)父类中的方法。

除利用继承加方法重写这种实现方式外,还有其他两种比较常见的的实现方式,一是利用接口类语法,二是利用 duck-typing 语法。

多态特性能提高代码的可扩展性和复用性。

接口和抽象类

面向对象编程中,抽象类和接口是两个经常被用到的语法概念,是面向对象四大特性,以及很多设计模式、设计思想、设计原则编程实现的基础。

语法特性

接口不能包含属性,只能声明方法,方法不包含代码实现。类实现接口的时候,必须实现接口中声明的所有方法。

抽象类不允许被实例化,只能被继承,可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫作抽象方法。子类继承抽象类,必须实现抽象类中的所有抽象方法。

作用

接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。

抽象类是对成员变量和方法的抽象,是一种 is-a 关系,解决代码复用问题。

应用场景

如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,就使用接口。

如果要表示一种 is-a 的关系,并且是为了解决代码复用问题,就使用抽象类。

基于接口而非实现编程

基于接口而非实现编程,是一条比较抽象、泛化的设计思想,能非常有效地提高代码质量。另一个表述方式是基于抽象而非实现编程。

越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。

定义接口时,命名要足够通用,不能包含跟具体实现相关的字眼,仅与特定实现有关的方法也不要定义在接口中。

越不稳定越需要接口。而如果在实际业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那就没有必要为其设计接口,可以直接使用实现类。

继承还是组合

在面向对象编程中,通常说组合优于继承。

继承主要是解决复用问题,而最大的问题是,继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。

通过组合、接口、委托三个技术手段,完全可以替换掉继承,不用或者少用继承关系,特别是一些复杂的继承关系。

继承并非一无是处,继承改写成组合要做更细粒度的类的拆分,增加代码的复杂程度和维护成本。

如果类之间的继承结构稳定、层次浅、关系简单,就可以使用继承。还有一些特殊的场景要求必须使用继承,如不能改变函数入参类型,还想使用多态。反之就尽量使用组合来替代继承。

面向对象与面向过程

除面向对象外,还有面向过程编程和函数式编程两种编程范式。面向过程随着面向对象的出现,已经慢慢退出舞台,而函数式编程目前还没有被广泛接受。

什么是面向过程编程

面向过程编程是一种编程范式或编程风格,以过程(方法、函数、操作)作为组织代码的基本单元,主要特点为数据(成员变量、属性)与方法相分离,通过拼接一组顺序执行的方法来操作数据完成一项功能。

面向过程编程语言最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。

面向对象编程的优势

  1. 对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。OOP 更能应对这种复杂类型的程序。
  2. 面向对象具有封装、抽象、继承、多态四大特性,利用这些特性可以写出更加易复用、易扩展、易维护的代码。
  3. OOP 语言更加人性化、更加高级、更加智能。

看似而不是面向对象的代码

  1. 滥用 getter、setter 方法,破坏封装。
  2. 滥用全局变量和全局方法,方法与数据分离。Constants 类和 Utils 类最常用到。
  3. 定义数据和方法分离的类,如基于 MVC 三层结构做 Web 方面的后端开发(贫血模型)。

为什么容易写出面向过程的代码

  1. 符合人的流程化思维方式。
  2. 相比面向对象要更加容易。

是否要杜绝面向过程

面向过程编程是面向对象编程的基础,面向对象编程离不开基础的面向过程编程。

不管使用哪种风格来写代码,最终目的是写出易维护、易读、易复用、易扩展的高质量代码。只要能避免面向过程编程风格的一些弊端,控制好副作用,就不用避讳在面向对象编程中写面向过程风格的代码。

业务开发模式

很多业务系统基于 MVC 三层架构来开发,其中 M 表示 Model,V 表示 View,C 表示 Controller,项目分为展示层、逻辑层和数据层。

Web 或 App 项目很多都是前后端分离,其中后端负责提供接口给前端调用,又分为 Repository 层、Service 层和 Controller 层。Repository 层负责数据访问,Service 层负责业务逻辑,Controller 层负责暴露接口。

基于贫血模型的传统开发模式

传统开发模式中,XxxEntity 和 XxxRepository 组成数据访问层,XxxBo 和 XxxService 组成业务逻辑层,XxxVo 和 XxxController 组成接口层。

Bo 是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在 Service 中。

这种只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。

贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。

基于充血模型的 DDD 开发模式

充血模型(Rich Domain Model)与贫血模型相反,数据和对应的业务逻辑被封装到同一个类中,满足面向对象的封装特性,是典型的面向对象编程风格。

领域驱动设计,即 DDD,主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。DDD 在 2004 年被提出,随微服务兴起而被熟知。

基于充血模型的 DDD 开发模式,跟基于贫血模型的传统开发模式之间的差别,主要在 Service 层。

DDD 开发模式中,Service 层由 Service 类和 Domain 类组成。Domain 就相当于贫血模型中的 Bo,区别在于既包含数据,也包含业务逻辑。而 Service 类变得非常单薄,仅负责与 Repository 交互、业务聚合和一些非功能性工作(如幂等、事务、日志等)。

传统开发模式受欢迎的原因

第一,大多系统业务比较简单,不需要精心设计充血模型,贫血模型足以应付。即便使用充血模型,模型本身包含的业务逻辑也不多,领域模型也比较单薄,跟贫血模型差别不大。

第二,充血模型的设计要比贫血模型更有难度。

第三,思维固化,有学习和转型的成本。

何时该使用 DDD 开发模式

基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。

除代码层面的区别外,两种开发模式有不同的开发流程。DDD 开发模式下,需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。需求的开发基于定义好的领域模型,在应对复杂业务系统开发时更有优势。

面向对象分析和设计

跟 OOP 经常联系在一起的还有面向对象分析(OOA)和面向对象设计(OOD)。分析、设计、编程(实现),正是面向对象软件开发要经历的三个阶段。

面向对象分析

业务需求一般比较明确,而针对框架、类库、组件等非业务系统的开发,其中一个难点是需求一般比较抽象和模糊,需要自己去挖掘、权衡、假设,把抽象的问题具象化,最终产生清晰的、可落地的需求定义。

需求分析的过程实际上是一个不断迭代优化的过程。不要试图一次给出一个完美的解决方案,而是先给出一个基础的方案,然后再慢慢优化,这样的思考过程更容易摆脱无从下手的窘境。

面向对象设计

OOA 的产出是详细具体的需求描述,而 OOD 的产出是类,主要包含以下几个部分。

一、划分职责、识别出有哪些类。

可以根据需求描述,把功能点罗列出来,然后看哪些功能点职责相近,操作同样的属性,可否应该归为同一个类。如果需求非常复杂,功能点太多,先划分模块,再在模块内部划分和识别类。

二、定义类及其方法和属性。

识别出需求描述中的动词和名次,分别作为候选的方法和属性,再进行过滤筛选。

三、定义类与类之间的交互关系。

四、将类组装起来并提供执行入口。


本篇文章是在学习极客时间 - 设计模式之美课程后的总结。可以扫描文章尾部的二维码,到专栏进行系统学习。

设计模式之美

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • © 2016-2020 姜越

谢谢老板

支付宝
微信