1. 三大基本特征
1.1 封装、继承、多态
面向对象编程(OOP)的三大特征是封装、继承、多态:
# 封装: 把数据与操作数据的方法绑定在一起,对外隐藏内部实现
# 继承: 子类复用/扩展父类的属性和方法(is-a 关系)
# 多态: 同一接口在不同实现下表现不同行为
三大特征的工程意义:封装降低耦合(改动局部化),继承促复用(消除重复),多态支撑扩展(开闭原则)。但三者都不可滥用——过度继承是架构腐化的头号来源。
2. 封装与访问控制
2.1 信息隐藏
封装的本质是信息隐藏:把「内部状态」藏起来,只暴露「受控的操作接口」。对外面的代码,对象是一堆行为,而不是一堆可乱改的字段。
# 反模式: 直接暴露可变字段
# obj.count += 1 // 绕过校验与约束
# 正模式: 通过方法操作
# obj.increment() // 内部可加校验/同步/日志
2.2 访问修饰符的选用
| 修饰符 | 可见范围 | 适用 |
|---|---|---|
| public | 全部 | 对外接口 |
| protected | 子类 + 包内 | 供扩展的钩子 |
| 默认(包) | 包内 | 内部协作 |
| private | 类内 | 内部实现 |
工程建议:字段一律 private,暴露用方法/属性;只有确需子类重写的才用 protected。访问控制越严,改动的冲击面越小。
3. 继承:is-a 与陷阱
3.1 is-a 关系
继承表达 is-a:子类是父类的特化(Dog is-a Animal)。合理的继承是「子类在父类能力上增加/细化」,不合理的继承是「为了复用代码而强行继承」。
# 合理继承: 子类扩展行为
# class Circle extends Shape { override draw() {...} }
# 不合理继承: 只为用父类方法
# class Stack extends ArrayList // 栈不是"一种"ArrayList!破坏 LSP
3.2 继承的两个经典问题
- 脆弱的基类:父类一改,所有子类行为漂移,难以追责。
- 破坏封装:子类访问父类 protected 字段,父类内部约定被侵入。
- 菱形继承:多继承下同一父类被继承两次(C++ 需虚拟继承;Java 用接口规避)。
规避策略:优先组合(持有对象的引用)而非继承——组合表达的「has-a」更弱耦合、更易测试、更好演化。
4. 多态:重载与重写
4.1 编译期与运行期多态
# 重载(overload): 同名方法、不同参数 —— 编译期决定(静态多态)
# 重写(override): 子类重新实现父类方法 —— 运行期决定(动态多态)
# 多态实现条件: 继承 + 重写 + 父类引用指向子类对象
# Animal a = new Dog(); a.speak(); // 实际调用 Dog.speak()
多态的价值:面向接口编程。调用方依赖抽象(父类/接口),不依赖具体实现——替换实现时调用方不用改。这是开闭原则的基石。
4.2 多态的底层
动态多态靠虚方法表(vtable):每个类一张表,记录方法指针,调用时查表定位实际方法。理解这一点对「为什么接口调用比直接调用略慢」「为什么静态方法没有多态」有帮助。
5. 抽象类与接口
5.1 各自定位
| 抽象类 | 接口 | |
|---|---|---|
| 继承关系 | is-a(类继承) | can-do / 契约(实现) |
| 状态 | 可有实例字段 | 无状态(Java8+ 可有 default 方法) |
| 多继承 | 单继承 | 可多实现 |
| 演进 | 加方法会破坏子类 | default 方法可平滑扩展 |
# 选型: 需共享状态/实现 → 抽象类;纯契约/多实现 → 接口
# 现代工程趋势: 接口为主,抽象类为辅(多用接口声明能力)
5.2 接口即契约
接口表达「能做某件事」的能力契约。依赖抽象(接口)而非具体类,是依赖倒置原则(SOLID 的 D)的实现手段——高层模块依赖抽象,低层模块实现抽象。
6. SOLID 设计原则
6.1 五大原则速览
- S(单一职责):一个类只负责一件事。改动原因唯一。
- O(开闭原则):对扩展开放、对修改关闭。新功能加代码不删改旧逻辑(多态 + 策略)。
- L(里氏替换):子类必须能替换父类而不破坏程序。继承必须保持 is-a 语义。
- I(接口隔离):接口小而专,不强迫实现类实现用不到的方法。
- D(依赖倒置):依赖抽象,不依赖具体实现。
# 反例(违反 OCP)
# if (type == "email") sendEmail(); else if (type == "sms") sendSms();
# 正例(多态 + 策略)
# 每个发送渠道实现 Sender 接口,新渠道加一个实现类
# 调用方遍历 Sender 列表即可 —— 加渠道不改调用方
SOLID 的落地价值:让「加功能」从「改旧代码」变成「加新代码」,降低回归风险与耦合。
7. 组合优于继承
7.1 组合的优势
# 组合: class Bird { Flyable fly; } // has-a 能力
# 继承: class Bird extends Animal // is-a 关系
# 组合的好处: 灵活换实现、弱耦合、易测试(可注入 mock)
# 何时坚持继承: 真正 is-a + 需要复用父类实现 + 不破坏 LSP
经验法则:默认组合,确需继承才继承。先问「子类真的是一种父类吗」,「能复用父类实现吗」,「换成组合会不会更简单」——多数场景组合胜出。
7.2 装饰器模式(组合的典范)
装饰器用「组合 + 递归包装」扩展对象能力,替代深继承链:new BufferedInputStream(new FileInputStream(...)) 就是装饰器——层层包装、能力叠加,无需为每种组合写一个子类。
8. 常见反模式
- 上帝类(God Class):一个类塞满所有职责,违背单一职责。拆分。
- 继承替代组合:为复用写 is-a,一旦需求变化基类成了枷锁。
- 过度抽象:为「可能扩展」建一堆接口,实际只有一个实现。抽象要为「真实变化」预留,不为臆想。
- 贫血模型:类只有 getter/setter 无行为,退化成数据类——行为放哪了?(领域逻辑应尽量内聚进对象。)
9. 工程实践清单
# OOP 落地检查单
# 1) 字段 private,行为走方法
# 2) 依赖接口/抽象,不依赖具体类
# 3) 默认组合,继承只在真 is-a 且不破坏 LSP
# 4) 新功能优先加实现类,不改旧代码(OCP)
# 5) 一个类讲一个故事(SRP)
# 6) 接口小而专(ISP),抽象为真实变化预留
参考文章
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。