面向过程的定义
面向过程编程
是一种编程范式或编程风格。
- 它以
过程
(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据
(可以理解为成员变量、属性)与方法相分离为最主要的特点。 - 面向过程风格是一种 流程化 的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
面向过程编程语言
最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。
与面向对象的区别
代码的组织方式不同
- 面向过程风格的代码被组织成了一组方法集合及其数据结构,方法和数据结构的定义是分开的。
- 面向对象风格的代码被组织成一组类,方法和数据结构被绑定一起,定义在类中。
OOP 更加能够应对大规模复杂程序的开发
- 简单的程序容易被划分成顺序执行的几个步骤。
- 大规模复杂程序的开发来说,整个程序的处理流程错综复杂,并非只有一条主线。如果把整个程序的处理流程画出来的话,会是一个网状结构。如果我们再用面向过程编程这种流程化、线性的思维方式,去翻译这个网状结构,去思考如何把程序拆解为一组顺序执行的方法,就会比较吃力。这个时候,面向对象的编程风格的优势就比较明显了。
- 面向对象编程是以类为思考对象。先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立交互关系,而完成这些工作完全不需要考虑错综复杂的处理流程。当我们有了类的设计之后,然后再像搭积木一样,按照处理流程,将类组装起来形成整个程序。这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰。
- 面向对象编程提供了一种更加清晰的、更加模块化的代码组织方式。业务逻辑复杂,代码量大的模块,可能要定义数百个函数、数百个数据结构,类就是一种非常好的组织这些函数和数据结构的方式,是一种将代码模块化的有效手段。
- 面向过程编程和面向对象编程并非完全对立的。很多软件开发中,尽管利用的是面向过程的编程语言,也都有借鉴面向对象编程的一些优点。
OOP 风格的代码更易复用、易扩展、易维护
面向过程编程是一种非常简单的编程风格,并没有像面向对象编程那样提供丰富的特性。
- 封装特性更有利于提高代码的易维护性。 将数据和方法绑定在一起,通过访问权限控制,只允许外部调用者通过类暴露的有限方法访问数据,而不会像面向过程编程那样,数据可以被任意方法随意修改。
- 抽象特性可以提高代码的可扩展性。 基于接口的抽象,可以让我们在不改变原有实现的情况下,轻松替换新的实现逻辑。
- 继承特性可以提高代码的复用性。 如果两个类有一些相同的属性和方法,我们就可以将这些相同的代码,抽取到父类中,让两个子类继承父类。这样两个子类也就可以重用父类中的代码,避免了代码重复写多遍。
- 多态特性也可以提高代码的复用性。 在子类中重写原来的功能逻辑,用子类替换父类。在实际的代码运行过程中,调用子类新的功能逻辑,而不是在原有代码上做修改。这就遵从了“对修改关闭、对扩展开放”的设计原则。
OOP 语言更加人性化、更加高级、更加智能
跟二进制指令、汇编语言、面向过程编程语言相比,面向对象编程语言的编程套路、思考问题的方式,是完全不一样的。前三者是一种计算机思维方式,而面向对象是一种人类的思维方式。
进行面向对象编程时候,我们是在思考,如何给业务建模,如何将真实的世界映射为类或者对象,这让我们更加能聚焦到业务本身,而不是思考如何跟机器打交道。
可以这么说,越高级的编程语言离机器越“远”,离我们人类越“近”,越“智能”。
假面向对象
滥用 getter、setter 方法
为每个属性都定义 getter
和 setter
方法,违反了面向对象编程的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格。如:
public class ShoppingCart {
private int itemsCount;
private double totalPrice;
private List<ShoppingCartItem> items = new ArrayList<>();
public int getItemsCount() {
return this.itemsCount;
}
public void setItemsCount(int itemsCount) {
this.itemsCount = itemsCount;
}
public double getTotalPrice() {
return this.totalPrice;
}
public void setTotalPrice(double totalPrice) {
this.totalPrice = totalPrice;
}
public List<ShoppingCartItem> getItems() {
return this.items;
}
public void addItem(ShoppingCartItem item) {
items.add(item);
itemsCount++;
totalPrice += item.getPrice();
}
// ...省略其他方法...
}
itemsCount
和totalPrice
。虽然我们将它们定义成private
私有属性,但是提供了public
的getter
、setter
方法,这就跟将这两个属性定义为public
公有属性,没有什么两样了。- 任何代码都可以随意调用
setter
方法,来重新设置itemsCount
、totalPrice
属性的值,这也会导致其跟items
属性的值不一致。- 暴露不应该暴露的
setter
方法,明显违反了面向对象的封装特性。items
属性返回的是一个List
集合容器。外部调用者在拿到这个容器之后,是可以操作容器内部数据的,也就是说,外部代码还是能修改items
中的数据,导致itemsCount
、totalPrice
、items
三者数据不一致。
在设计实现类的时候,除非真的需要,否则,尽量不要给属性定义 setter
方法。除此之外,尽管 getter
方法相对 setter
方法要安全些,但是如果返回的是集合容器(比如例子中的 List
容器),也要防范集合内部数据被修改的危险。
滥用全局变量和全局方法
在面向对象编程中,常见的全局变量有:单例类对象
、静态成员变量
、常量
等,常见的全局方法有静态方法
。静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。
Constants 类
一些代码中的配置参数,一般都设置为常量,放到一个配置类中。如:
public class Constants {
public static final String MYSQL_ADDR_KEY = "mysql_addr";
public static final String MYSQL_DB_NAME_KEY = "db_name";
public static final String MYSQL_USERNAME_KEY = "mysql_username";
public static final String MYSQL_PASSWORD_KEY = "mysql_password";
public static final String REDIS_DEFAULT_ADDR = "192.168.7.2:7234";
public static final int REDIS_DEFAULT_MAX_TOTAL = 50;
public static final int REDIS_DEFAULT_MAX_IDLE = 50;
public static final int REDIS_DEFAULT_MIN_IDLE = 20;
public static final String REDIS_DEFAULT_KEY_PREFIX = "rt:";
// ...省略更多的常量定义...
}
弊端:
- 影响代码的可维护性。 如果参与开发同一个项目的工程师有很多,在开发过程中,可能都要涉及修改这个类,比如往这个类里添加常量,那这个类就会变得越来越大,成百上千行都有可能,查找修改某个常量也会变得比较费时,而且还会增加提交代码冲突的概率。
- 影响代码的复用性。 即便某个类只依赖
Constants
类中的一小部分常量,我们仍然需要把整个Constants
类也一并引入,也就引入了很多无关的常量到新的项目中。
改进:
- 拆解。 将
Constants
类拆解为功能更加单一的多个类,比如跟MySQL
配置相关的常量,我们放到MysqlConstants
类中。 - 融合。 不单独地设计
Constants
常量类,而是哪个类用到了某个常量,我们就把这个常量定义到这个类中。
Utils 类
将没有继承关系的两个类中,具有相同功能逻辑的部分抽取出来,定义为只包含静态方法的 Utils
类。达到代码复用的目的。
只包含静态方法不包含任何属性的 Utils
类,是彻彻底底的面向过程的编程风格。但这并不是说,我们就要杜绝使用 Utils
类了。
而是要尽量避免滥用,不要不加思考地随意去定义 Utils
类。
设计 Utils
类的时候,最好也能细化一下,针对不同的功能,设计不同的 Utils
类,比如 FileUtils
、IOUtils
、StringUtils
、UrlUtils
等,不要设计一个过于大而全的 Utils
类。
定义数据和方法分离的类
基于贫血模型的开发模式:
基于 MVC 三层结构做 Web 方面的后端开发,经常会将数据定义在一个类中,方法定义在另一个类中。
我们会定义相应的 VO
(View Object)声明接口返回的数据、BO
(Business Object)、Entity
。一般情况下,VO
、BO
、Entity
中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller
类、Service
类、Repository
类中。这就是典型的面向过程的编程风格。
详见 充血模型和贫血模型
易于面向过程的原因
思维方式不同
- 面向过程编程风格符合人的流程化思维方式,先做什么、后做什么,如何一步一步地顺序执行一系列操作,最后完成整个任务。
- 面向对象编程风格正好相反。它是一种自底向上的思考方式。它不是先去按照执行流程来分解任务,而是将任务翻译成一个一个的小的模块(也就是类),设计类之间的交互,最后按照流程将类组装起来,完成整个任务。
复杂度不同
在面向对象编程中,类的设计还是挺需要技巧,挺需要一定设计经验的。你要去思考如何封装合适的数据和方法到一个类里,如何设计类之间的关系,如何设计类之间的交互等等诸多设计问题。