面向对象编程(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):内部状态不对外直接暴露
  • 接口最小化:只暴露必要的公共方法
  • 受控访问:外部只能通过公共方法与对象交互

为什么需要封装

  1. 数据安全:防止外部随意修改内部状态,保证状态一致性
  2. 降低耦合:外部不依赖内部实现,内部修改不影响外部
  3. 简化使用:使用者只需要知道接口,不需要知道实现细节
  4. 便于维护:实现可以随时替换,只要接口不变

实现方式:访问控制

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(继承、遗产)。

字面意思:子类从父类那里"继承"属性和方法,就像子女继承父母的遗产。

定义

继承是一种机制,子类(派生类)可以继承父类(基类/超类)的属性和方法,并在此基础上添加新的属性和方法,或重写已有的方法。

为什么需要继承

  1. 代码复用:子类直接复用父类的代码,不用重复写
  2. 层次建模:用"is-a"关系建模现实世界的分类
  3. 多态基础:继承是实现多态的前提

核心概念

概念 说明
父类(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;
    }
}

继承的注意事项

  1. Java 是单继承:一个类只能继承一个父类(但可以实现多个接口)
  2. 构造方法不能继承:子类必须调用父类构造方法(super()
  3. private 成员不能继承:父类的私有成员子类无法直接访问
  4. 继承是强耦合:父类变化会影响所有子类
  5. 慎用继承:优先用组合(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 来自希腊语 polymorphospoly-(多)+ morphe(形态)。

字面意思:多种形态——同一个接口,不同的实现。

定义

多态是指同一接口可以有不同的实现,程序在运行时根据对象的实际类型自动选择合适的实现。

简单说:同一个方法调用,不同对象有不同的行为。

多态的类型

类型 说明 举例
编译时多态(静态多态) 编译时确定调用哪个方法 方法重载(Overload)
运行时多态(动态多态) 运行时确定调用哪个方法 方法重写(Override)+ 向上转型

通常说 OOP 的多态,指的是运行时多态

多态的三个必要条件

  1. 继承:要有父子类关系(或接口实现关系)
  2. 重写:子类要重写父类的方法
  3. 向上转型:父类引用指向子类对象

举例

// 父类
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();  // 自动调用对应类型的方法
        }
    }
}

多态的好处

  1. 扩展性强:新增子类不需要修改已有代码
  2. 统一接口:用父类/接口统一处理不同实现
  3. 降低耦合:调用方只依赖父类/接口,不依赖具体实现
  4. 符合开闭原则:对扩展开放,对修改关闭

向上转型与向下转型

// 向上转型(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 来自拉丁语 abstractioabs-(离开)+ 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 个,优先学这些:

  1. 单例模式—— 全局唯一实例
  2. 工厂模式—— 解耦对象创建
  3. 策略模式—— 算法可替换
  4. 观察者模式—— 事件通知机制
  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 优点

  1. 建模能力强:用对象直接建模现实世界,符合人类直觉
  2. 代码复用:继承、组合、多态提供了强大的复用机制
  3. 可维护性好:封装降低了耦合,修改一个类不影响其他类
  4. 扩展性强:多态和开闭原则让新增功能变得容易
  5. 适合大型项目:清晰的组织结构便于团队协作和代码管理
  6. 生态丰富:大量的框架、库、设计模式都是 OOP 的

6.2 缺点

  1. 学习曲线陡:封装、继承、多态、抽象这些概念对初学者不直观
  2. 代码量多:类、接口、继承层次... 简单问题可能被过度设计
  3. 性能开销:动态绑定、对象创建、垃圾回收有额外开销
  4. 过度设计风险:为了用 OOP 而用 OOP,简单问题复杂化
  5. 继承的陷阱:继承是强耦合,父类变化影响所有子类
  6. 状态管理复杂:对象状态分散在各处,调试和追踪困难

6.3 什么时候用 OOP

适合用 OOP 的场景

  • 大型、复杂的业务系统
  • 需要建模现实世界实体的场景
  • 需要高度可扩展性的系统
  • 团队协作开发的项目
  • GUI 应用(天然的对象模型)

不适合用 OOP 的场景

  • 简单的脚本/工具
  • 性能极度敏感的底层代码
  • 纯算法/数学计算
  • 数据处理管道(函数式更合适)

记住:范式是工具,不是教条。 选最适合问题的范式,而不是最熟悉的范式。


七、OOP 实践建议

7.1 设计类的原则

  1. 高内聚,低耦合
  • 高内聚:一个类的所有方法都围绕同一个职责
  • 低耦合:类之间的依赖越少越好
  1. 优先用组合,而非继承
  • 继承是强耦合,组合是松耦合
  • 只有明确的"is-a"关系才用继承
  1. 面向接口编程,而非面向实现编程
  • 依赖抽象,不依赖具体类
  • 变量类型尽量用接口/抽象类
  1. 封装变化
  • 找出会变化的部分,把它们封装起来
  • 变化的部分和不变的部分分离

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 经典