外观
面向对象编程
约 3439 字大约 11 分钟
PHPWeb后端入门
2026-03-04
今天我们将和张华同学一起,运用现代软件工程的核心思想——面向对象编程(OOP) 来构建一个复杂的校园购物车系统。如果不想让代码变成一团乱麻,学会对象的模块化设计是进阶架构师的必经之路。
🎯 学习目标
通过本课学习,你将掌握类与对象、封装、继承、多态等核心概念,领会从“蛋炒饭”到“盖浇饭”的编程哲学跃迁,并能运用它们设计出易于维护和扩展的健壮后端系统!
1. 面向过程与面向对象:蛋炒饭与盖浇饭的哲学
在PHP的编程世界中,我们可以选择不同的范式来构建程序。其中,面向过程 就像是制作一份“蛋炒饭”,所有的食材(鸡蛋、米饭、葱花)和翻炒步骤紧密交织在一起,程序员扮演执行者,追求的是特定步骤下一气呵成的执行效率。
相对而言,面向对象(Object-Oriented, OO)则更像是制作“盖浇饭”。米饭是米饭,菜是菜,它们作为独立的模块存在,通过特定的方式组合在一起。在处理大型复杂项目时,这种方式不仅提高了代码的可读性和可维护性,还极大地增强了代码的复用率,此时的程序员更像是一位运筹帷幄的指挥官。
💡 知识扩展:现代编程范式的融合
虽然面向对象在构建大型复杂系统时占据统治地位,但现代 PHP 并非非黑即白。最新的 PHP 版本在保持传统脚本语言灵活性的同时,也允许开发者在局部性能敏感的场景继续使用面向过程的方式,甚至引入了一些函数式编程的特性,做到了多范式的美妙融合。
2. 类与对象:造物主的图纸与实体
在面向对象的思维中,类(Class)就是一张精密的抽象图纸,它定义了事物的静态特征(属性,如商品名称)和动态行为(方法,如计算价格)。而 对象(Object)则是根据这张图纸在现实(内存)中实际建造出来的具体实体。
使用 new 关键字,我们就能完成类的 实例化。每个对象在内存中都是独立的,你可以给它们赋予不同的属性值。在类的内部方法中访问自身属性时,我们会使用特殊的保留变量 $this,它代表着对象对“当前自我”的引用。
📐
Product 类 (图纸)
class Product {
public $name;
public $price;
}
等待根据图纸制造具体的对象实例...
🛡️ 知识扩展:PHP 8.2 的只读类
随着架构设计的演进,对象状态的安全性越来越受重视。在 PHP 8.2 中,官方引入了只读类 (readonly class) 的新特性。当你将整个类声明为只读后,它的所有属性在初始化后便绝对不可更改,这简直就是打造“不可变数据对象”的神兵利器,彻底杜绝了数据在流转中被意外篡改的风险。
3. 魔术方法:PHP的幕后魔法师
PHP中有一类特殊的方法,它们总是以双下划线 __ 开头,被称为 魔术方法。它们不需要被手动显式调用,而是在脚本运行的特定生命周期节点自动触发。
最常用的两位魔法师是 __construct() 和 __destruct()。前者是 构造方法,在对象使用 new 诞生的那一刻自动执行,负责属性的初始化等“接生”工作;后者是 析构方法,在对象被销毁或脚本结束时登场,负责关闭文件、释放资源等“善后”任务。
🎩 知识扩展:更多奇妙的魔法
除了生与死的魔法,PHP 还提供了拦截不可访问属性读取的 __get() 和写入的 __set(),以及拦截未定义方法调用的 __call()。如果你想让一个对象像字符串一样被直接输出,还可以实现 __toString() 这个隐式转换魔法,让对象在前端展示时具备更丰富的表现力。
4. 类常量与静态成员:共享单车与永恒真理
并非所有的数据都必须绑定在某个特定的对象身上。类常量 使用 const 关键字定义,它是属于整个类的永恒真理,一旦定义便不可修改,非常适合用来存储诸如圆周率、系统配置标识等固定不变的信息。
而 静态成员(使用 static 关键字声明的属性和方法)则像是小区里的共享单车,它不属于某一个具体的居民实例,而是被类的所有实例所共享。访问它们时,我们不需要实例化对象,直接使用类名加上范围解析操作符 :: 即可,在类内部则推荐使用 self:: 来调用。
🔗 知识扩展:后期静态绑定
当类的继承关系变得复杂时,self 关键字可能会显得有些刻板,因为它在编译时就固定指向了定义它所在的类。为了解决这个问题,PHP 引入了后期静态绑定 (Late Static Bindings) 特性,使用 static:: 可以在运行时动态解析并调用实际发起调用的子类,极大地提升了静态成员的扩展弹性。
5. 面向对象三大特性:封装、继承与多态
面向对象的基石由三大核心特性牢牢支撑。首先是 封装,它通过 public(公有)、protected(受保护)和 private(私有)这三种访问控制修饰符,将对象内部的数据细节隐藏起来,只对外暴露安全的交互接口。
其次是 继承,使用 extends 关键字,子类可以毫无保留地继承父类的非私有资产,同时还能添加专属的新功能。如果你希望某个类或方法终止继承树,可以使用 final 关键字将其封印。最后是 多态,同一种方法指令作用于不同的子类对象,会产生不同的执行结果,赋予了系统极强的灵活性。
👔 父类:Car (普通汽车)
属性 $brand = "大众"
属性 $wheels = 4
方法 run() {
echo "以 60km/h 正常行驶";
}
echo "以 60km/h 正常行驶";
}
extends (继承)
⬇️
🏎️ 子类:SportsCar (跑车)
从父类自动继承的资产 ↓
属性 $brand = "大众"
属性 $wheels = 4
新增
方法 turboBoost() {echo "开启氮气加速!";
}
重写 (Override)
方法 run() {echo "以 240km/h 极速狂飙!";
}
> 等待指令...
_
🧬 知识扩展:打破单继承限制的 Trait
PHP 的类遵循严格的单继承原则,即一个类只能直接继承一个父类。但如果想在跨越不同家族谱系的类中复用相同的代码怎么办?Trait 特性应运而生。它允许开发人员在不同层次的独立类中自由引入代码片段,就像是一套可插拔的共享技能包,完美突破了单继承的物理局限。
6. 抽象类与接口:制定江湖规矩
当我们需要规范一组子类的行为,但又不想给出所有的具体实现时,抽象类 就派上用场了。使用 abstract 声明的类不能被直接实例化,它更像是一份半成品的模具,强制要求所有继承它的子类必须实现其中定义的抽象方法。
如果说抽象类是半成品,那么 接口(interface)就是极其纯粹的强制契约。它只定义公共的方法签名,绝不包含任何实现细节。一个类可以使用 implements 关键字同时签署多个接口契约,从而在不同的业务场景中扮演多重角色。
契约约束 (Contract)
interface AnimalBehavior
// 声明了发声规范,所有签契约的动物必须实现该方法
public function makeSound();
implements
🐶
Class Dog
已签契约
echo "汪汪汪!(威慑)";
🐱
Class Cat
已签契约
echo "喵~ (撒娇)";
🦆
Class Duck
已签契约
echo "嘎嘎嘎!(觅食)";
👇 点击上面的动物,体验多态带来的神奇变幻!
等待统一调用 $animal->makeSound()...
⚖️ 知识扩展:接口的强类型进化
随着 PHP 版本的快速迭代,接口在大型协同开发中的威力与日俱增。结合现代 PHP 的类型声明 (Type Declarations),我们可以在接口中严格约束参数和返回值的具体数据类型。这种强类型约束确保了每一个实现了该接口的底层类,都能严丝合缝地遵守顶层架构师制定的规范,大幅降低了运行时错误。
7. 安全防线:反序列化漏洞攻防
在购物车数据的临时存储与传输中,我们偶尔会使用 serialize() 将复杂的对象结构转换为字符串,再用 unserialize() 进行 反序列化 还原。然而,如果反序列化的字符串来自于不可信的用户输入(如伪造的Cookie),就可能撕开一道致命的安全裂口。
攻击者可以精心构造恶意的序列化字符串,当服务器端毫无防备地执行还原时,往往会触发被篡改对象的 __destruct() 等生命周期魔术方法,从而导致 对象注入 并最终引发未授权操作。
[!WARNING] ⚠️ 知识扩展:实战中的反序列化漏洞与防御 历史上曾爆发过大量基于 PHP 反序列化的严重安全事件。例如在某开源系统库中发现的 CVE-2024-40624 以及 FoxCMS 中的 CVE-2025-29306 漏洞,都是因为对用户可控参数直接使用
unserialize(),从而允许远程攻击者执行任意系统命令。当前业界最主流且稳妥的防御策略是彻底抛弃原生序列化,转而使用不包含可执行代码的JSON数据交换格式 (json_encode/json_decode) 来处理外部数据交互。
8. 终极实战:便捷校园购物车系统与数学工具
理论必须经受实战的检验。张华同学设想的校园 购物车系统,正是面向对象思想的最佳练兵场。作为课程实训的彩蛋,我们还可以为商品包装设计一个计算常见图形参数的数学工具,比如利用数学公式 S=πr2 来计算圆形食品的包装面积。
首先,我们通过面向对象的建模思维,利用 UML图 清晰地描绘出商品、食品、电子产品与购物车之间的关联与继承关系:
接下来,我们编写核心的 后端处理逻辑,通过封装、继承和多态的综合运用,使得复杂的购物车金额计算变得异常简洁优雅。
<?php
// 1. 定义抽象商品父类(体现封装与抽象概念)
abstract class Product {
protected $name;
protected $price;
public function __construct($name, $price) {
$this->name = $name;
$this->price = $price;
}
public function getName() {
return $this->name;
}
// 强制子类可能会重写,但提供基础实现
public function getPrice() {
return $this->price;
}
}
// 2. 定义电子产品子类(体现继承特性)
class Electronics extends Product {
private $warrantyPeriod;
public function __construct($name, $price, $warrantyPeriod) {
// 使用 parent 关键字调用父类构造方法
parent::__construct($name, $price);
$this->warrantyPeriod = $warrantyPeriod;
}
// 获取附加保修信息
public function getWarrantyInfo() {
return $this->name . " (提供 " . $this->warrantyPeriod . " 年原厂保修)";
}
}
// 3. 定义购物车类(体现静态成员与对象聚合)
class Cart {
private $items = [];
public static $maxCapacity = 50; // 静态属性:全系统共享的购物车最大容量
// 添加商品,利用类型提示确保只能传入Product类型的实例
public function addItem(Product $product) {
if (count($this->items) < self::$maxCapacity) {
$this->items[] = $product;
return true;
}
return false;
}
// 结算总价
public function getTotalPrice() {
$total = 0;
foreach ($this->items as $item) {
// 体现多态:无论具体是Food还是Electronics,都能正确调用对应的getPrice方法
$total += $item->getPrice();
}
return $total;
}
}
/* --- 执行示例说明 --- */
// 实例化商品对象
$laptop = new Electronics("全栈开发专用笔记本", 8999, 3);
// 实例化购物车对象
$myCart = new Cart();
// 将对象作为参数进行交互
$myCart->addItem($laptop);
// 输出结果,使用 \n 保持终端输出整洁
echo $laptop->getWarrantyInfo() . "\n";
echo "当前购物车总计:" . $myCart->getTotalPrice() . " 元\n";
?>以上代码可以直接在终端通过 php cart.php 命令运行,即可验证业务逻辑是否闭环。
9. 课程知识点总结
| 知识模块 | 核心概念解析 | 校园购物车实战应用场景 |
|---|---|---|
| 面向对象基础 | 类图纸、对象实体、实例化过程 | 抽象出系统中的“商品”、“用户”和“购物车”实体 |
| 魔术方法 | __construct 接生、__destruct 善后 | 在用户将商品加入购物车的一瞬间自动初始化商品信息 |
| 常量与静态成员 | const 永恒、static 共享单车 | 设定全校统一的“购物车最大承载商品数量”限制 |
| 三大基石特性 | 封装细节、继承资产、多态执行 | 隐藏敏感的价格计算逻辑,无缝派生出食品与电子产品类 |
| 抽象类与接口 | abstract 半成品、interface 强制契约 | 制定所有能够参与满减促销活动的商品必须遵守的计算接口 |
| 业务安全防护 | 反序列化漏洞成因与防范策略 | 确保通过 Session 或 Cookie 存储的购物车数据不被恶意篡改 |
10. 后续进阶建议
- 思考:目前购物车的数据在页面刷新后可能会丢失,如何利用 PHP Data Objects (PDO) 技术,将对象的状态 持久化 保存到 MySQL 数据库中?
- 挑战:试着为当前的购物车系统引入一个“日志记录器”接口,并创建基于文件和基于数据库的两个不同日志类,体会依赖反转(Dependency Inversion)带来的解耦快感。
