软件分析与设计笔记 20:面向对象概念:对象、关系与多态

面向对象不是「用某个语言写代码」:对象与类的区别、封装真正在保护什么、五种关系(依赖、关联、聚合、组合、继承)各自何时用,以及多态为什么能减少条件判断。

学习#面向对象#软件设计#UML#学习笔记

本文是 《软件分析与设计笔记》 系列的第 20 篇,共 28 篇

这是我在学习软件分析与设计时整理的系列笔记。概念部分是我按自己的理解写的,示例与图示都是自己编、自己画的。理解有偏差的地方欢迎指出。

0 面向对象不是「用某个语言写代码」

一个常见的误解是:用 Java 或 C# 写程序,就是在做面向对象。但如果代码里只有一个「主流程类」加几十个静态方法,那只是用面向对象的语法写过程式代码。

面向对象(object orientation) 是一种组织系统描述的方式:把系统看成一组相对独立的、各自负责一件事的对象,它们通过互相发消息来协作。

它带来的直接好处是:改变了「改动会波及多远」这件事。后面几篇(22–25)讲的分析与设计,都是在这个基础上展开的。

1 对象与类

1.1 对象、类与实例

  • 对象(object) —— 一个具体的、可区分的东西:「张三」「订单 A1091」;
  • 类(class) —— 对一组对象的共同描述:「客户」「订单」;
  • 实例(instance) —— 类是模板,对象是它的一个实例。

1.2 状态与行为

一个对象有两部分:

  • 状态(state) —— 它当前是什么样,由属性(attribute) 记录;
  • 行为(behaviour) —— 它能做什么,由操作(operation) 定义(在代码里就是方法)。

1.3 行为跟着数据走

关键在两者的绑定:行为应该由持有状态的那个对象提供。「计算订单总价」这个操作应该属于订单,而不是属于某个外部工具类 —— 因为总价依赖的数据(明细、单价)都在订单里。这条原则看起来朴素,但它正是笔记 25 里那个「依恋情结」坏味道的判断依据。

2 抽象与封装

2.1 抽象

抽象(abstraction) 是只看需要关心的部分,忽略其余。建模时不可能把所有细节都画进去 —— 一张订单在库存模块里关心的是数量,在财务模块里关心的是金额,两处看到的「订单」本来就不该完全一样。

2.2 封装真正保护的是什么

封装(encapsulation) 是把状态和行为打包在一起,并决定哪些对外可见。这个概念常被简化成「用 private 修饰字段」,但它真正保护的是信息隐藏(information hiding):外部只能通过你提供的方式改变状态,你因此可以保证状态始终合法。

举个具体的:如果金额字段可以被外部直接赋值,那任何一处代码都能写出「金额为负」的订单。加了设置方法之后,你可以在那里拦住非法值。

3 五种关系

面向对象里的五种关系:依赖是临时使用,关联是长期引用并可带重数,聚合是部分可独立存在的整体部分关系,组合是部分随整体消亡的强聚合,继承是子类复用并覆盖父类行为;每种关系都给出 UML 符号与例子

3.1 五种关系一览

关系中文一句话判断
dependency依赖临时用一下就完了
association关联长期持有对方的引用
aggregation聚合has-a,部分可以独立存在
composition组合强聚合,部分随整体消亡
inheritance继承is-a,子类可以替换父类

3.2 依赖与关联

依赖(dependency) 是最弱的耦合:某个方法临时用到另一个对象。它出现在参数、局部变量里。

关联(association) 是长期引用,需要写清两件事:

  • 重数(multiplicity) —— 一端对应几个,例如 1 对 *;
  • 角色名(role name) —— 这个引用在业务上叫什么,例如「客户」持有的是他下的「订单」。

3.3 聚合与组合的分界线

聚合(aggregation) 和组合(composition) 容易混。两者的分界线不是「是否包含」,而是生命周期:

  • 旅行团包含若干旅客 —— 旅行团取消了,旅客还在,这是聚合;
  • 订单包含订单明细 —— 订单删了,明细没有单独存在的意义,这是组合。

3.4 继承

继承(inheritance) 表达 is-a:子类(subclass)复用父类(superclass)的定义,并可以覆盖(override)其中一部分行为。它的前提是一条硬约束:子类必须能替换父类而不破坏使用者的预期。如果做不到,就不该用继承 —— 常见替代方案是组合。

4 多态:把条件判断换成对象

4.1 不用多态的写法

多态(polymorphism) 是同一段调用代码,在不同对象上产生不同行为。

先看不用的写法。假设要按客户类型算折扣:

