设计模式之美-课程笔记2
理论三:面向对象的优势以及面向过程编程的意义
- 什么是面向过程编程以及面向过程编程语言?
- 面向对象编程相比于面向过程编程的优势?
- 为什么说POP更高级?
- 有哪些看似是OO其实是PO风格的代码?
- 在面向对象编程中,为什么更容易写出面向过程风格的代码?
- OPP和PP没用武之地了吗?
什么是面向过程编程以及面向过程编程语言
类比于OOD和OOP,定义如下:
- 面向过程编程也是一种编程范式或编程风格。它以过程(方法、函数、操作)作为组织代码的基本单元,以数据(成员变量、属性)与方法相分离为最主要特点。它是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成功能。
- 面向过程编程语言,不支持类和对象,不支持面向对象编程,仅支持面向过程编程。
看例子对比下
面向过程,按照步骤(动作)一步步来操作数据。方法和数据的定义分开。
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/zheng/user.txt", "/home/zheng/formatted_users.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 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/zheng/users.txt", "/home/zheng/formatted_users.txt");
}
}
面向对象编程相比于面向过程的优势
1. OOP更能应对大规模复杂程序的开发
大规模程序的处理流程会成为一张网状图错综复杂。主线也不止一条。
OOP允许我们先建模,再考虑他们的行为,最后才是按照流程把他们搭建。相比更清晰容易一点。另外,类天然作为组织数据对象和行为的一个载体,较好的帮我们组织代码,形成模块化。
2. OOP的代码更加易复用、易扩展、易维护
OOP的四大特性让这些实现变得更容易。
3. OOP更人性化、更高级、更智能
更贴近人类的思路。(相比于指令化顺序运行。)
理论四:哪些代码设计看似是面向对象,其实是面向过程?
哪些代码设计看似是面向对象,其实是面向过程?
1. 滥用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
,我们定义了Getter和Setter,对于items
,我们定义了Getter
和addItem
。
-
乍一看没问题,仔细一想(通过业务逻辑),前两个变量的Setter方法提供了修改的途径,可能导致被不正确修改后,与iterms里属性不一致。面向对象封装,就是通过控制访问权限,隐藏内部数据,暴露有限的接口访问、修改内部数据。
-
Items是一个集合,他的Getter返回的是一个集合容器List,外部调用者拿到它之后仍然可以改变。比如
ShoppingCart cart = new ShoppCart(); ... cart.getItems().clear(); // 清空购物车
同样的,清空购物车可能会导致数据不一致与总数量和价格。我们应该定义一个clear方法,将清空逻辑封装在里面, 透明的给使用者调用。
public class ShoppingCart { // ...省略其他代码... public void clear() { items.clear(); itemsCount = 0; totalPrice = 0.0; } }
-
此时我们又要获取到items,获取购物车列表,怎么办?Java中提供了一个
Collections.unmodifiableList()
方法,这个方法重写了List容器中修改相关的方法add(),clear(),一旦调用这些,会抛出一个UnsupportedOperationException
异常。因此实现让Getter返回一个不可变容器。public class ShoppingCart { // ...省略其他代码... public List<ShoppingCartItem> getItems() { return Collections.unmodifiableList(this.items); } } public class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E> { public boolean add(E e) { throw new UnsupportedOperationException(); } public void clear() { throw new UnsupportedOperationException(); } // ...省略其他代码... } ShoppingCart cart = new ShoppingCart(); List<ShoppingCartItem> items = cart.getItems(); items.clear();//抛出UnsupportedOperationException异常
-
还有一个问题,虽然改变不了容器,但是对于每个对象的属性,我们还是可以更改。e.g.
ShoppingCart cart = new ShoppingCart(); cart.add(new ShoppingCartItem(...)); List<ShoppingCartItem> items = cart.getItems(); ShoppingCartItem item = items.get(0); item.setPrice(19.0); // 这里修改了item的价格属性
总结下来就是,除非真的需要,尽量不定义Setter方法。除此之外,Getter返回集合容器的时候也要注意。
2. 滥用全局变量和全局方法
-
全局变量:单例类对象,静态成员变量,常量。常见的全局方法有静态方法。静态方法一般用来操作变量或者外部数据。(数据和方法分类,不符合封装特性)->面向过程风格。
-
例子看看(常量)
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:"; // ...省略更多的常量定义... }
代码的可维护性变差。大家都在往这个常量类中增加内容,这个类会变得很大,查找费时,代码提交容易冲突。
增加了代码的编译时间。
依赖这个类的代码越来越多,每次修改这个Constant类,依赖他的类都得重新编译,浪费时间。可能影响开发效率。
影响代码的复用性。比如在另一个项目中要复用一个类,而这个类依赖这个Constant,那么又得将整个Constant一并引入。(许多无关的常量)
-
如何改进?
- 拆分。mysql相关的就拆成MySQLConstant,Redis就是RedisConstant。
- 或者不定义Constant,而是哪个类使用这个常量,就将它定义在那个类中。比如RedisConfig中定义Redis相关的常量。提高了类设计的内聚性和代码的复用性。
-
另一个例子(Utils)
Utils类存在的意义就是复用通用的工具。
我们也知道,继承可以提供代码复用,但是有时两个类不一定具有继承关系。所以定义一个静态类,来使用的时候拼接到合适的地方。
这个其实是非常“面向过程”的。但是他确实解决了开发中的问题,所以我们辩证对待,不能滥用而非不能用。认真思考是否需要定义这种类每次。
你需要单独定义util类吗?是否可以将utils中的方法放到其他类?都回答完这些问题再做决定。
我们的最终目的是写出合适的代码以尽可能小的代价。
utils也可以像Constant一样适当的细化拆分。
3. 定义数据和方法分离的类
-
数据在一个类,操作数据的方法在另一个类。
后端开发MVC中到处都是-> _->
传统的 MVC 结构分为 Model 层、Controller 层、View 层这三层。不过,在做前后端分离之后,三层结构在后端开发中,会稍微有些调整,被分为 Controller 层、Service 层、Repository 层。Controller 层负责暴露接口给前端调用,Service 层负责核心业务逻辑,Repository 层负责数据读写。而在每一层中,我们又会定义相应的 VO(View Object)、BO(Business Object)、Entity。一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。这就是典型的面向过程的编程风格
-
这种叫做基于贫血模型的开发模式。后续会讲。
在面向对象编程中,为什么容易写出面向过程风格的代码?
面向过程更符合一种流程化的思想(相对简单),面向对象自底向上,先将任务和实体分解,理清之间的交互再组装。
面向过程编程及面向过程编程语言就真的无用武之地了吗?
- 微小程序;
- 数据处理;
- 以算法为主,数据为辅助,面向过程更适合一点;
- 面向过程有点像面向对象的基础;
- 辩证看待两种风格的配合。最终目的是写出好代码。易用易拓展,易读易维护。