面向对象编程(OOP)
一、OOP 基础认知
1.1 词源与本义
Object 来自拉丁语 objectum,由 ob-(向前)+ jacere(扔)构成,字面意思是"扔到前面的东西"。
哲学含义:客体——与主体(subject)相对,是被认知、被作用的对象。
移植到编程中:
对象(Object)是一个自包含的实体,它封装了状态(数据)和行为(方法),可以接收消息、处理数据,并与其他对象交互。
OOP(Object-Oriented Programming):面向对象编程——以对象为核心组织程序的编程范式。
1.2 核心思想
OOP 的本质只有一句话:
把程序组织成一组相互交互的对象,每个对象封装了自己的状态和行为。
展开来说:
- 封装:把数据和操作数据的方法打包在一起,对外隐藏内部实现
- 抽象:只暴露必要的接口,隐藏复杂的实现细节
- 继承:子类可以复用父类的属性和方法,并扩展新功能
- 多态:同一接口可以有不同的实现,调用时自动选择合适的实现
记忆口诀:封装、继承、多态、抽象——OOP 四大支柱。
1.3 为什么需要 OOP
OOP 诞生之前,主流是过程式编程——程序由一系列函数调用组成,数据和函数是分离的。
随着软件规模越来越大,过程式编程暴露出问题:
- 数据不安全:任何函数都可以修改任何数据,难以追踪状态变化
- 复用困难:代码复用主要靠复制粘贴,维护成本高
- 扩展困难:新增功能往往需要修改大量已有代码
- 难以建模:现实世界是由"事物"组成的,过程式的"函数 + 数据"模型与现实脱节
OOP 的解决方案:
- 把数据和操作数据的方法绑定在一起(封装)
- 通过继承实现代码复用
- 通过多态实现灵活扩展
- 用对象直接建模现实世界的实体
1.4 历史发展
| 年份 | 里程碑 | 意义 |
|---|---|---|
| 1967 | Simula 67 | OOP 的鼻祖,首次引入类、对象、继承、子类型。最初用于模拟离散事件 |
| 1972 | Smalltalk | 第一个纯 OOP 语言,"一切皆对象"。Alan Kay 发明了"面向对象"这个词 |
| 1979 | C++ | Bjarne Stroustrup 在 C 语言基础上加入 OOP 特性。OOP 进入主流 |
| 1995 | Java | Sun 推出,"一次编写,到处运行"。推动 OOP 成为行业标准 |
| 1995 | Python / Ruby | 动态 OOP 语言的代表,更灵活、更简洁 |
| 2000s | C# / Scala / Kotlin | 多范式语言,OOP 为主体,融合函数式特性 |
二、核心概念详解
2.1 对象(Object)
定义
对象是状态和行为的封装体,是 OOP 程序的基本构建单元。
组成
| 组成 | 说明 | 举例 |
|---|---|---|
| 状态(State) | 对象的数据,用属性/字段表示 | 汽车的颜色、速度、油量 |
| 行为(Behavior) | 对象能做什么,用方法表示 | 汽车的启动、加速、刹车 |
| 标识(Identity) | 对象的唯一身份,即使状态相同也是不同对象 | 两辆一模一样的车,仍然是两辆车 |
举例
// 一个汽车对象的概念模型
汽车对象
├── 状态
│ ├── 颜色 = "红色"
│ ├── 速度 = 0
│ └── 油量 = 100
└── 行为
├── 启动()
├── 加速()
├── 刹车()
└── 加油()
2.2 类(Class)
定义
类是创建对象的模板/蓝图,定义了一类对象共同的属性和方法。
对象是类的实例(instance),类是对象的类型(type)。
类与对象的关系
类(蓝图) 对象(实例)
───────── ──────────
汽车 ──────────→ 红色的汽车
蓝色的汽车
白色的汽车
- 一个类可以创建多个对象
- 每个对象都有自己的独立状态
- 所有对象共享相同的方法(代码复用)
举例
// 类:汽车的蓝图
public class Car {
// 属性(状态)
private String color;
private int speed;
private int fuel;
// 构造方法:创建对象时调用
public Car(String color) {
this.color = color;
this.speed = 0;
this.fuel = 100;
}
// 方法(行为)
public void accelerate(int amount) {
if (fuel > 0) {
speed += amount;
fuel -= 1;
}
}
public void brake(int amount) {
speed = Math.max(0, speed - amount);
}
public void refuel(int amount) {
fuel = Math.min(100, fuel + amount);
}
// Getter:只读访问属性
public int getSpeed() {
return speed;
}
}
// 创建对象(实例化)
Car myCar = new Car("红色");
Car yourCar = new Car("蓝色");
myCar.accelerate(30); // myCar 速度变为 30
yourCar.accelerate(50); // yourCar 速度变为 50
// 两个对象状态独立,互不影响
2.3 封装(Encapsulation)
词源
Encapsulation 来自拉丁语 capsula(小盒子),en-(进入)+ capsula(盒子)= 装进盒子里。
字面意思:把东西装进盒子里,对外只露出必要的接口。
定义
封装是把数据和操作数据的方法绑定在一起,并对外隐藏内部实现细节的机制。
核心原则
- 数据隐藏(Data Hiding):内部状态不对外直接暴露
- 接口最小化:只暴露必要的公共方法
- 受控访问:外部只能通过公共方法与对象交互
为什么需要封装
- 数据安全:防止外部随意修改内部状态,保证状态一致性
- 降低耦合:外部不依赖内部实现,内部修改不影响外部
- 简化使用:使用者只需要知道接口,不需要知道实现细节
- 便于维护:实现可以随时替换,只要接口不变
实现方式:访问控制
Java 的四种访问修饰符:
| 修饰符 | 同类 | 同包 | 子类 | 全局 | 说明 |
|---|---|---|---|---|---|
| private | ✓ | ✗ | ✗ | ✗ | 最严格,只有自己能访问 |
| default(包级私有) | ✓ | ✓ | ✗ | ✗ | 同一个包内可以访问 |
| protected | ✓ | ✓ | ✓ | ✗ | 同包和子类可以访问 |
| public | ✓ | ✓ | ✓ | ✓ | 最宽松,所有人都能访问 |
举例:封装的好坏对比
// 坏的封装:属性直接暴露
public class BadBankAccount {
public double balance; // 公开的余额,任何人都能直接改
}
// 使用:可以随意修改,非常危险
BadBankAccount account = new BadBankAccount();
account.balance = 999999; // 直接改成一百万,绕过了所有业务规则
// 好的封装:属性私有,通过方法访问
public class GoodBankAccount {
private double balance; // 私有的余额,外部不能直接改
// 存款:有校验逻辑
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("存款金额必须为正");
}
balance += amount;
}
// 取款:有校验逻辑
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("取款金额必须为正");
}
if (amount > balance) {
throw new IllegalStateException("余额不足");
}
balance -= amount;
}
// 查询:只读
public double getBalance() {
return balance;
}
}
// 使用:只能通过受控的方法操作
GoodBankAccount account = new GoodBankAccount();
account.deposit(1000); // 正常存款
account.withdraw(500); // 正常取款
// account.balance = 999999; // 编译错误,无法直接修改
封装的设计原则
把所有属性设为 private,只暴露必要的 public 方法。
记住:封装不是为了藏东西,是为了控制。 控制数据的访问方式,保证状态的一致性。
2.4 继承(Inheritance)
词源
Inheritance 来自拉丁语 hereditas(继承、遗产)。
字面意思:子类从父类那里"继承"属性和方法,就像子女继承父母的遗产。
定义
继承是一种机制,子类(派生类)可以继承父类(基类/超类)的属性和方法,并在此基础上添加新的属性和方法,或重写已有的方法。
为什么需要继承
- 代码复用:子类直接复用父类的代码,不用重复写
- 层次建模:用"is-a"关系建模现实世界的分类
- 多态基础:继承是实现多态的前提
核心概念
| 概念 | 说明 |
|---|---|
| 父类(Superclass / Base Class) | 被继承的类,也叫基类、超类 |
| 子类(Subclass / Derived Class) | 继承父类的类,也叫派生类 |
| extends | Java 中表示继承的关键字 |
| is-a 关系 | 继承表示"是一种"的关系。例如:狗是一种动物 |
| 方法重写(Override) | 子类重新实现父类的方法 |
| super | 引用父类的成员或调用父类构造方法 |
举例
// 父类:动物
public class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + " 在吃东西");
}
public void sleep() {
System.out.println(name + " 在睡觉");
}
public void makeSound() {
System.out.println(name + " 发出声音");
}
}
// 子类:狗,继承自动物
public class Dog extends Animal {
private String breed; // 子类新增的属性
public Dog(String name, int age, String breed) {
super(name, age); // 调用父类构造方法
this.breed = breed;
}
// 子类新增的方法
public void fetch() {
System.out.println(name + " 在捡球");
}
// 方法重写:狗叫的方式和一般动物不同
@Override
public void makeSound() {
System.out.println(name + " 汪汪汪!");
}
}
// 子类:猫,继承自动物
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
public void scratch() {
System.out.println(name + " 在磨爪子");
}
@Override
public void makeSound() {
System.out.println(name + " 喵喵喵~");
}
}
// 使用
Dog dog = new Dog("旺财", 3, "金毛");
dog.eat(); // 继承自 Animal:旺财 在吃东西
dog.sleep(); // 继承自 Animal:旺财 在睡觉
dog.makeSound(); // 重写后的方法:旺财 汪汪汪!
dog.fetch(); // 子类特有方法:旺财 在捡球
Cat cat = new Cat("咪咪", 2);
cat.eat(); // 咪咪 在吃东西
cat.makeSound(); // 咪咪 喵喵喵~
cat.scratch(); // 咪咪 在磨爪子
继承的层次结构
Animal(动物)
/ \
Dog Cat(猫)
/ \
Golden Husky
Retriever(哈士奇)
(金毛)
方法重写(Override)vs 方法重载(Overload)
| 对比项 | 方法重写(Override) | 方法重载(Overload) |
|---|---|---|
| 定义 | 子类重新实现父类的方法 | 同一个类中有多个同名方法,参数不同 |
| 关系 | 父子类之间 | 同一个类内部 |
| 方法签名 | 必须完全相同 | 必须不同(参数数量/类型/顺序) |
| 返回值 | 必须相同(或是子类型) | 无关 |
| 多态 | 运行时多态 | 编译时多态 |
| 注解 | @Override | 不需要 |
// 方法重载:同一个类,同名不同参
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
继承的注意事项
- Java 是单继承:一个类只能继承一个父类(但可以实现多个接口)
- 构造方法不能继承:子类必须调用父类构造方法(
super()) - private 成员不能继承:父类的私有成员子类无法直接访问
- 继承是强耦合:父类变化会影响所有子类
- 慎用继承:优先用组合(has-a)代替继承(is-a)
组合优于继承
设计原则:优先使用组合(composition)而非继承(inheritance)。
继承是is-a关系(是一种),组合是has-a关系(有一个)。
// 继承方式:强耦合
public class Stack extends ArrayList {
// 问题:ArrayList 的所有方法都被继承了,
// 包括 push/pop 之外的方法,破坏了栈的语义
}
// 组合方式:松耦合
public class Stack {
private ArrayList list; // 持有一个列表对象
public void push(Object item) {
list.add(item);
}
public Object pop() {
return list.remove(list.size() - 1);
}
// 只暴露栈需要的方法,内部实现可以随时替换
}
2.5 多态(Polymorphism)
词源
Polymorphism 来自希腊语 polymorphos:poly-(多)+ morphe(形态)。
字面意思:多种形态——同一个接口,不同的实现。
定义
多态是指同一接口可以有不同的实现,程序在运行时根据对象的实际类型自动选择合适的实现。
简单说:同一个方法调用,不同对象有不同的行为。
多态的类型
| 类型 | 说明 | 举例 |
|---|---|---|
| 编译时多态(静态多态) | 编译时确定调用哪个方法 | 方法重载(Overload) |
| 运行时多态(动态多态) | 运行时确定调用哪个方法 | 方法重写(Override)+ 向上转型 |
通常说 OOP 的多态,指的是运行时多态。
多态的三个必要条件
- 继承:要有父子类关系(或接口实现关系)
- 重写:子类要重写父类的方法
- 向上转型:父类引用指向子类对象
举例
// 父类
public class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
// 子类1
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪!");
}
}
// 子类2
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵喵~");
}
}
// 多态演示
public class PolymorphismDemo {
public static void main(String[] args) {
// 向上转型:父类引用指向子类对象
Animal animal1 = new Dog(); // Animal 引用,实际是 Dog 对象
Animal animal2 = new Cat(); // Animal 引用,实际是 Cat 对象
// 同一个方法调用,不同的行为
animal1.makeSound(); // 输出:汪汪汪!
animal2.makeSound(); // 输出:喵喵喵~
// 多态的威力:可以统一处理不同类型的对象
Animal[] animals = { new Dog(), new Cat(), new Dog() };
for (Animal animal : animals) {
animal.makeSound(); // 自动调用对应类型的方法
}
}
}
多态的好处
- 扩展性强:新增子类不需要修改已有代码
- 统一接口:用父类/接口统一处理不同实现
- 降低耦合:调用方只依赖父类/接口,不依赖具体实现
- 符合开闭原则:对扩展开放,对修改关闭
向上转型与向下转型
// 向上转型(Upcasting):子类 → 父类,自动转换,安全
Animal animal = new Dog(); // 自动转型,没问题
// 向下转型(Downcasting):父类 → 子类,强制转换,有风险
Dog dog = (Dog) animal; // 强制转型,需要确保实际类型是 Dog
// 安全的向下转型:先检查类型
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.fetch(); // 调用子类特有方法
}
// Java 16+ 模式匹配:更简洁
if (animal instanceof Dog dog) {
dog.fetch(); // 直接用 dog,不需要强转
}
动态绑定(Dynamic Binding)
多态的实现机制是动态绑定(也叫晚绑定):
- 编译时:只检查方法是否存在(基于引用类型)
- 运行时:根据对象的实际类型找到对应的方法并调用
这就是为什么
Animal animal = new Dog(); animal.makeSound();调用的是 Dog 的方法——运行时绑定到实际类型。
2.6 抽象(Abstraction)
词源
Abstraction 来自拉丁语 abstractio,abs-(离开)+ trahere(拉)= 抽离出来。
字面意思:从具体事物中抽取出本质特征,忽略非本质细节。
定义
抽象是只展示必要的信息,隐藏实现细节的过程。它关注"对象能做什么",而不关注"怎么做"。
抽象 vs 封装
| 概念 | 关注点 | 目的 |
|---|---|---|
| 抽象 | 从外部看,对象提供什么功能 | 简化使用,降低复杂度 |
| 封装 | 从内部看,如何隐藏实现细节 | 保护数据,降低耦合 |
抽象是设计层面的(提供什么接口),封装是实现层面的(怎么隐藏内部)。封装是实现抽象的手段。
抽象的实现方式
Java 中有两种实现抽象的机制:
1. 抽象类(Abstract Class)
// 抽象类:不能实例化,只能被继承
public abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// 抽象方法:只有声明,没有实现
// 子类必须实现(除非子类也是抽象类)
public abstract double area();
public abstract double perimeter();
// 普通方法:可以有实现
public String getColor() {
return color;
}
}
// 具体子类:必须实现所有抽象方法
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}
2. 接口(Interface)
// 接口:定义一组行为契约
public interface Drawable {
void draw(); // 抽象方法(默认 public abstract)
default void info() { // 默认方法(Java 8+)
System.out.println("这是一个可绘制的对象");
}
}
public interface Resizable {
void resize(double factor);
}
// 类可以实现多个接口(Java 的多继承通过接口实现)
public class Circle implements Drawable, Resizable {
private double radius;
@Override
public void draw() {
System.out.println("画一个圆,半径:" + radius);
}
@Override
public void resize(double factor) {
radius *= factor;
}
}
抽象类 vs 接口
| 对比项 | 抽象类 | 接口 |
|---|---|---|
| 关键字 | abstract class | interface |
| 继承 | 单继承(一个类只能继承一个抽象类) | 多实现(一个类可以实现多个接口) |
| 构造方法 | 可以有 | 不能有 |
| 字段 | 可以有各种字段 | 只能有 public static final 常量 |
| 方法实现 | 可以有抽象方法和具体方法 | Java 8 前只有抽象方法;Java 8+ 可有 default/static 方法 |
| 设计目的 | 代码复用 + 抽象 | 定义行为契约(能力) |
| 关系 | "is-a"(是一种) | "can-do"(能做什么) |
什么时候用抽象类,什么时候用接口
- 用抽象类:几个类有很多共同代码,你想复用这些代码,并且它们之间有明确的"is-a"关系
- 用接口:你想定义一组能力/契约,不同的类可以以不同方式实现这些能力,而且这些类可能没有共同的父类
经验法则:抽象类是"是什么",接口是"能做什么"。
三、OOP 设计原则
3.1 SOLID 原则
SOLID 是五个面向对象设计原则的缩写,由 Robert C. Martin("Bob 大叔")提出。
| 缩写 | 原则 | 英文 | 核心思想 |
|---|---|---|---|
| S | 单一职责原则 | Single Responsibility Principle | 一个类只做一件事 |
| O | 开闭原则 | Open/Closed Principle | 对扩展开放,对修改关闭 |
| L | 里氏替换原则 | Liskov Substitution Principle | 子类必须能替换父类 |
| I | 接口隔离原则 | Interface Segregation Principle | 接口要小而专,不要大而全 |
| D | 依赖倒置原则 | Dependency Inversion Principle | 依赖抽象,不依赖具体 |
S — 单一职责原则(SRP)
一个类应该只有一个引起它变化的原因。
换句话说:一个类只负责一件事。
// 违反 SRP:一个类干了三件事
public class UserService {
public void registerUser(String name, String email) {
// 1. 业务逻辑:校验、注册
if (name == null || email == null) throw new IllegalArgumentException();
saveToDatabase(name, email);
sendWelcomeEmail(email);
}
private void saveToDatabase(String name, String email) {
// 2. 数据访问:操作数据库
}
private void sendWelcomeEmail(String email) {
// 3. 邮件发送:发邮件
}
}
// 遵循 SRP:拆分成三个类,各管各的
public class UserService { // 业务逻辑
private UserRepository repository;
private EmailService emailService;
public void registerUser(String name, String email) {
if (name == null || email == null) throw new IllegalArgumentException();
repository.save(name, email);
emailService.sendWelcome(email);
}
}
public class UserRepository { // 数据访问
public void save(String name, String email) { ... }
}
public class EmailService { // 邮件发送
public void sendWelcome(String email) { ... }
}
好处:
- 每个类职责清晰,容易理解
- 修改一个功能不影响其他功能
- 代码更容易测试和维护
O — 开闭原则(OCP)
软件实体(类、模块、函数)应该对扩展开放,对修改关闭。
换句话说:新增功能应该通过添加新代码实现,而不是修改已有代码。
// 违反 OCP:每加一种形状就要修改这个类
public class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.radius * c.radius;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.width * r.height;
}
// 加三角形?又要改这里...
return 0;
}
}
// 遵循 OCP:用多态,新增形状只需要加新类
public interface Shape {
double area();
}
public class Circle implements Shape {
private double radius;
public double area() { return Math.PI * radius * radius; }
}
public class Rectangle implements Shape {
private double width, height;
public double area() { return width * height; }
}
// 新增三角形:只加新类,不用修改已有代码
public class Triangle implements Shape {
private double base, height;
public double area() { return 0.5 * base * height; }
}
public class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.area(); // 永远不用改
}
}
实现方式:多态、接口、抽象类
L — 里氏替换原则(LSP)
子类必须能够替换掉它的父类,而不改变程序的正确性。
换句话说:用父类的地方,换成子类也应该正常工作。
// 违反 LSP:正方形不是矩形的子类?
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 正方形宽高相等,同时改
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
// 问题:用 Rectangle 的地方换成 Square 会出 bug
public void test(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.getArea() == 20; // Square 的话面积是 16,断言失败!
}
正确做法:正方形和矩形是并列关系,不是继承关系。可以用一个共同的父类/接口。
public interface Shape {
int getArea();
}
public class Rectangle implements Shape { ... }
public class Square implements Shape { ... }
LSP 的本质:继承不能破坏父类的契约(前置条件、后置条件、不变量)。子类只能扩展,不能改变父类的行为约定。
I — 接口隔离原则(ISP)
客户端不应该被迫依赖它不需要的接口。
换句话说:接口要小而专,不要大而全。胖接口要拆成多个小接口。
// 违反 ISP:一个大而全的接口
public interface Worker {
void work();
void eat();
void sleep();
void attendMeeting();
}
// 人类员工:所有方法都需要
public class HumanWorker implements Worker {
public void work() { ... }
public void eat() { ... }
public void sleep() { ... }
public void attendMeeting() { ... }
}
// 机器人员工:不需要吃饭睡觉开会,但被迫实现
public class RobotWorker implements Worker {
public void work() { ... }
public void eat() { /* 机器人不吃饭,但必须实现 */ }
public void sleep() { /* 机器人不睡觉,但必须实现 */ }
public void attendMeeting() { /* 机器人不开会,但必须实现 */ }
}
// 遵循 ISP:拆成多个小接口
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public interface MeetingAttendable {
void attendMeeting();
}
// 人类:实现需要的接口
public class HumanWorker implements Workable, Eatable, Sleepable, MeetingAttendable {
// 四个方法都实现
}
// 机器人:只实现需要的接口
public class RobotWorker implements Workable {
public void work() { ... } // 只需要实现 work
}
好处:
- 接口职责单一,更清晰
- 实现类不会被迫实现不需要的方法
- 修改一个接口不影响不相关的客户端
D — 依赖倒置原则(DIP)
高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
换句话说:依赖抽象(接口/抽象类),不要依赖具体实现。
// 违反 DIP:高层依赖低层
public class LightBulb { // 低层模块:灯泡
public void turnOn() { ... }
public void turnOff() { ... }
}
public class Switch { // 高层模块:开关
private LightBulb bulb; // 直接依赖具体的灯泡
public Switch(LightBulb bulb) {
this.bulb = bulb;
}
public void toggle() {
// 开/关逻辑
}
}
// 问题:Switch 只能控制 LightBulb,不能控制其他电器
// 遵循 DIP:两者都依赖抽象
public interface Switchable { // 抽象:可开关的
void turnOn();
void turnOff();
}
public class LightBulb implements Switchable { // 低层依赖抽象
public void turnOn() { ... }
public void turnOff() { ... }
}
public class Fan implements Switchable { // 新增风扇,也实现接口
public void turnOn() { ... }
public void turnOff() { ... }
}
public class Switch { // 高层也依赖抽象
private Switchable device; // 依赖接口,不依赖具体类
public Switch(Switchable device) {
this.device = device;
}
public void toggle() {
// 开/关逻辑,对所有 Switchable 都管用
}
}
好处:
- 高层和低层解耦
- 容易替换实现(换灯泡 → 换风扇,不用改开关)
- 更容易测试(可以用 mock 对象)
DIP 是**依赖注入(Dependency Injection)和控制反转(Inversion of Control)**的理论基础。DIP、DI 与 IoC 在 Spring Framework 中的工程化落地见 Spring Framework 核心体系。
3.2 其他重要原则
DRY 原则(Don't Repeat Yourself)
不要重复自己。 相同的代码不要写两遍,应该抽取成公共方法/类。
KISS 原则(Keep It Simple, Stupid)
保持简单。 简单的设计比复杂的设计更好。
YAGNI 原则(You Aren't Gonna Need It)
你不会需要它。 不要为了"将来可能用到"而提前写代码,需要的时候再加。
迪米特法则(Law of Demeter / 最少知识原则)
一个对象应该对其他对象有最少的了解。 只和直接朋友说话,不和陌生人说话。
// 违反迪米特法则:链式调用,知道太多内部结构
String city = person.getAddress().getCity().getName();
// 遵循迪米特法则:封装一层,只和直接朋友交互
String city = person.getCityName();
// Person 内部实现 getCityName(),外部不需要知道 Address 的结构
四、设计模式概述
4.1 什么是设计模式
设计模式是在特定场景下,解决特定设计问题的通用、可复用的解决方案。
简单说:设计模式是前人总结的最佳实践套路,是 OOP 设计的"经验公式"。
注意:
- 设计模式不是代码模板,是设计思路
- 设计模式不是银弹,该用才用,不要为了用模式而用模式
- 设计模式是语言无关的,任何 OOP 语言都可以用
4.2 GoF 23 种设计模式
GoF(Gang of Four,四人帮)在《设计模式》一书中总结了 23 种经典设计模式,分为三大类:
创建型模式(5 种)—— 怎么创建对象
| 模式 | 用途 | 一句话说明 |
|---|---|---|
| 单例模式(Singleton) | 确保一个类只有一个实例 | 全局唯一的对象 |
| 工厂方法模式(Factory Method) | 定义创建对象的接口,由子类决定实例化哪个类 | 把 new 的决定权交给子类 |
| 抽象工厂模式(Abstract Factory) | 创建一系列相关或相互依赖的对象 | 工厂的工厂,创建一族产品 |
| 建造者模式(Builder) | 分步构建复杂对象 | 一步一步拼出复杂对象 |
| 原型模式(Prototype) | 通过复制已有对象创建新对象 | 克隆一个对象 |
结构型模式(7 种)—— 怎么组织类和对象
| 模式 | 用途 | 一句话说明 |
|---|---|---|
| 适配器模式(Adapter) | 把一个接口转换成客户端期望的另一个接口 | 转接头,不兼容的接口变兼容 |
| 桥接模式(Bridge) | 将抽象与实现分离,使它们可以独立变化 | 用组合代替继承,解耦两个维度 |
| 组合模式(Composite) | 将对象组合成树形结构,统一处理单个对象和组合 | 树形结构,叶子和容器一视同仁 |
| 装饰器模式(Decorator) | 动态地给对象添加额外的职责 | 套壳子,一层一层加功能 |
| 外观模式(Facade) | 为子系统的一组接口提供一个统一的高层接口 | 统一入口,简化复杂子系统的使用 |
| 享元模式(Flyweight) | 共享细粒度对象,减少内存占用 | 复用对象,节省内存 |
| 代理模式(Proxy) | 为其他对象提供一个代理以控制对这个对象的访问 | 替身,控制访问真实对象 |
行为型模式(11 种)—— 对象之间怎么交互
| 模式 | 用途 | 一句话说明 |
|---|---|---|
| 责任链模式(Chain of Responsibility) | 将请求沿链传递,直到有对象处理它 | 接力棒,一个传一个 |
| 命令模式(Command) | 将请求封装为对象,支持排队、记录、撤销 | 把请求变成对象,可保存可撤销 |
| 解释器模式(Interpreter) | 定义语言的文法,解释语言中的句子 | 写个简单的解释器 |
| 迭代器模式(Iterator) | 顺序访问集合对象的元素,不暴露内部表示 | 遍历集合的统一方式 |
| 中介者模式(Mediator) | 用中介对象封装一系列对象的交互 | 中间人,大家都通过它通信 |
| 备忘录模式(Memento) | 保存和恢复对象的状态 | 快照,后悔药 |
| 观察者模式(Observer) | 一对多的依赖关系,状态变化自动通知 | 订阅-通知,状态变了自动推 |
| 状态模式(State) | 允许对象在内部状态改变时改变它的行为 | 状态机,状态变了行为也变 |
| 策略模式(Strategy) | 定义一系列算法,封装起来,使它们可以互换 | 算法可替换,换策略不改代码 |
| 模板方法模式(Template Method) | 定义算法骨架,子类实现具体步骤 | 骨架定好,细节子类填 |
| 访问者模式(Visitor) | 在不改变类的前提下增加新操作 | 操作和数据结构分离 |
4.3 最常用的设计模式
如果只学 5 个,优先学这些:
- 单例模式—— 全局唯一实例
- 工厂模式—— 解耦对象创建
- 策略模式—— 算法可替换
- 观察者模式—— 事件通知机制
- 装饰器模式—— 动态添加功能
设计模式的学习方法:先理解每个模式解决什么问题、适用什么场景,再看代码示例。不要死记硬背,理解了自然就会用。
详细内容查看设计模式理论与实战手册
五、OOP 与其他范式的关系
5.1 OOP 的定位
OOP 是结构维度的编程范式,它回答的是"代码怎么组织"的问题。
它可以和不同的执行模型范式组合:
| 组合 | 代表语言 | 说明 |
|---|---|---|
| OOP + 命令式 | Java、C++、C#、Python | 主流 OOP,方法内部是命令式代码 |
| OOP + 函数式 | Scala、OCaml、CLOS、F# | 对象不可变,方法是纯函数 |
5.2 OOP vs 过程式
| 对比项 | 过程式 | 面向对象 |
|---|---|---|
| 核心单元 | 函数(过程) | 对象 |
| 数据与行为 | 分离的 | 封装在一起 |
| 组织方式 | 按功能划分 | 按对象划分 |
| 复用方式 | 函数调用、复制粘贴 | 继承、组合、多态 |
| 状态管理 | 全局/局部变量,分散 | 封装在对象内部,受控 |
| 适用场景 | 小型程序、算法密集型 | 大型程序、业务建模 |
5.3 OOP vs 函数式
| 对比项 | 面向对象 | 函数式 |
|---|---|---|
| 核心单元 | 对象 | 函数 |
| 状态 | 封装的可变状态 | 不可变数据 |
| 复用方式 | 继承、组合 | 高阶函数、组合子 |
| 多态 | 子类型多态(继承) | 参数多态(泛型) |
| 并发 | 需要锁、同步 | 天然线程安全 |
| 建模方式 | 用对象建模实体 | 用函数建模变换 |
| 扩展方向 | 新增类型(加子类) | 新增操作(加函数) |
表达式问题(Expression Problem):
- OOP 容易加新类型(加子类),难加新操作(要改所有类)
- 函数式容易加新操作(加函数),难加新类型(要改所有函数)
- 这是两种范式的根本差异,没有谁优谁劣,只是侧重点不同。
5.4 现代趋势:多范式融合
现代语言的趋势是多范式融合——不再是纯 OOP 或纯函数式,而是取两者之长:
- Java 8+:OOP 主体 + Lambda/Stream/Record/模式匹配
- Kotlin:OOP + 函数式 + 空安全 + 协程
- Scala:深度融合 OOP 和函数式
- Python:OOP + 函数式 + 动态特性
未来的方向不是"OOP vs 函数式",而是"OOP + 函数式"——用对象组织结构,用函数式思想处理数据变换。
六、OOP 的优缺点
6.1 优点
- 建模能力强:用对象直接建模现实世界,符合人类直觉
- 代码复用:继承、组合、多态提供了强大的复用机制
- 可维护性好:封装降低了耦合,修改一个类不影响其他类
- 扩展性强:多态和开闭原则让新增功能变得容易
- 适合大型项目:清晰的组织结构便于团队协作和代码管理
- 生态丰富:大量的框架、库、设计模式都是 OOP 的
6.2 缺点
- 学习曲线陡:封装、继承、多态、抽象这些概念对初学者不直观
- 代码量多:类、接口、继承层次... 简单问题可能被过度设计
- 性能开销:动态绑定、对象创建、垃圾回收有额外开销
- 过度设计风险:为了用 OOP 而用 OOP,简单问题复杂化
- 继承的陷阱:继承是强耦合,父类变化影响所有子类
- 状态管理复杂:对象状态分散在各处,调试和追踪困难
6.3 什么时候用 OOP
适合用 OOP 的场景:
- 大型、复杂的业务系统
- 需要建模现实世界实体的场景
- 需要高度可扩展性的系统
- 团队协作开发的项目
- GUI 应用(天然的对象模型)
不适合用 OOP 的场景:
- 简单的脚本/工具
- 性能极度敏感的底层代码
- 纯算法/数学计算
- 数据处理管道(函数式更合适)
记住:范式是工具,不是教条。 选最适合问题的范式,而不是最熟悉的范式。
七、OOP 实践建议
7.1 设计类的原则
- 高内聚,低耦合
- 高内聚:一个类的所有方法都围绕同一个职责
- 低耦合:类之间的依赖越少越好
- 优先用组合,而非继承
- 继承是强耦合,组合是松耦合
- 只有明确的"is-a"关系才用继承
- 面向接口编程,而非面向实现编程
- 依赖抽象,不依赖具体类
- 变量类型尽量用接口/抽象类
- 封装变化
- 找出会变化的部分,把它们封装起来
- 变化的部分和不变的部分分离
7.2 常见的反模式
| 反模式 | 说明 | 怎么避免 |
|---|---|---|
| 上帝类(God Class) | 一个类什么都干,巨大无比 | 拆分,遵循 SRP |
| 霰弹手术(Shotgun Surgery) | 一个小改动要改很多类 | 重新划分职责 |
| 特性依恋(Feature Envy) | 一个方法频繁访问另一个类的方法和数据 | 把方法移到那个类 |
| 过度继承 | 继承层次太深,关系复杂 | 用组合代替继承 |
| 无用接口 | 接口只有一个实现,没有多态需求 | 删掉接口,直接用类 |
| 数据类(Data Class) | 只有字段和 getter/setter,没有行为 | 把相关行为移到类里 |
7.3 学习路径建议
第一阶段:基础
├── 理解对象、类、属性、方法
├── 掌握封装、继承、多态、抽象
└── 会用类和对象写简单程序
第二阶段:进阶
├── 理解多态的实现机制(动态绑定)
├── 掌握接口和抽象类的区别与使用
├── 理解 SOLID 原则
└── 学习常用设计模式
第三阶段:深入
├── 理解 OOP 的本质和局限性
├── 学习其他范式(函数式、响应式)
├── 掌握多范式编程
└── 形成自己的设计哲学
附录:参考资源
- 《设计模式:可复用面向对象软件的基础》 —— GoF,设计模式经典
- 《代码整洁之道》 —— Robert C. Martin,代码质量与设计原则
- 《重构:改善既有代码的设计》 —— Martin Fowler,重构与设计模式
- 《Head First 设计模式》 —— 入门级设计模式读物,通俗易懂
- 《面向对象分析与设计》 —— Grady Booch,OOAD 经典