java
double discount(Customer c) {
    if (c.getType().equals("VIP")) {
        return 0.2;
    } else if (c.getType().equals("CORPORATE")) {
        return 0.15;
    }
    return 0.0;
}

问题不在写法,而在于每加一种客户类型,都要回来改这个方法。而且同样的 if 很可能散落在好几处。

4.2 用多态改写之后

用多态之后:

java
abstract class Customer {
    abstract double discount();
}

class VipCustomer extends Customer {
    double discount() { return 0.2; }
}

class CorporateCustomer extends Customer {
    double discount() { return 0.15; }
}

调用方只写 c.discount()。新增一种客户类型,只需要加一个类,已有的代码一行都不用动。

这就是「多态能减少条件判断」的含义:把「判断类型然后分支」换成了「让对象自己回答」。它也是后面笔记 24 讲设计原则时的核心工具。

5 单继承与多继承

单继承(single inheritance) 指一个子类只能有一个父类,多继承(multiple inheritance) 可以有多个。

多继承看起来更灵活,但会带来一个真实的歧义:两个父类有同名方法时,该用谁的?所以很多语言(包括 Java、C#)在类这一层只允许单继承,把「多个能力的组合」交给另一种机制去表达 —— 接口。这一点会在笔记 24 展开。

6 常见误解

  • 「把所有东西都建成类就是面向对象」。 类多不等于设计好。一个类如果既是数据结构又是工具集合,那它只是把过程式代码装进了类里;
  • 「继承用来复用代码」。 复用的手段很多,继承的语义是 is-a。为了省几行代码而继承,往往会在后面几周付出代价;
  • 「封装就是加 private」。 如果每个字段都配了无脑的读写方法,等于没封装;
  • 「组合优于继承是绝对规则」。 更准确的说法是:当关系不是 is-a 时,别用继承。

7 本节自测

  1. 类、对象、实例三者是什么关系?
  2. 「计算订单总价」这个方法为什么应该属于订单,而不是放在一个工具类里?
  3. 聚合和组合的分界线是什么?各举一个例子。
  4. 用多态改写一段 if (type.equals(...)) 的代码,改完最大的收益是什么?
  5. 为什么很多语言在类的层面只允许单继承?
  6. 封装真正保护的是什么?如果金额字段可以被外部直接赋值会怎样?

参考答案

  1. 类是模板,对象是它的一个实例;对象是「一个具体可区分的东西」。类是「对一组对象的共同描述」(§1.1)。
  2. 因为行为应该由持有状态的对象提供 —— 总价依赖的明细、单价都在订单里。放工具类里就是笔记 25 讲的「依恋情结」(§1.3)。
  3. 分界线是整体消亡时部分还活不活得下去。聚合:部分可独立存在(如旅客与订单,订单取消旅客还在);组合:部分随整体消亡(如订单与订单明细)(§3)。
  4. 新增一种类型只需新增一个类,已有的判断代码一行都不用改,而且判断不再散落在多处(§4)。
  5. 因为多继承会带来菱形继承的歧义(同一个祖先被两条路径继承时,成员该用哪一份)。所以多数语言用「单继承 + 多接口」解决(§5)。
  6. 封装真正保护的是信息隐藏 —— 外部只能通过你提供的方式改变状态。如果金额可被直接赋值,任何代码都能写出「金额为负」的订单,而你没有任何地方可以拦住它(§2.2)。

8 术语表

术语中文一句话解释
object orientation面向对象把系统组织成一组互相协作的对象的方法
object对象具体的、可区分的实体
class类对一组对象的共同描述
instance实例类的一个具体对象
state状态对象当前的属性取值
behaviour行为对象能执行的操作
attribute属性记录对象状态的数据项
operation操作对象提供的行为,通常对应一个方法
abstraction抽象只保留当前关心部分的做法
encapsulation封装把状态与行为打包并控制外部访问
information hiding信息隐藏对外隐藏内部细节,只暴露必要接口
dependency依赖临时使用另一个对象的关系
association关联长期持有对方引用的关系
multiplicity重数一端可以对应多少个实例
role name角色名关联中该引用在业务上的称呼
aggregation聚合部分可独立存在的整体—部分关系
composition组合部分随整体消亡的强聚合
inheritance继承子类复用并覆盖父类定义的关系
superclass父类被继承的类
subclass子类继承父类的类
single inheritance单继承一个类只能有一个父类
multiple inheritance多继承一个类可以有多个父类
polymorphism多态同一调用在不同对象上产生不同行为
override覆盖子类重新实现父类的方法

© 2026 HuanYu Blog

记录技术,也记录思考