其实之前对面向对象和面向过程等也有过大概的探究,不过都是应付面试做的一些皮毛的理解,对其发展过程,根本性的区别实际上也不甚了了,例如之前的这篇Blog:【Java SE基础 三】面向对象思想与类模型中给出过一版通俗意义的区别:
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。总而言之就是把问题分解成对象,对象直接隔离且有自己完备的结构,可以互相通过方法沟通来达成结构清晰稳定的目标
面向对象优势: 由于面向对象更容易从我们自身的角度出发去看问题,所以有天然的拟人优势,而且由于结构清晰明了,所以具备如下优势:较高的开发效率;较高的扩展性和代码可重用性;保证软件的高可维护性,这些优势又是由面向对象的特征来决定的。
以上的对面向对象和面向过程的对比以及面向对象的优势描述的其实很清晰,但之前我的理解是不深刻的,例如为什么面向对象有较高的扩展性?为什么开发的软件有高可维护性?这些都没有深入的思考,所以从根里还是不能很区分,再加上MVC贫血模式的开发方式下,往往接口和数据分离,看起来还是再基于面向过程编程,这样概念就更模糊了,所以有必要辨析清楚。
面向对象VS面向过程
什么是面向对象与面向对象编程语言?
- 面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
- 面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。
类比面向对象编程与面向对象编程语言的定义,面向过程编程和面向过程编程语言:
- 面向过程编程也是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
- 面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。
举一个直观的例子来看下:假设我们有一个记录了用户信息的文本文件 users.txt,每行文本的格式是 name&age&gender
(比如,小王 &28& 男)。我们希望写一个程序,从 users.txt 文件中逐行读取用户信息,然后格式化成 name\tage\tgender
(其中,\t 是分隔符)这种文本格式,并且按照 age 从小到大排序之后,重新写入到另一个文本文件 formatted_users.txt
中。针对这样一个小程序的开发,我们一块来看看,用面向过程和面向对象两种编程风格,编写出来的代码有什么不同,首先来看面向过程,使用c语言实现:
struct User { char name[64]; int age; char gender[16]; }; struct User parse_to_user(char* text) { // 将text(“小王&28&男”)解析成结构体struct User } char* format_to_text(struct User user) { // 将结构体struct User格式化成文本("小王\t28\t男") } void sort_users_by_age(struct User users[]) { // 按照年龄从小到大排序users } void format_user_file(char* origin_file_path, char* new_file_path) { // open files... struct User users[1024]; // 假设最大1024个用户 int count = 0; while(1) { // read until the file is empty struct User user = parse_to_user(line); users[count++] = user; } sort_users_by_age(users); for (int i = 0; i < count; ++i) { char* formatted_user_text = format_to_text(users[i]); // write to new file... } // close files... } int main(char** args, int argv) { format_user_file("/home/user.txt", "/home/formatted_users.txt"); }
接着是面向对象的写法,使用java语言实现:
public class User { private String name; private int age; private String gender; public User(String name, int age, String gender) { this.name = name; this.age = age; this.gender = gender; } public static User praseFrom(String userInfoText) { // 将text(“小王&28&男”)解析成类User } public String formatToText() { // 将类User格式化成文本("小王\t28\t男") } } public class UserFileFormatter { public void format(String userFile, String formattedUserFile) { // Open files... List users = new ArrayList<>(); while (1) { // read until file is empty // read from file into userText... User user = User.parseFrom(userText); users.add(user); } // sort users by age... for (int i = 0; i < users.size(); ++i) { String formattedUserText = user.formatToText(); // write to new file... } // close files... } } public class MainApplication { public static void main(String[] args) { UserFileFormatter userFileFormatter = new UserFileFormatter(); userFileFormatter.format("/home/users.txt", "/home/formatted_users.txt"); } }
从上面的代码中,我们可以看出,面向过程和面向对象最基本的区别就是,代码的组织方式不同。面向过程风格的代码被组织成了一组方法集合及其数据结构(struct User),方法和数据结构的定义是分开的。面向对象风格的代码被组织成一组类,方法和数据结构被绑定一起,定义在类中。
面向对象的优势
上边的例子,两种编程风格实现的代码貌似差不多,顶多就是代码的组织方式有点区别,没有感觉到面向对象编程有什么明显的优势呀!之所以有这种感觉,主要原因是这个例子程序比较简单、不够复杂。
1.OOP 更加能够应对大规模复杂程序的开发
对于简单程序的开发来说,不管是用面向过程编程风格,还是用面向对象编程风格,差别确实不会很大,甚至有的时候,面向过程的编程风格反倒更有优势。因为需求足够简单,整个程序的处理流程只有一条主线,很容易被划分成顺序执行的几个步骤,然后逐句翻译成代码,这就非常适合采用面向过程这种面条式的编程风格来实现。
但对于大规模复杂程序的开发来说,整个程序的处理流程错综复杂,并非只有一条主线。如果把整个程序的处理流程画出来的话,会是一个网状结构。如果我们再用面向过程编程这种流程化、线性的思维方式,去翻译这个网状结构,去思考如何把程序拆解为一组顺序执行的方法,就会比较吃力。这个时候,面向对象的编程风格的优势就比较明显了。
首先面向对象提供了一种网状复杂业务开发的高效开发方式面向对象编程是以类为思考对象。在进行面向对象编程的时候,我们并不是一上来就去思考,如何将复杂的流程拆解为一个一个方法,而是
- 先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立交互关系,而完成这些工作完全不需要考虑错综复杂的处理流程
- 当我们有了类的设计之后,然后再像搭积木一样,按照处理流程,将类组装起来形成整个程序。
这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰。
其次,面向对象编程还提供了一种更加清晰的、更加模块化的代码组织方式。比如,我们开发一个电商交易系统,业务逻辑复杂,代码量很大,可能要定义数百个函数、数百个数据结构,那如何分门别类地组织这些函数和数据结构,才能不至于看起来比较凌乱呢?类就是一种非常好的组织这些函数和数据结构的方式,是一种将代码模块化的有效手段,虽然像C语言这样的面向过程开发,也能通过文件夹分门别类处理,但是并不强制,面向对象的类强制完成了这种代码组织方式。实际上面向过程语言也能实现面向对象的代码组织形式,只不过实现的成本更高而已。
2.OOP 风格的代码更易复用、易扩展、易维护
面向对象支持面向对象思想的四大编程特性:封装、继承、抽象、多态:
- 封装特性:封装特性是面向对象编程相比于面向过程编程的一个最基本的区别,因为它基于的是面向对象编程中最基本的类的概念。面向对象编程通过类这种组织代码的方式,将数据和方法绑定在一起,通过访问权限控制,只允许外部调用者通过类暴露的有限方法访问数据,而不会像面向过程编程那样,数据可以被任意方法随意修改。因此,面向对象编程提供的封装特性更有利于提高代码的易维护性。
- 抽象特性:函数本身就是一种抽象,它隐藏了具体的实现。我们在使用函数的时候,只需要了解函数具有什么功能,而不需要了解它是怎么实现的。从这一点上,不管面向过程编程还是是面向对象编程,都支持抽象特性。不过,面向对象编程还提供了其他抽象特性的实现方式。这些实现方式是面向过程编程所不具备的,比如基于接口实现的抽象。基于接口的抽象,可以让我们在不改变原有实现的情况下,轻松替换新的实现逻辑,提高了代码的可扩展性。
- 继承特性:继承特性是面向对象编程相比于面向过程编程所特有。如果两个类有一些相同的属性和方法,我们就可以将这些相同的代码,抽取到父类中,让两个子类继承父类。这样两个子类也就可以重用父类中的代码,避免了代码重复写多遍,继承特性提高了代码的复用性。
- 多态特性:基于这个特性,我们在需要修改一个功能实现的时候,可以通过实现一个新的子类的方式,在子类中重写原来的功能逻辑,用子类替换父类。在实际的代码运行过程中,调用子类新的功能逻辑,而不是在原有代码上做修改。这就遵从了“对修改关闭、对扩展开放”的设计原则,提高代码的扩展性。除此之外,利用多态特性,不同的类对象可以传递给相同的方法,执行不同的代码逻辑,提高了代码的复用性。
所以说,基于这四大特性,利用面向对象编程,我们可以更轻松地写出易复用、易扩展、易维护的代码。当然,我们不能说,利用面向过程风格就不可以写出易复用、易扩展、易维护的代码,但没有四大特性的帮助,付出的代价可能就要高一些。
3.OOP 语言更加人性化、更加高级、更加智能
跟二进制指令、汇编语言、面向过程编程语言相比,面向对象编程语言的编程套路、思考问题的方式,是完全不一样的。前三者是一种计算机思维方式,而面向对象是一种人类的思维方式。我们在用前面三种语言编程的时候,我们是在思考,如何设计一组指令,告诉机器去执行这组指令,操作某些数据,帮我们完成某个任务。而在进行面向对象编程时候,我们是在思考,如何给业务建模,如何将真实的世界映射为类或者对象,这让我们更加能聚焦到业务本身,而不是思考如何跟机器打交道。可以这么说,越高级的编程语言离机器越远,离我们人类越近,越智能,由此可见,未来更智能的编程语言可能只需要我们搞清楚需求文档就自动实现了,这他喵不就是PaaS低代码平台么?
面向对象语言写的面向过程代码
实际上为什么长久以来不很分的清面向对象和面向过程的概念,就是一些错误的编程习惯,虽然在用Java这种面向对象编程语言和类、对象这种最小组织单元编写代码,但是写出来的其实很多都是面向过程风格的代码
1 滥用 getter、setter 方法
在之前参与的项目开发中,我们经常是直接用 IDE 或者 Lombok 插件(如果是 Java 项目的话)自动生成所有属性的 getter、setter 方法,主要是因为虽然现在不一定用的上,但以后可能也会用。但这种方式实际上破坏了面向面向对象编程的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格。
在设计实现类的时候,除非真的需要,否则,尽量不要给属性定义 setter 方法。除此之外,尽管 getter 方法相对 setter 方法要安全些,但是如果返回的是集合容器(比如例子中的 List 容器),也要防范集合内部数据被修改的危险
2.全局变量和全局方法
在面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。单例类对象在全局代码中只有一份,所以,它相当于一个全局变量。静态成员变量归属于类上的数据,被所有的实例化对象所共享,也相当于一定程度上的全局变量。而常量是一种非常常见的全局变量,比如一些代码中的配置参数,一般都设置为常量,放到一个 Constants 类中。静态方法一般用来操作静态变量或者外部数据,也就是我们常用的各种 Utils 类,里面的方法一般都会定义成静态方法,可以在不用创建对象的情况下,直接拿来使用。静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。
虽然存在这种面向过程的全局变量与全局方法,但不表示我们就是完全排斥的,只要利于我们代码复用也是可以大胆用的,不过对于这两种类的设计,我们尽量能做到职责单一,定义一些细化的小类,比如 RedisConstants、FileUtils,而不是定义一个大而全的 Constants 类、Utils 类。除此之外,如果能将这些类中的属性和方法,划分归并到其他业务类中,那是最好不过的了,能极大地提高类的内聚性和代码的可复用性。
3. 基于贫血模型的开发模式
面向对象编程过程中,常见的面向过程风格的代码。那就是,数据定义在一个类中,方法定义在另一个类中。实际上,像我们这种基于 MVC 三层结构做 Web 方面的后端开发,这样的代码可能天天都在写。这种开发模式叫作基于贫血模型的开发模式,也是我们现在非常常用的一种 Web 项目的开发模式。相较于这种贫血模式,基于DDD的充血模式开发方式也是一种好的实现方法。
辩证看待面向过程与面向对象
实际上,我们在进行面向对象编程的时候,很容易不由自主地就写出面向过程风格的代码,这是因为面向过程编程风格符合人的流程化思维方式,例如在生活中,去完成一个任务,我们一般都会思考,应该先做什么、后做什么,如何一步一步地顺序执行一系列操作,最后完成整个任务,实际上做需求也是类似,我们趋向于流程化的解决问题。而面向对象编程风格正好相反。它是一种自底向上的思考方式。它不是先去按照执行流程来分解任务,而是将任务翻译成一个一个的小的模块(也就是类),设计类之间的交互,最后按照流程将类组装起来,完成整个任务。面向对象设计更难,所以大脑趋利避害。
不管使用面向过程还是面向对象哪种风格来写代码,我们最终的目的还是写出易维护、易读、易复用、易扩展的高质量代码。只要我们能避免面向过程编程风格的一些弊端,控制好它的副作用,在掌控范围内为我们所用,我们就大可不用避讳在面向对象编程中写面向过程风格的代码
总结一下
面向对象还是面向过程?其实不是那么重要,我们只看最终目的:是否能写出易维护、易读、易复用、易扩展的高质量代码,面对复杂网状业务场景的时候我们更趋向于使用面向对象的方式,因为设计它的语言四大基本特性(封装、继承、抽象、多态)和代码组织方式(类和对象)具备编写高质量代码的先决条件。但这并不能说明面向过程一无是处,在简单的线状业务场景下面向过程可能会更快的实现,甚至我们经常使用的网站开发模式MVC贫血模式就是基于面向过程实现的,因为简单且流程化的思维更容易被我们理解。所以还是那个道理:谈概念和优势的时候一定要关联使用场景,没有最好,只有最合适。