面向对象编程 OOP 全称是 Object Oriented Programming
面向对象编程中有两个非常重要概念,类和对象。大部分编程语言都是面向对象编程语言,比如 Java、C++、Go、Python、C#、Ruby、JavaScript、Objective-C、Scala、PHP、Perl等等。并且大部分程序员在开发项目的时候,都是基于面向对象编程语言进行的面向对象编程。
面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石
面向对象分析 OOA 全称是 Object Oriented Analysis;面向对象设计 OOD 全称是 Object Oriented Design。OOA、OOD、OOP 三个连在一起就是面向对象分析、设计、编程,正好是面向对象软件开发要经历的三个阶段
分析和设计两个阶段最终的产出是类的设计,包括程序被拆解为哪些类,每个类有哪些属性方法,类与类之间如何交互等等。它们比其他的分析和设计更加具体、更加落地、更加贴近编码,更能够顺利地过渡到面向对象编程环节
UML(Unified Modeling Language)统一建模语言。很多讲解面向对象或设计模式的书籍,常用它来画图表达面向对象或设计模式的设计思路
UML 是一种非常复杂的东西。它不仅仅包含常提到的类图,还有用例图、顺序图、活动图、状态图、组件图等。但是即便仅仅使用类图,学习成本也是很高的。就单说类之间的关系,UML 就定义了很多种,比如泛化、实现、关联、聚合、组合、依赖等
要想完全掌握,并且熟练运用这些类之间的关系,来画UML类图,肯定要花很多的学习精力。而且,UML作为一种沟通工具,即便你能完全按照UML规范来画类图,可对于不熟悉的人来说,看懂的成本也还是很高的。
所以,从我的开发经验来说,UML 在互联网公司的项目开发中,用处可能并不大。为了文档化软件设计或者方便讨论软件设计,大部分情况下,随手画个不那么规范的草图,能够达意,方便沟通就够了,而完全按照 UML 规范来将草图标准化,所付出的代价是不值得的
很多类图并没有完全遵守 UML 的规范标准。对 UML 类图规范做了简化,并配上了详细的文字解释,力图让你一眼就能看懂
类通过暴露有限的访问接口,授权外部仅能通过类提供的函数来访问内部信息或者数据
public class Wallet {
private String id;
private long createTime;
private BigDecimal balance;
private long balanceLastModifiedTime;
// ...省略其他属性...
public Wallet() {
this.id = IdGenerator.getInstance().generate();
this.createTime = System.currentTimeMillis();
this.balance = BigDecimal.ZERO;
this.balanceLastModifiedTime = System.currentTimeMillis();
}
// 注意:下面对 get 方法做了代码折叠,是为了减少代码所占文章的篇幅
public String getId() { return this.id; }
public long getCreateTime() { return this.createTime; }
public BigDecimal getBalance() { return this.balance; }
public long getBalanceLastModifiedTime() { return this.balanceLastModifiedTime; }
public void increaseBalance(BigDecimal increasedAmount) {
if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
this.balance = this.balance.add(increasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
public void decreaseBalance(BigDecimal decreasedAmount) {
if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
if (decreasedAmount.compareTo(this.balance) > 0) {
throw new InsufficientAmountException("...");
}
this.balance = this.balance.subtract(decreasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}
对于封装这个特性,需要编程语言本身提供一定的语法机制来支持。这个语法机制就是 访问权限控制。 例子中的 private、public 等关键字就是 Java 语言中的访问权限控制语法
在面向对象编程中,借助编程语言提供的接口类 interface 或者抽象类 abstract 这两种语法机制,来实现抽象这一特性。
把编程语言提供的接口语法叫作 “接口类” 而不是 “接口”。是因为 “接口” 这个词太泛化,比如 API 接口等
public interface IPictureStorage {
void savePicture(Picture picture);
Image getPicture(String pictureId);
void deletePicture(String pictureId);
void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
}
public class PictureStorage implements IPictureStorage {
// ...省略其他属性...
@Override
public void savePicture(Picture picture) { ... }
@Override
public Image getPicture(String pictureId) { ... }
@Override
public void deletePicture(String pictureId) { ... }
@Override
public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... }
}
利用 Java 中的 interface 接口语法来实现抽象特性。在使用时只需要了解 IPictureStorage 这个接口类暴露了哪些方法就可以了,不需要去查看 PictureStorage 类里的具体实现逻辑
继承是用来表示类之间的 is-a 关系。从继承关系上来讲,继承可以分为两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类
为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持,比如 Java 使用 extends 关键字来实现继承。
继承最大的一个好处就是代码复用。过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差
继承这个特性也是一个非常有争议的特性。很多人觉得继承是一种反模式。应该尽量少用,甚至不用。关于这个问题,在后面讲到“多用组合少用继承”这种设计思想的时候,会非常详细地再讲解
多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。对于多态这种特性,纯文字解释不好理解,直接看代码
public class DynamicArray {
private static final int DEFAULT_CAPACITY = 10;
protected int size = 0;
protected int capacity = DEFAULT_CAPACITY;
protected Integer[] elements = new Integer[DEFAULT_CAPACITY];
public int size() { return this.size; }
public Integer get(int index) { return elements[index];}
//...省略n多方法...
public void add(Integer e) {
ensureCapacity();
elements[size++] = e;
}
protected void ensureCapacity() {
//...如果数组满了就扩容...代码省略...
}
}
public class SortedDynamicArray extends DynamicArray {
@Override
public void add(Integer e) {
ensureCapacity();
int i;
for (i = size-1; i>=0; --i) { //保证数组中的数据有序
if (elements[i] > e) {
elements[i+1] = elements[i];
} else {
break;
}
}
elements[i+1] = e;
++size;
}
}public class Example {
public static void test(DynamicArray dynamicArray) {
dynamicArray.add(5);
dynamicArray.add(1);
dynamicArray.add(3);
for (int i = 0; i < dynamicArray.size(); ++i) {
System.out.println(dynamicArray.get(i));
}
}
public static void main(String args[]) {
DynamicArray dynamicArray = new SortedDynamicArray();
test(dynamicArray); // 打印结果:1、3、5
}
}多态这种特性也需要编程语言提供特殊的语法机制来实现
DynamicArray dynamicArray = new SortedDynamicArray();对于多态特性的实现方式,除了利用“继承加方法重写”这种实现方式之外,还有其他两种比较常见的的实现方式,一个是利用接口类语法,另一个是利用 duck-typing 语法。
不过,并不是每种编程语言都支持接口类或者 duck-typing 这两种语法机制,比如C++就不支持接口类语法,而 duck-typing 只有一些动态语言才支持,比如 Python、JavaScript 等。
利用接口类来实现多态特性
public interface Iterator {
boolean hasNext();
String next();
String remove();
}
public class Array implements Iterator {
private String[] data;
public boolean hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}
public class LinkedList implements Iterator {
private LinkedListNode head;
public boolean hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}public class Demo {
private static void print(Iterator iterator) {
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
public static void main(String[] args) {
Iterator arrayIterator = new Array();
print(arrayIterator);
Iterator linkedListIterator = new LinkedList();
print(linkedListIterator);
}
}Iterator 是一个接口类,定义了一个可以遍历集合数据的迭代器。Array 和 LinkedList 都实现了接口类 Iterator
用 duck-typing 来实现多态特性(Python代码)
class Logger:
def record(self):
print("I write a log into file.")
class DB:
def record(self):
print("I insert data into db. ")
def test(recorder):
recorder.record()
def demo():
logger = Logger()
db = DB()
test(logger)
test(db)duck-typing 实现多态的方式非常灵活。Logger 和 DB 两个类没有任何关系,但是只要都有定义了 record() 方法,就可以被传递到 test()方法中,在实际运行的时候,执行对应的 record() 方法
也就是说,只要两个类具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系,这就是所谓的 duck-typing,是一些动态语言所特有的语法机制。而像 Java 这样的静态语言,通过继承实现多态特性,必须要求两个类之间有继承关系,通过接口实现多态特性,类必须实现对应的接口
多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里氏替换原则、利用多态去掉冗长的if-else语句等等
面向过程这种编程范式随着面向对象的出现,已经慢慢退出了舞台,而函数式编程如今也已被广泛接受和采用
在过往的工作中,很多人总以为使用面向对象编程语言来做开发,就是在进行面向对象编程。但实际上只是在用面向对象编程语言,编写面向过程风格的代码而已,并没有发挥面向对象编程的优势
面向过程编程是一种编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
面向过程和面向对象最基本的区别就是,代码的组织方式不同。面向过程风格的代码被组织成了一组方法集合及其数据结构,方法和数据结构的定义是分开的。面向对象风格的代码被组织成一组类,方法和数据结构被绑定一起,定义在类中。
开发中定义完类的属性之后,就顺手把这些属性的getter、setter方法都定义上。或者 Lombok 插件自动生成所有属性的 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();
}
// ...省略其他方法...
}再来看 items 属性,定义了 getter 方法和 addItem() 方法,并没有定义它的 setter 方法。因为 items 属性的 getter 方法返回的是一个List 集合容器。外部调用者获取到这个容器之后,是可以操作容器内部数据的
ShoppingCart cart = new ShoppingCart();
...
cart.getItems().clear(); // 清空购物车但是这样的代码写法,会导致 itemsCount、totalPrice、items 三者数据不一致。不应该将清空购物车的业务逻辑暴露给上层代码。正确的做法应该是,在 ShoppingCart 类中定义一个 clear() 方法,将清空购物车的业务逻辑封装在里面,透明地给调用者使用
public class ShoppingCart {
// ...省略其他代码...
public void clear() {
items.clear();
itemsCount = 0;
totalPrice = 0.0;
}
}熟悉 Java 语言可以通过 Java 提供的 Collections.unmodifiableList() 方法,让 getter 方法返回一个不可被修改的 UnmodifiableList 集合容器,而这个容器类重写了 List 容器中跟修改数据相关的方法,比如 add()、clear() 等方法。一旦我们调用这些修改数据的方法,代码就会抛出 UnsupportedOperationException 异常,这样就避免了容器中的数据被修改
public class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E> {
public boolean add(E e) {
throw new UnsupportedOperationException();
}
public void clear() {
throw new UnsupportedOperationException();
}
// ...省略其他代码...
}public class ShoppingCart {
// ...省略其他代码...
public List<ShoppingCartItem> getItems() {
return Collections.unmodifiableList(this.items);
}
}
ShoppingCart cart = new ShoppingCart();
List<ShoppingCartItem> items = cart.getItems();
items.clear();//抛出UnsupportedOperationException异常但是这样的实现还是有点问题。因为当调用者通过 ShoppingCart 的 getItems() 获取到 items 之后,虽然没法修改容器中的数据,但仍然可以修改容器中每个对象(ShoppingCartItem)的数据
ShoppingCart cart = new ShoppingCart();
cart.add(new ShoppingCartItem(...));
List<ShoppingCartItem> items = cart.getItems();
ShoppingCartItem item = items.get(0);
item.setPrice(19.0); // 这里修改了item的价格属性这个问题该如何解决呢?在后面讲到设计模式的时候,还会详细地讲到
在面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。
单例类对象在全局代码中只有一份,所以它相当于一个全局变量。静态成员变量归属于类上的数据,被所有的实例化对象所共享,也相当于一定程度上的全局变量。而常量是一种非常常见的全局变量,比如一些代码中的配置参数,一般都设置为常量,放到一个 Constants 类中。静态方法一般用来操作静态变量或者外部数据。
静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。
在刚刚介绍的这些全局变量和全局方法中,Constants 类和 Utils 类最常用
常见的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 类的设计
实际上如果是基于 MVC 三层结构做 Web 方面的后端开发,这样的代码你可能天天都在写。
传统的 MVC 结构分为 Model 层、Controller 层、View 层这三层。但在前后端分离之后,三层结构在后端开发中,会稍微有些调整,被分为 Controller 层、Service 层、Repository 层
而在每一层中,又会定义相应的 VO(View Object)、BO(Business Object)、Entity。一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。这就是典型的面向过程的编程风格
实际上这种开发模式叫作基于贫血模型的开发模式,也是现在非常常用的一种 Web 项目的开发模式
| 特性 | 旧版(Java 8 时代) | 当前(Java 21) |
|---|---|---|
| 数据类 | POJO + getter/setter | record(不可变数据类,自动生成 equals/hashCode/toString) |
| 继承层级 | class 任意继承 | sealed class 显式限定继承层级(更安全) |
| 类型判断 | instanceof 强转 | instanceof 模式匹配 + switch 模式匹配(更简洁) |
| 贫血模型 | 广泛使用 | 依然主流;可用 record 表达 VO/BO/Entity 数据 |
面向对象四大特性与贫血模型开发模式不随版本变化;Java 21 的 record、sealed、模式匹配从语言层面对 OOP 做了增强和补充。