前言
本文主要介绍什么是面向对象编程,以及面向对象编程与面向过程编程的差异。并总结了实际开发中,几种看似是面向对象编程,实际为面向过程编程的常见情形。
一、什么是面向对象编程
关于面向对象编程可以总结为以下两句话:
- 面向对象是一种编程范式或者编程风格,它以类或者对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
- 面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。
谈面向对象编程时不可避免地要与面向过程编程进行对比,以下是总结出的面向过程编程的定义:
- 面向过程编程也是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
- 面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。
注意:另外的说法是三大特性:封装、继承、多态,不包含抽象
下面通过代码对比,加深对面向过程与面向对象的理解:
假设我们有一个记录了用户信息的文本文件 users.txt,每行文本的格式是 name&age&gender(比如,xiaowang&28&male)。我们希望写一个程序,从 users.txt 文件中逐行读取用户信息,然后格式化成 name#age#gender这种文本格式,并且按照 age 从小到大排序之后,重新写入到另一个文本文件formatted_users.txt.
面向过程编码:
public class FileFormatProcess {
private void format(String originalFile, String targetFile) throws IOException {
Stream<String> lines = Files.lines(new File(originalFile).toPath());
Stream<String[]> users = formatToMapList(lines);
Stream<String[]> result = sortByAge(users);
formatToText(result, targetFile);
}
private void formatToText(Stream<String[]> result, String targetFile) throws FileNotFoundException {
try (PrintWriter writer =
new PrintWriter(new FileOutputStream(new File(targetFile)))) {
result.map(each -> String.join("#", each)).forEach(each -> {
writer.write(each);
writer.println();
});
}
}
private Stream<String[]> sortByAge(Stream<String[]> infos) {
return infos.sorted(Comparator.comparingInt(u -> Integer.parseInt(u[1])));
}
private Stream<String[]> formatToMapList(Stream<String> lines) {
return lines.map(line -> {
return line.split("&");
});
}
public static void main(String[] args) throws IOException {
new FileFormatProcess().format("G:\\github\\userfile.txt",
"G:\\github\\fomatteduserfile.txt");
}
}
面向对象编码
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 parseFrom(String userInfoText) {
String[] array = userInfoText.split("&");
if (array.length != 3) {
// error
}
// 这里要检查用户名,年龄,姓名的合法性,省略
return new User(array[0], Integer.parseInt(array[1]), array[2]);
}
public String formattedUserText() {
StringBuilder builder = new StringBuilder();
builder.append(name).append("#")
.append(age).append("#")
.append(gender);
return builder.toString();
}
public int getAge() {
return age;
}
}
public class UserFileFormatter {
private void format(String originalFile, String targetFile) throws Exception {
Stream<String> lines = Files.lines(new File(originalFile).toPath());
Stream<User> users = formatToUserList(lines);
Stream<User> result = sortByAge(users);
formattedUserText(result, targetFile);
}
private void formattedUserText(Stream<User> users, String targetFile) throws Exception {
try (PrintWriter writer =
new PrintWriter(new FileOutputStream(new File(targetFile)))) {
users.map(User::formattedUserText).forEach(each -> {
writer.write(each);
writer.println();
});
}
}
private Stream<User> sortByAge(Stream<User> users) {
return users.sorted(Comparator.comparingInt(User::getAge));
}
private Stream<User> formatToUserList(Stream<String> lines) {
return lines.filter(line -> !"".equals(line)).map(User::parseFrom);
}
public static void main(String[] args) throws Exception {
new UserFileFormatter().format("G:\\github\\userfile.txt", "G:\\github\\fomatteduserfile.txt");
}
}
从上面的代码中,我们可以看出,面向过程和面向对象最基本的区别就是,代码的组织方式不同。面向过程风格的代码被组织成了一组方法集合及其数据结构(String[]),方法和数据结构的定义是分开的。面向对象风格的代码被组织成一组类,方法和数据结构被绑定一起,定义在类中。
当然,这个并不是唯一的区别。
二、面向对象vs面向过程
1)OOP 更加能够应对大规模复杂程序的开发
上面的例子,比较简单。尤其是使用过了jdk8的新特性后,代码都比较整洁。面向对象跟面向过程两者风格貌似相差不大。
对于简单程序的开发来说,不管是用面向过程编程风格,还是用面向对象编程风格,差别确实不会很大。
如果需求足够简单,整个程序的处理流程只有一条主线,很容易被划分成顺序执行的几个步骤,然后逐句翻译成代码,这就非常适合采用面向过程这种面条式的编程风格来实现。
但对于大规模复杂程序的开发来说,整个程序的处理流程错综复杂,并非只有一条主线,而是一个网站结构。这个时候,使用面向对象的编程风格的优势就比较明显了。
面向对象编程是以类为思考对象。面向对象编程,我们并不是一上来就去思考,如何将复杂的流程拆解为一个一个方法,而是先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立怎么调用,而完成这些工作完全不需要考虑错综复杂的处理流程。当我们有了类的设计之后,然后再像搭积木一样,按照处理流程,将类组装起来形成整个程序。这种开发模式、思考问题的方式,能让我们在应对复杂程序开发的时候,思路更加清晰。
同时,比如,我们开发一个电商交易系统,业务逻辑复杂,代码量很大,可能要定义数百个函数、数百个数据结构,那如何分门别类地组织这些函数和数据结构,才能不至于看起来比较凌乱呢?类就是一种非常好的组织这些函数和数据结构的方式,是一种将代码模块化的有效手段。
2)OOP 风格的代码更易复用、易扩展、易维护
上面的例子比较简单,没有用到面向对象的高级特性:封装,继承,抽象,多态。而这四种特性,能够帮助我们写出更易复用、易扩展、易维护的代码。关于这四大特性的介绍,参考:面向对象的四个特性
面向过程编程是一种非常简单的编程风格,并没有像面向对象编程那样提供丰富的特性。
封装特性是面向对象编程相比于面向过程编程的一个最基本的区别,因为它基于的是面向对象编程中最基本的类的概念。面向对象编程通过类这种组织代码的方式,将数据和方法绑定在一起,通过访问权限控制,只允许外部调用者通过类暴露的有限方法访问数据,而不会像面向过程编程那样,数据可以被任意方法随意修改。因此,面向对象编程提供的封装特性更有利于提高代码的易维护性。
抽象性,函数本身就是一种抽象,它隐藏了具体的实现。我们在使用函数的时候,只需要了解函数具有什么功能,而不需要了解它是怎么实现的。从这一点上,不管面向过程编程还是是面向对象编程,都支持抽象特性。不过,面向对象编程还提供了其他抽象特性的实现方式。这些实现方式是面向过程编程所不具备的,比如基于接口实现的抽象。基于接口的抽象,可以让我们在不改变原有实现的情况下,轻松替换新的实现逻辑,提高了代码的可扩展性。
继承可以提高代码的复用性。
多态性,可以让我们的代码遵从对“修改关闭、对扩展开放”的设计原则,提高代码的扩展性。除此之外,多态还可以提高代码的复用性。
三、面向对象避坑
运用java这种面向对象编程语言,并不能写出真正的面向对象编程的代码。下面只要总结几种看似是面向对象,实际上是面向过程编程风格的代码,并且分析一下,为什么我们很容易写出这样的代码
1)滥用setter、getter方法
先看一段代码
public class ShoppingCart {
private int itemCount;
private double totalPrice;
private List<ShoppingCartItem> items = new ArrayList<>();
public int getItemCount() {
return itemCount;
}
public void setItemCount(int itemCount) {
this.itemCount = itemCount;
}
public double getTotalPrice() {
return totalPrice;
}
public void setTotalPrice(double totalPrice) {
this.totalPrice = totalPrice;
}
public List<ShoppingCartItem> getItems() {
return items;
}
public void addItem(ShoppingCartItem item) {
items.add(item);
itemCount++;
totalPrice += item.getPrice();
}
}
上面代码为几乎为每个属性都提供了setter、getter
方法。项目中还会经常用到lombock
插件。下面分析存在的问题:
itemsCount、totalPrice
这两个属性提供了public
的setter
方法,是外部可以修改它们的值,这样会导致与items
属性值不一致问题。这里setter
破坏了封装特性,没有控制访问权限,退化为面向过程编程。
items
属性提供的getter
方法,返回原始集合的引用,外部可以对其进行修改,也会导致属性不一致问题,比如
// 问题1
ShoppingCart cart = new ShoppCart();
...
cart.getItems().clear(); // 清空购物车
// 问题2
ShoppingCart cart = new ShoppingCart();
cart.add(new ShoppingCartItem(...));
List<ShoppingCartItem> items = cart.getItems();
ShoppingCartItem item = items.get(0);
item.setPrice(19.0); // 这里修改了item的价格属性
针对问题一,用户想要清空购物车时,应该调用ShoppingCart
提供的方法,getter
应该返回一个不可修改的对象引用。问题二可以返回一个深拷贝对象进行解决。
上面改动
public List<ShoppingCartItem> getItems() {
List<ShoppingCartItem> tmpShoppingCartItems = new ArrayList<>(items);
return Collections.unmodifiableList(tmpShoppingCartItems);
}
public void clear() {
items.clear();
itemCount = 0;
totalPrice = 0.0;
}
小结:除非真的需要,否则,尽量不要给属性定义 setter 方法。同时返回集合时要谨慎。
2) 滥用全局变量和全局方法
面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。
我们经常用的有Constants
类(存放配置参数等常量)跟Utils
类(定义成静态方法),静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。当然,并不是说完全没有好处。下面对这两种类型分别讨论。
Constans
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
类拆解为功能更加单一的多个类,比如跟 MySQL 配置相关的常量,我们放到 MysqlConstants
类中;跟 Redis
配置相关的常量,我们放到 RedisConstants
类中。另一种是,不单独地设计 Constants
常量类,而是哪个类用到了某个常量,我们就把这个常量定义到这个类中。比如,RedisConfig
类用到了 Redis 配置相关的常量,那我们就直接将这些常量定义在 RedisConfig
中,这样也提高了类设计的内聚性和代码的复用性。
在开发中,Utils
类的出现往往是这样一种情景:如果我们有两个类 A 和 B,它们要用到一块相同的功能逻辑,为了避免代码重复,我们可能会想到继承,但A 类和 B 类并不一定具有继承关系,如果仅仅为了代码复用,生硬地抽象出一个父类出来,会影响到代码的可读性。这个时候,我们就会用到Utils
类。
实际上,只包含静态方法不包含任何属性的 Utils 类,是面向过程的编程风格。但这并不是说,我们就要杜绝使用 Utils 类了。实际上,从刚刚讲的 Utils 类存在的目的来看,它在软件开发中还是挺有用的,能解决代码复用问题。所以,这里并不是说完全不能用 Utils 类,而是说,要尽量避免滥用,不要不加思考地随意去定义 Utils 类。
同样的,在设计Utils
时,可以考虑细化一下,针对不同的功能,设计不同的 Utils
类,比如 FileUtils、IOUtils、StringUtils、UrlUtils
等。
3)定义数据与方法分类的类
现在的Web开发中,常用到三层结构:Controller 层、Service 层、Repository 层。
Controller 层负责暴露接口给前端调用,Service 层负责核心业务逻辑,Repository 层负责数据读写。
而在每一层中,我们又会定义相应的 VO(View Object)、BO(Business Object)、Entity。一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。这就是典型的面向过程的编程风格。
这种开发模式叫作基于贫血模型的开发模式,也是我们现在非常常用的一种 Web 项目的开发模式。
与之相对应的是,充血模式。两者比较参考后面的文章
总结
面向过程与面向对象编程各有其优缺点。了解两者的差异后,在实际开发中才能更好地选择哪种开发模式。