封装
数据隐藏、公开与私有接口、内聚性原则——以及为什么封装良好的类更易使用、测试和维护。我们将在 Book 和 Customer 的基础上引入 ShoppingCart。
什么是对象?类如何定义对象?实例如何被创建?——以网络书店为贯穿全程的案例。
软件系统用来表示和管理现实世界中的事物。一家网络书店涉及书籍、顾客、订单和支付。面向对象设计(OOD)为我们提供了一种将这些事物翻译成软件的原则性方法——直观、可维护、可扩展。
核心思路是:识别问题域中各种独立的事物,描述每样事物知道什么(数据)和能做什么(行为),再定义一个模板(类),从中按需创建任意数量的对象。
整个课程使用同一个领域。下表列出我们将在八个模块中逐步构建的所有类:
| 现实世界的事物 | 我们要建的类 |
|---|---|
| 一本书 | Book |
| 一个注册用户 | Customer |
| 购物车 | ShoppingCart |
| 一笔购买交易 | Order |
| 订单中的一行 | OrderItem |
| 支付方式 | Payment |
| 库存管理 | Inventory |
| 用户评论 | Review |
表 1 — 网络书店中的现实事物与对应的类。
每个对象都有三个基本属性:
| 属性 | 含义 |
|---|---|
| 身份(Identity) | 每个对象都是独立的个体。就算两本书的书名、价格完全相同,它们仍然是两个不同的对象。 |
| 状态(State) | 对象当前持有的数据。状态可以随时间改变——书的库存数量随着售出而减少。 |
| 行为(Behaviour) | 对象能做什么,用方法表达。行为在类中定义一次,该类的所有对象共享。 |
表 2 — 对象的三个基本属性。
| 概念 | 定义与示例 |
|---|---|
| 属性(Attribute) | 对象内部的数据。每个对象有自己的一份属性副本。示例:Book 有 isbn、title、author、price、stock。 |
| 方法(Method) | 对象能做的事情。方法定义在类中,该类所有对象共享。示例:is_available() 检查库存是否大于零。 |
| 构造函数(Constructor) | 创建对象时自动调用的特殊方法。在 Python 中就是 __init__,负责把属性初始化为起始值。 |
表 3 — 属性、方法与构造函数。
class Book: """书店中可购买的一本书。""" # ── 构造函数 ────────────────────────────────────────── def __init__(self, isbn: str, title: str, author: str, price: float, stock: int = 0): # self 指向正在被创建的那个对象 self.isbn = isbn # 唯一编号 self.title = title # 书名 self.author = author # 作者 self.price = price # 定价(英镑) self.stock = stock # 库存数量 # ── 方法 ───────────────────────────────────────────── def get_details(self) -> str: """返回该书的可读描述。""" return f"{self.title} 作者:{self.author} £{self.price:.2f}" def is_available(self) -> bool: """库存大于零时返回 True。""" return self.stock > 0 def apply_discount(self, percent: float) -> None: """按百分比打折(输入 0–100)。""" if not 0 <= percent <= 100: raise ValueError("折扣必须在 0 到 100 之间") self.price *= (1 - percent / 100)
self 指向调用该方法的那个具体对象。每次定义方法时,第一个参数必须是 self,但调用时 Python 会自动传入,不需要手动写。
UML 把类画成一个分三格的矩形:上格是类名,中格是属性,下格是方法。属性前的字符表示可见性:− 表示私有,+ 表示公开。
实例化是从类创建一个具体对象的行为。在 Python 中,直接调用类名并传入构造函数所需的参数即可。每次调用都会产生一个完全独立的对象,拥有自己的属性副本。
# 用同一个 Book 类创建两本不同的书 book1 = Book( isbn = "978-0-13-110362-7", title = "The C Programming Language", author = "Kernighan & Ritchie", price = 45.99, stock = 12 ) book2 = Book( isbn = "978-0-20-163361-0", title = "The Pragmatic Programmer", author = "Hunt & Thomas", price = 39.99, stock = 0 # 已缺货 ) print(book1.get_details()) # "The C Programming Language 作者:Kernighan & Ritchie £45.99" print(book1.is_available()) # True print(book2.is_available()) # False book1.apply_discount(10) # 只对 book1 打九折 print(book1.price) # 41.391 print(book2.price) # 39.99 — book2 不受影响
书店还需要用户。按照同样的模式:
class Customer: """书店的注册用户。""" def __init__(self, customer_id: str, name: str, email: str): self.customer_id = customer_id self.name = name self.email = email def get_name(self) -> str: return self.name def update_email(self, new_email: str) -> None: self.email = new_email # 创建一个用户对象 customer1 = Customer("C001", "陈晨晨", "[email protected]") print(customer1.get_name()) # "陈晨晨" customer1.update_email("[email protected]") print(customer1.email) # "[email protected]"
bookA = Book("978-0-13-110362-7", "The C Programming Language", "Kernighan & Ritchie", 45.99, 5) bookB = Book("978-0-13-110362-7", "The C Programming Language", "Kernighan & Ritchie", 45.99, 5) # 内容相同…… print(bookA.title == bookB.title) # True — 值相同 # ……但是不同的对象 print(bookA is bookB) # False — 身份不同 # 修改一个不影响另一个 bookA.apply_discount(20) print(bookA.price) # 36.792 print(bookB.price) # 45.99 — 不受影响
| 运算符 | 测试的内容 |
|---|---|
== | 值相等——对象的内容是否相同? |
is | 身份相同——两个变量是否指向内存中完全相同的一个对象? |
表 4 — == 测值,is 测身份。
== 和 is 搞混。对于自定义类,如果没有定义 __eq__ 方法,== 会退化为测身份。我们将在模块三讲解如何正确定义 __eq__。
book1 与 book2 是 Book 的两个独立实例,各有自己的状态。customer1 是 Customer 的独立实例。| 图的类型 | 展示内容 |
|---|---|
| 类图(Class Diagram) | 设计图——类名、属性名称和类型、方法签名。没有具体数据,描述所有对象的共同结构。 |
| 对象图(Object Diagram) | 实况快照——具体对象及它们在某一时刻的属性值。对象标题格式为 对象名 : 类名,并带下划线。 |
表 5 — 类图 vs 对象图。
SysML v2 中"定义/用法"的区分,是 OOD 中"类/对象"区分的直接推广。对应关系非常精确:
part defpartattributeaction def / action:= 设初始值part myBook : Book// SysML v2 中等价于 Book 类的写法 part def Book { attribute isbn : String; attribute title : String; attribute author : String; attribute price : Real; attribute stock : Integer := 0; // := 是初始值 action def get_details : String; action def is_available : Boolean; action def apply_discount { in percent : Real; } } // 创建一个具体的 Book 实例 part book1 : Book { :>> isbn = "978-0-13-110362-7"; :>> title = "The C Programming Language"; :>> author = "Kernighan & Ritchie"; :>> price = 45.99; :>> stock = 12; }
part 是建模时的声明,描述系统架构中的一个具体元素,而不是运行时堆上的内存分配。结构类比成立;运行语义不同。
对象 = 数据(状态)+ 行为(方法)打包在一起。这种打包叫封装。
类是设计图,对象是实例。一个类可以创建无数个对象,每个对象独立拥有自己的状态。
从类创建对象。Python 里直接写 book1 = Book(...)。
每个对象都有身份(独立个体)、状态(属性当前的值)和行为(可调用的方法)。
两个对象可以持有相同的数据,但仍然是完全独立的个体。== 测值,is 测身份。
类 → part def;对象 → part。定义/用法的区分是类/实例区分的直接推广。
数据隐藏、公开与私有接口、内聚性原则——以及为什么封装良好的类更易使用、测试和维护。我们将在 Book 和 Customer 的基础上引入 ShoppingCart。