> For the complete documentation index, see [llms.txt](https://yunzhao.gitbook.io/notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yunzhao.gitbook.io/notes/computer-science/design-patterns/oop.md).

# OOP

**面向对象编程**（OOP，Object Oriented Programming）是一种编程范式或编程风格，以类（class）和对象（object）作为组织代码的基本单元，并将封装、抽象、继承、多态四个特性作为代码设计和实现的基石。

**四大特性**封装、抽象、继承、多态，有些人也认为只有三大特性，把抽象排除在外。

**面向对象编程语言**（OOPL，Object Oriented Programming Language）是支持类或对象的语法机制，并有现成的语法机制，能方便地实现面向对象编程四大特性（封装、抽象、继承、多态）的编程语言。

按照严格的定义，很多语言都不是面 OOPL，比如 JavaScript 不支持封装和继承，Go 摒弃了继承。但是按照不严格的定义，很多语言都可以说是 OOPL，只要某种编程语言支持类或对象的语法概念，并且以此作为组织代码的基本单元，那就可以被粗略地认为它就是 OOPL 了。

OOP 一般使用 OOPL 来进行，但是，不用 OOPL，我们照样可以进行 OOP。反过来讲，即便我们使用 OOPL，写出来的代码也不一定是 OOP 风格的，也有可能是面向过程编程风格的。

**面向对象分析**（OOA，Object Oriented Analysis）和**面向对象设计**（OOA，Object Oriented Design）的产出是类的设计，包括程序被拆解成哪些类、类有哪些属性方法、类于类之间的关系。

## 四大特性

### 封装（Encapsulation）

也叫信息隐藏或数据访问保护。封装的意义：

* 提高代码的可维护性。如果对类中的属性访问不做限制，那么则类不可控，属性被随意修改，修改的逻辑散落在代码各个角落。
* 类通过仅暴露必要的操作，可以提高类的易用性，调用者不需要了解过多的细节。

### 抽象（Abstraction）

封装讲的是如何隐藏信息、保护数据；而抽象讲的是隐藏方法的具体实现，调用者只关系方法提供了哪些功能，并不需要知道这些功能是怎么实现的。

通常使用接口类或抽象类两种语法机制实现。

{% hint style="info" %}
上文讲到有时候抽象被排除在四大特性之外。原因如下：\
实现抽象不一定需要接口类或抽象类，因为类的方法是通过编程语言的函数这一语法机制本省就是抽象，使用者在调用函数的时候，并不需要知道内部逻辑。
{% endhint %}

抽象的意义：

* 抽象和封装都是处理复杂性的有效手段，面对复杂系统，人脑有限，所以必须忽略掉非关键的细节。
* 提高代码的可扩展性、维护性，修改实现不需要修改定义。

### 继承（Inheritance）

继承表示类之间 is-a 的关系。继承分为单继承和多继承。

继承最大的好处是代码复用。不过，过度使用继承会导致代码的可读性，可维护性变差，父类和子类耦合度过高。所以继承这个特性非常有争议，我们应该尽量少用。

### 多态（Polymorphism）

多态是指子类可以替换父类，在实际的代码运行过程中，调用子类的方法实现。

多态的实现一般需要三种语法机制：

* 父类对象引用可以指向子类对象。
* 支持继承。
* 子类可以重写（override）父类方法。

多态可以提高代码的可扩展性和复用性。

## 面向过程

### What？

**面向过程编程**也是一种编程范式或编程风格。它以过程（方法、函数、操作）作为组织代码的基本单元，以数据与方法相分离为最主要的特点。面向过程风格是一种**流程化**的编程风格，通过拼接一组顺序执行的方法来操作数据完成一项功能。

**面向过程编程语言**首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念，不支持丰富的面向对象编程特性（比如继承、多态、封装），仅支持面向过程编程。

### OOP 有哪些优势？

* OOP 能够应对大规模复杂程序开发。对于简单程序，面向过程、面向对象差别不大，有时面向过程更简单。
* OOP 更易复用、易扩展、易维护。由上文的四大特性可见。
* OOP 语言更加人性化，更接近于人的思维，而机器语言、汇编、面向过程是按照计算机的思维方式。

### 本质是面向过程的代码

在实际开发中，不是把代码塞到类里，就表示在面向对象编程了。很多情况下，有很多表面上像面向对象的代码，本质上时面向过程的风格。

#### 滥用 getter、setter 方法

我们经常会遇到定义完类后，随手就把所有 属性的 getter、setter 加上去，或者用 lombok 等。这样违反了面向对象封装的特性。如下面的例子：

```java
@Data
public class ShoppingCart {
  private int itemsCount;
  private double totalPrice;
  private List<ShoppingCartItem> items = new ArrayList<>();
  
  public void addItem(ShoppingCartItem item) {
    items.add(item);
    itemsCount++;
    totalPrice += item.getPrice();
  }
  // ...省略其他方法...
}
```

上面的代码有很多问题：

* items、totalPrice 定义为私有属性，但是又提供 getter、setter 方法，本质上就是 public 属性了。任何地方都可以修改这两个值，会造成和 items 的数据不一致问题。
* items 提供了 getter 方法，返回的是 List，外部拿到这个容器后，可以修改容器内数据。**解决方案**：可以使用`Collections.unmodifiableList()`。
* 就算用不可变容器，还是有问题，因为用户拿到容器后，再拿容器内的某个元素，还是可以修改元素的某个字段值。

总结一下：

* 能不暴露 setter 方法就尽量不要暴露。
* getter 方法如果返回的是容器或者类，也要注意内部的数据被修改。

#### 滥用全局变量和全局方法

全局变量：单例类对象、静态成员变量、常量，比如 Constants；全局方法：静态方法，比如 Utils。

**全局变量**

我们会见到把程序中所有的常量都集中方法一个 Constants 类中，这种大而全的常量类并不好，原因是：

* 可维护性差，一个项目有很多工程师开发，那么就都可能修改这个类，这个类就会很大，查找修改某个常量也会比较耗时，还可能引起代码冲突。
* 增加编译时间，依赖这个 Constants 的类很多，所以只要这个常量类有修改，很多类都需要重新编译。
* 复用性差，如果有另一个项目也依赖 Constants 类，但是只依赖一小部分，这样就引入了很多无关的常量。

解决方案：

* 把大的 Constants 拆分，如 MySQLConstants、RedisConstants 等。
* 不设计 Constants 类，哪个类使用了常量，就把这个常量定义在这个类中。

**全局方法**

为什么会出现 Utils 类呢？因为存在一种情况，两个类有一段逻辑是重复的，虽然继承可以解决代码复用问题，但是这两个类又有没 is-a 的关系，这个时候就出现了 Utils 类。

我们并不是要测地杜绝 Utils 类，只是不要滥用。另外，Utils 也可以细化。

#### 数据和方法分离

传统的 MVC 是 Model、Controller、View。前后端分离后，后端的三层结构变为 Controller、Service、Repository，在每一层都有对应的 VO、BO、Entity，其实这就是数据和方法分离。比如，BO 只定义了数据，而操作数据的方法都在 Service 中，这就是典型的面向过程的编程风格。

这种开发模式叫做基于**贫血模式**的开发方式。

### 面向过程的取舍

虽然上文讲了 OOP 的各种优势，也讲了哪些代码表面上是面向对象，本质上是面向过程。但是面向过程并不是完全无用武之地。

若是开发微小程序，或者做一些数据处理（算法为主、数据为辅），那么面向过程更加适合。

面向对象和面向过程可以并存的，甚至在一些标准库（JDK、Apache Commons、Guava）中都可以见到面向过程风格的代码。

所以，我们最终的目的是写出易维护、易读、易复用、易扩展的高质量代码。

## 贫血模型

MVC 架构指的是 Model、View、Controller，表示展示层、逻辑层、数据层，现在多是前后端分离，后端分为 Repository、Service、Controller 三层。

一般在写代码的时候，Entity 和 Repository 类组成数据层，Bo 和 Service 类组成业务逻辑层，Vo 和 Controller 类组成接口层。可以发现，Entity、Bo、Vo 是一个纯粹的数据结构，不包含任何业务逻辑，业务逻辑放在另一个类中，这是一种典型的面向过程的编程风格。像 Bo 这种只包含数据，不包含业务逻辑的类叫做**贫血模型**（Anemic Domain Model）。

**充血模型**（Rich Domain Model）刚好相反，数据和业务被封装在同一个类中。基于充血模型的 DDD 开发与贫血模型的主要差别在于 Service 层，DDD 的 Service 层包含Service 类和 Domain 类，Domain 类包含数据和业务逻辑，而 Service 类很单薄。

**为什么贫血模型这么受欢迎**？

* 系统业务简单，贫血模型足以应付，不需要费心思设计充血模型。
* 充血模型更有难度。
* 思维已固化，转型有成本。

**那什么项目应该考虑用充血模型呢**？

业务复杂的系统，比如包含各种利息计算模型、还款模型等复杂业务的金融系统。
