理论一:什么是面向对象?
面向对象编程和面向对象编程语言的区别?
面向对象是一种编程风格,面向对象编程语言是支持类或者对象的语法机制,使用面向对象语言能够很方便的实现面向对象编程。
核心概念: 类(class)和对象(object) 四大特性:封装、抽象、继承、多态
UML(Unified Model Language),统一建模语言。
Algorithms4/umlcheatsheet.jpg at master · gdhucoder/Algorithms4 (github.com)
理论二:封装、抽象、继承、多态分别可以解决哪些编程问题?
封装(Encapsulation)
使用钱包的例子,id和createTime在初始化的时候生成,同时只提供增加和修改余额的方法。
封装的意义:可控、通过有限的方法暴露必要的属性,一方面可以控制权限和保护数据一致性,另一方面也更加易于使用。
抽象(Abstraction)
过滤非必要信息:抽象只关注功能点不关注实现过程
很多设计原则都体现了抽象的思想,比如开闭原则、基于接口而非实现编程
定义接口名称的时候要有抽象思维,不要在方法定义中暴露太多的实现细节
继承(inheritance)
符合人类的认知,但是过多的继承会导致代码可读性、可维护性变差。所以很多人觉得继承是一种反模式。比如”多用组合少用继承“这一设计思想。
多态(polymorphism)
多态的定义:子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。
多态的前提是编程语言提供的特殊语法机制——继承加方法重写:
- 父类对象可以引用子类对象,DynamicArray dynamicArray = new SortedDynamicArray();
- 支持继承
- 支持子类重写父类的方法
除了这种方式外,还有两种常见的方式 —— 一是利用接口类语法,二是duck-typing语法。
**多态能解决的问题:**提高代码的可扩展性和复用性。
理论三:面向对象和面向过程
发展流程:面向过程 -》 面向对象 -》 函数式编程
其中粗略的来讲,面向过程已经几乎过时,面向对象是当前主流的设计思想,而函数式编程目前还没有被广泛接受。
在实际工作中,很多人只是在用面向对象语言实现面向过程的编码罢了。
抛出问题:
(1)什么是面向过程编程与面向过程编程语言?
(2)面向对象编程相比面向过程编程有哪些优势?
(3)为什么说面向对象编程语言比面向过程编程语言更高级?
(4)有哪些看似是面向对象实际是面向过程风格的代码?
(5)在面向对象编程中,为什么容易写出面向过程风格的代码?
(6)面向过程编程和面向过程编程语言就真的无用武之地了吗?
(1)定义:面向过程编程也是一种编程范式,以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为主要的特点。
(2)优势:
- 大规模复杂程序的开发(对于简单编程来说,面向过程反倒更有优势;对于大规模开发流程来说,面向对象编程提供了一种更加清晰的、更加模块化的代码组织方式)
- OOP风格的代码更易复用、易扩展、易维护 —— 也就是四大特性:封装、抽象、继承、多态
- OOP语言更加人性化、更加高级、更加智能
在文章中我讲到,面向对象编程比面向过程编程,更加容易应对大规模复杂程序的开发。但像 Unix、Linux 这些复杂的系统,也都是基于 C 语言这种面向过程的编程语言开发的,你怎么看待这个现象?这跟我之前的讲解相矛盾吗?
使用任何一个编程语言编写的程序,最终执行上都要落实到CPU一条一条指令的执行(无论通过虚拟机解释执行,还是直接编译为机器码),CPU看不到是使用何种语言编写的程序。对于所有编程语言最终目的是两种:提高硬件的运行效率和提高程序员的开发效率。然而这两种很难兼得。 C语言在效率方面几乎做到了极致,它更适合挖掘硬件的价值,如:C语言用数组char a[8],经过编译以后变成了(基地址+偏移量)的方式。对于CPU来说,没有运算比加法更快,它的执行效率的算法复杂度是O(1)的。从执行效率这个方面看,开发操作系统和贴近硬件的底层程序,C语言是极好的选择。 C语言带来的问题是内存越界、野指针、内存泄露等。它只关心程序飞的高不高,不关心程序猿飞的累不累。为了解脱程序员,提高开发效率,设计了OOP等更“智能”的编程语言,但是开发容易毕竟来源于对底层的一层一层又一层的包装。完成一个特定操作有了更多的中间环节, 占用了更大的内存空间, 占用了更多的CPU运算。从这个角度看,OOP这种高级语言的流行是因为硬件越来越便宜了。我们可以想象如果大众消费级的主控芯片仍然是单核600MHz为主流,运行Android系统点击一个界面需要2秒才能响应,那我们现在用的大部分手机程序绝对不是使用JAVA开发的,Android操作系统也不可能建立起这么大的生态。
理论四:哪些代码看似是面向对象,实际是面向过程的?
这一章节主要回答的是上一章节的后三个问题。
三个典型的看似是面向对象风格但其实是面向编程的代码案例
1. 滥用getter、setter方法
不推荐使用Lombok这样的插件,它违反了面向对象编程的封装特性。
比如一个购物车类,有三个属性:数量、价格和购物车列表。
数量和价格定义getter和setter方法明显违反了面向对象的封装特性,items即使只暴露getter方法也是有问题的,因为上层代码拿到list之后仍然可以对购物车中的数据做修改,比如像下面这样:
|
|
但是如果不提供getter方法实际上是看不到购物车的,这时候怎么办呢?解决方法也很简单,以Java为例,我们可以通过 Java 提供的 Collections.unmodifiableList() 方法,让 getter 方法返回一个不可被修改的 UnmodifiableList 集合容器,而这个容器类重写了 List 容器中跟修改数据相关的方法,比如 add()、clear() 等方法。一旦我们调用这些修改数据的方法,代码就会抛出 UnsupportedOperationException 异常,这样就避免了容器中的数据被修改。具体的代码实现如下所示。
|
|
不过这样也是有风险的,因为即使无法修改List,也可以修改List中的item,这个问题可以留待日后讨论。
总结: 在设计实现类的时候,除非真的需要,尽量不要给属性定义setter方法。除此之外,尽管getter方法相对setter方法要安全些,但是如果返回的是集合容器(比如List),也要防范集合内部数据被修改的风险。
2. 滥用全局变量和全局方法
在面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。单例类对象在全局代码中只有一份,所以,它相当于一个全局变量。静态成员变量归属于类上的数据,被所有的实例化对象所共享,也相当于一定程度上的全局变量。而常量是一种非常常见的全局变量,比如一些代码中的配置参数,一般都设置为常量,放到一个 Constants 类中。静态方法一般用来操作静态变量或者外部数据。你可以联想一下我们常用的各种 Utils 类,里面的方法一般都会定义成静态方法,可以在不用创建对象的情况下,直接拿来使用。静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。
下面以Constants和Utils这两种常见的类为例,说说全局变量和全局方法的利与弊。
Constants
定义一个又大又全的常量类不是一个很好的设计思路,主要原因有:
- 影响代码的可维护性,增加了代码冲突的概率
- 增加代码的编译时间,对于比较大的工程来说,编译一次项目花费的时间可能会很长
- 影响代码的复用性
如何解决?
- 其一是按照功能进行拆分,比如MySQLConstants、RedisConstants等等
- 其二是不单独设计Constants常量类,而是直接定义在使用的类中,这样也提高了类设计的内聚性和代码的复用性
Utils
Utils类我们一般会放一些很常用的操作,比如URL拼接、JSON数据的处理等等,这些方法不需要共享任何数据,不需要定义任何属性,所以是彻彻底底的面向过程的编程风格。在这种情况下,我们是可以使用Utils类的,只是要避免滥用,要避免不加思考的随意定义Utils类。
此外,与常量类一样,尽量针对不同的功能设计不同的Utils类,比如FileUtils、IOUtils、StringUtils、UrlUtils等。
3. 定义数据和方法分离的类
我们再来看最后一种面向对象编程过程中,常见的面向过程风格的代码。那就是,数据定义在一个类中,方法定义在另一个类中。你可能会觉得,这么明显的面向过程风格的代码,谁会这么写呢?实际上,如果你是基于 MVC 三层结构做 Web 方面的后端开发,这样的代码你可能天天都在写。
传统的MVC架构(前后端分离的模式)分为Controller层、Service层、Repository层(Dao层),在每一层中也会定义相应的VO、Dto、Entity等。一般情况下,VO、Dto只会定义数据不会定义方法,所有操作都在对应的controller、Service以及Dao中,而这就是典型的面向过程的编程风格。
实际上,这种开发模式叫做基于贫血模型的开发模式。
为什么在面向对象编程中容易写出面向过程风格的代码?
- 面向过程符合流程化的思维模式,而面向对象是自底向上的,将任务进行拆解和组装
- 面向对象设计起来比较难,而很多工程师在开发的过程中更倾向于不动脑子的方式去实现需求
面向过程及面向过程的编程语言真的再无用武之地了吗?
- 在一些微小程序、数据处理和算法中,脚本式的面向过程的编程风格更合适
- 面向过程是面向对象的基础,即使在面向对象语言中,类里面每个方法的实现过程就是面向过程的
- 我们的最终目的是写出易维护、易读、易复用的高质量代码,所以这两种编程风格并不是非黑即白的。