
写PHP的人早晚会遇到一道坎单个脚本写得很顺一到系统级需求就卡壳。我也见过不少同学交期末大作业明明用了class Order {}里面却全是MySQL拼接、foreach嵌套和到处echo说白了就是给过程式代码披了一件对象的外套。问题不出在语法而出在对面向对象四个字的理解还停留在背定义。庖丁解牛这个故事其实把这件事讲透了——庖丁不看整头牛他看见的是骨节、经络、缝隙刀顺着空隙走所以一把刀用了十九年还像新的一样。今天这篇就照着这个思路把PHP面向对象拆开类与对象是什么关系三大特性到底在解决什么依赖和接口怎么搭才不别扭以及在序列化、反序列化、文件包含这些暗骨地带怎么稳稳地不翻车。适合刚接触框架的PHP后端新人、准备期末大作业的学生以及想真正读懂各种开源PHP源码的人。1. 先看清全牛类与对象不是名词游戏1.1 类是一张图纸对象才是盖好的房子很多教材把类和对象的关系说成类是模板对象是实例道理对但太干学完照样不会用。换个角度看类是一张建筑图纸图纸上写客厅30平方米、主卧15平方米图纸本身不占土地也不会有人住进去对象是根据图纸盖出来的那套房每套房的结构一样里面的住户却各不相同。对应到PHP里代码里写一个class User {}只是定义了一张图纸只有当通过new User()创建对象后才真正在内存里分配了这块数据空间每次创建出来的对象属性值都是独立互不干扰的。刚入门的人容易犯一个认知错误以为class里面定义的属性和方法会常驻在某个全局空间多个请求之间可以共享。实际完全不是这样。PHP的传统模型是一次请求一次生命周期同一个类可以在每个请求里反复实例化但对象从new开始活到请求结束一旦脚本跑完对象随之销毁。想跨请求保存数据得靠数据库、缓存、文件或者session而不是幻想某个对象会自己在内存里等你回来。1.2 二维数组到对象的转换先治数据结构松散的病PHP的数组几乎是万能的查数据库默认返回二维数组前端传参数也是数组甚至在很多老代码里连配置都扔在一个大数组里。数组的优点是真香缺点也很明显结构太松散。举个例子从数据库查用户表你得到的是$rows [ [id 1, name 张三, email zhangsanexample.com], [id 2, name 李四, email lisiexample.com], ];这种结构看着方便但它没有强制约束。假如某处拼错了字段名写成$row[emial]PHP不会在编译期报错只会悄悄返回一个未定义索引的警告然后继续跑。等bug浮出水面你往往要找半天。面向对象的第一个实际动作就是把这类松散数组收敛成有结构的数据类型class User { public function __construct( public readonly int $id, public readonly string $name, public readonly string $email ) {} } $users array_map( fn(array $row) new User((int)$row[id], $row[name], $row[email]), $rows );这样一改字段名写错会在IDE和静态检查阶段就被发现而且后续处理时可以直接调用对象方法比如$user-getProfileUrl()不用到处写一串数组套数组。把二维数组转成对象数组是很多PHP项目从能跑迈向好维护的第一步也是搜索里php二维数组改变键值这一类问题的最佳解法——你不再需要小心翼翼操作数组键名对象自己带着访问入口。1.3 从金额函数到金额对象把行为绑在数据上再拿一个网上高频问题举例PHP把小写数字金额转为大写。过程式做法是写一个全局函数function amountToChinese(float $amount): string { /* ... */ }功能能用但只要金额这个数字出现在不同地方你就得不停把这个函数到处引用而且金额的计算逻辑、检验逻辑、格式化逻辑全散落在各处。面向对象更推荐的做法是做一个值对象class Money { public function __construct(private readonly float|int $amount) {} public function toChineseUpper(): string { $digits [零, 壹, 贰, 叁, 肆, 伍, 陆, 柒, 捌, 玖]; $units [, 拾, 佰, 仟]; // 这里省略完整转换实现细节核心是数据和行为绑定在同一个类里 return 转换结果; } public function add(Money $other): Money { return new Money($this-amount $other-amount); } public function getAmount(): float|int { return $this-amount; } }区别在哪过程式函数里金额只是函数的参数值对象里金额是对象的灵魂。调用方把$money传来传去所有和金额相关的规则都能挂在这个对象上谁也不会绕开对象直接操作底层数值。这就是面向对象最开始的一点点骨感数据和行为不分离。2. 三刀落在骨缝上封装、继承、多态的真实分寸2.1 封装private不是摆设是状态守卫网上最常见的晒代码写法是属性全公开方法全公开谁都可以直接改。在只有一个开发者的玩具项目里这确实无所谓。但只要项目进入协作阶段public字段就是灾难。搜模拟炒股php能发现很多示例代码恰好能用来说明这个问题。假设你写了一个交易账户类class TradingAccount { public float $balance 0; }看起来简洁但问题很大。任何一处代码都能随手$account-balance 999999;风控模块根本不知道余额是被谁改的、通过什么流程改的、是否合法。如果某天需求变成卖出股票要校验持仓足够、提现要记录流水你只能满项目搜索-balance的赋值点挨个补逻辑。封装的正确姿势不是把字段藏起来故弄玄虚而是把状态的修改收口到方法里让每次变更都可以附带校验和观察点class TradingAccount { private float $balance 0; public function deposit(float $amount): void { if ($amount 0) { throw new InvalidArgumentException(存款金额必须大于0); } $this-balance $amount; } public function withdraw(float $amount): bool { if ($amount $this-balance) { return false; } $this-balance - $amount; return true; } public function getBalance(): float { return $this-balance; } }我把balance设成private余额只能通过deposit()和withdraw()修改。将来要加流水记录、加风控阈值、加消息通知只需在这两个方法里插入对应代码所有调用方自动获得新行为。这才是封装的意义你不是在限制别人而是在保护未来的自己。很多新人还有个误区以为封装就是属性私有、提供getter/setter并且两者一一对应。如果只是private $name; getName(){...} setName($v){...}那跟public没有任何区别。封装的精髓在于能对外暴露的操作越少越好暴露出来的操作必须带有业务语义。setName这种毫无语义的方法很多时候不如直接在构造函数里一次性定死。2.2 继承先搭骨架再谈复用继承是OOP里最容易被滥用的特性。一说到复用很多人第一反应就是extends结果硬造出一堆订单继承用户苹果继承香蕉的歪门关系。踩得最多的坑是构造函数。PHP中子类不会自动调用父类的构造函数这一点经常被忽略。看这个例子class Account { public function __construct( protected string $accountNumber, protected float $balance 0 ) {} } class SavingsAccount extends Account { public function __construct( string $accountNumber, private float $interestRate 0.03 ) { // 如果忘了 parent::__construct($accountNumber),$accountNumber 就没有被初始化 } }一旦子类重写了构造函数却忘记调用parent::__construct()父类的属性就处于未初始化状态后续任何方法一用到就报错。这种错误在框架里非常隐蔽因为IDE不一定会给红色波浪线得运行到特定路径才炸。所以我的个人经验是继承前先问自己父子之间是不是真正的is-a关系。SavingsAccount是Account吗是因为储蓄账户本来就是账户的一种可以继承。Order是User吗明显不是订单和用户是has-a关系——订单里有一个归属用户。后者应该通过属性组合class Order { public function __construct( private string $orderNo, private User $buyer ) {} }组合优于继承这句话在工程里比大多数设计原则都实用。另外如果一个类设计出来就没打算让别人继承直接加上final修饰编译器会帮你拦住所有不合理的继承行为这比靠文档约束可靠得多。2.3 多态同一个动作多种实现多态听起来玄实际就是一句话调用方只依赖一个抽象接口具体行为由实现类各自决定。还是用交易场景。股票、基金、债券都可以折算成日收益但计算方式完全不同。如果你写一堆if ($type stock) {...} elseif ($type fund) {...}每加一种新资产就得改一遍所有业务代码。面向对象的做法是先把日收益定义成接口interface Asset { public function dailyReturn(): float; } class Stock implements Asset { public function __construct(private float $openPrice, private float $closePrice) {} public function dailyReturn(): float { return ($this-closePrice - $this-openPrice) / $this-openPrice; } } class Fund implements Asset { public function __construct(private float $netValueToday, private float $netValueYesterday) {} public function dailyReturn(): float { return ($this-netValueToday - $this-netValueYesterday) / $this-netValueYesterday; } } function formatPortfolio(array $assets): array { // 调用方根本不管具体是什么资产只认 Asset 接口 return array_map(fn(Asset $asset) $asset-dailyReturn(), $assets); }新增一种资产只要实现Asset接口就行formatPortfolio()一行不用改。这种扩展性不是花架子当你的项目从3种资产长到30种资产时多态能救你命。3. 游刃有余的技术依赖注入、接口与轻量容器3.1 到处new是在拿刀砍骨头庖丁说自己的刀之所以十九年不坏是因为刀刃从来不碰硬骨头只走骨节之间那道缝。PHP里最硬的骨头就是类与类之间那层硬编码依赖。控制器里普遍有这样的写法class BookController { public function list(): void { $db new PDO(mysql:host127.0.0.1;dbnamelibrary, root, ); // 查询逻辑... } }每个方法里都new PDO一遍看起来没问题但数据库连接配置被复制了无数次换库、加日志、做测试全都寸步难行。更麻烦的是控制器和数据库连接焊死在一起想mock一个假数据库来做单元测试根本无从下手。解决办法是把依赖从外部传入这就是依赖注入。具体表现就是构造函数注入class BookController { public function __construct(private BookRepository $repository) {} public function list(): void { $books $this-repository-findAll(); // 输出逻辑... } }BookController不再关心BookRepository是怎么连数据库的它只知道自己需要一个能查书的仓库。将来要换数据源、加缓存层改的是组装BookController的地方而不是改控制器本身。3.2 接口与抽象类的分工依赖注入解决了谁创建谁的问题但还有一个前置问题依赖的类型应该声明成什么。你可以直接依赖一个具体类BookRepository也可以依赖一个接口BookRepositoryInterface。后者更灵活。接口和抽象类的选择很多新手搞不清楚。我习惯用一个粗俗的类比接口是一份合同要求签约方必须提供哪些服务但完全不干涉内部长什么样抽象类是一个半成品模具已经把一部分共用逻辑做好了子类只需要补全剩下的部分。比如日志系统可以定义接口interface Logger { public function log(string $message, string $level info): void; }文件日志、数据库日志、发送到远程日志服务的类各自实现Logger接口即可。调用方只要Logger不需要知道具体是写到文件还是写到数据库。而抽象类更适合有明显骨架的场景比如所有数据库驱动都要处理连接、超时这些公共步骤就可以在抽象类里写一遍公共代码子类只实现特定的SQL方言差异。实际工程建议是优先依赖接口而不是依赖具体类。你依赖接口你就逼着自己去思考对方能做什么而不是是什么设计出来的协作关系会清爽很多。3.3 接口、数组、对象与JSON的统一出口搜php接口数组对象的人多半是在前后端联调时被数据格式绕晕了。PHP后端返回给前端的数据经常一会儿是数组一会儿是对象一会儿又嵌套得乱七八糟前端解析起来得写一堆防御性判断。我踩过这个坑后定了一条规矩凡是给外部系统的接口统一用DTO加JSON绝不直接吐数据库行。所谓DTO就是专门用来传输数据的对象它不承担业务逻辑只负责组织数据结构。class BookDto implements JsonSerializable { public function __construct( public readonly int $id, public readonly string $title, public readonly string $author ) {} public function jsonSerialize(): array { return [ id $this-id, title $this-title, author $this-author, ]; } }接口出口处固定包装成统一结构function success(mixed $data): string { return json_encode([code 0, message ok, data $data], JSON_UNESCAPED_UNICODE); }这样前端永远面对的是{code, message, data}这个固定外壳data到底是什么由接口文档约定而不是由后端今天的数组心情决定。另外搜索词里还有php跨域jsonp。如果是被JSONP和跨域折磨过的后端最直接的方案是用CORS替代JSONP现代浏览器对JSONP的兼容性需求已经很低了CORS更干净也更安全// 跨域响应头按需收紧来源白名单 header(Access-Control-Allow-Origin: https://allowed.example.com); header(Access-Control-Allow-Headers: Content-Type, X-Requested-With); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS);如果你确实是在维护老系统JSONP的PHP端一般约定一个callback参数注意只允许白名单内的回调名字防止被注入然后输出echo $callback . ( . json_encode($data) . );。但新项目我基本不会再碰JSONPCORS省心得多。4. 暗骨遍布之处生命周期、魔术方法与对象序列化4.1 生命周期和请求绑得死死的这一节聊的是对象看不见的生死时刻。前面说过传统PHP模型里对象生命周期以一个请求为界限请求结束对象树全部销毁。这意味着你在这个请求里创建的一切对象都没办法留在下一次请求里直接被读取。在实际开发中这个性质带来两个直接后果。第一面向对象代码里不要试图保存状态到对象字段上等下一次请求再用那是白费力气要保存就保存到数据库、缓存或session。第二想提升性能重点不在让对象活着而在让对象尽快创建、尽快用完。很多框架类为什么设计成路由收到请求后才动态加载就是为了减少无谓的对象创建。这些设计背后全是同一个生命周期认知在支撑。4.2 魔术方法要用在刀刃上PHP的魔术方法不少常见的有__construct、__destruct、__get、__set、__call、__toString、__clone、__sleep、__wakeup等。它们本身不神秘就是在特定时机被PHP自动调用的钩子。初学者最爱滥用的是__get/__setclass User { private array $data []; public function __get(string $name): mixed { return $this-data[$name] ?? null; } public function __set(string $name, mixed $value): void { $this-data[$name] $value; } }这种写法让$user-email能访问到但IDE完全给不了自动补全提示属性名拼错了也不会在开发期暴露等到运行时才发现返回了null。一旦项目变大这类魔术就是埋雷。我的经验是__get/__set尽量少用就算要用也得在类里用property注解把可访问的属性明确声明出来让IDE和静态分析工具能兜底。__toString是另一个容易被忽视的坑。只要对象被当作字符串使用比如拼接、echo它就会自动触发。如果里面写了重逻辑你排查几层都想不到性能问题出在自动字符串转换上。所以__toString永远只做一件事快速返回一个有意义的字符串表示别干重活。__clone则和对象复制有关。PHP的对象默认是引用赋值$b $a其实是让$b指向同一个对象而不是复制。要真正复制对象必须用clone。如果这个对象内部还持有另一个对象浅复制会把内部对象原封不动地共享过去这时候就需要在__clone里手动把内部对象也复制一遍。这是深拷贝和浅拷贝的根源。4.3 序列化长什么样中文为什么总被说有问题PHP的serialize()把对象变成字符串格式本身是PHP私有的。一个User对象序列化出来大致长这样O:4:User:2:{s:8:userName;s:6:张三;s:8:userPass;s:6:123456;}O代表对象4是类名长度User是类名2是属性数量后面跟着属性名和属性值。字符串格式里中文不会被serialize()直接破坏因为它保存的是长度和字节内容重新unserialize()回来仍然能还原中文。那为什么大家总觉得序列化和中文扯上关系因为很多人把serialize()和json_encode()混在一起用。json_encode默认会把中文转成\uXXXX这种转义形式要显示中文需要再加JSON_UNESCAPED_UNICODE参数。两者根本不是一回事。我的建议是跨语言、跨系统的数据交换统一用JSON不要用PHP序列化字符串。JSON是人可读的、跨语言的、安全的PHP序列化字符串只适合PHP自己内部临时存储。4.4 反序列化与文件包含的防守姿势搜索里频繁出现php反序列化漏洞原理php文件包含漏洞说明这是很多人在安全题目和实际排查中会遇到的主题。我在这里只讲防守思路不讲攻击利用。unserialize()的杀伤力在于反序列化的时候PHP会在后台重建对象并自动触发某些魔术方法比如__wakeup()对象销毁时可能触发__destruct()。如果攻击者能控制你传入的序列化字符串他就可能利用项目里某个类的方法实现非预期的代码执行。很多CTF题目和真实漏洞都是从这里切入的。防守要点三条第一条永远不要unserialize()不可信数据。用户提交的、外部接口传过来的字符串只要不是你自己系统生成的序列化结果就不该走反序列化。第二条如果某个老业务必须反序列化用allowed_classes白名单$obj unserialize($rawString, [allowed_classes [User::class, Book::class]]);不在白名单里的类直接变成__PHP_Incomplete_Class攻击面会被大幅压缩。第三条业务数据尽量选择JSON而不是序列化JSON只是纯数据解析不会触发PHP类的魔术方法。文件包含也是同样的防守逻辑。所谓文件包含漏洞常见成因是把用户输入直接拼到include语句的路径里比如include ./pages/ . $_GET[page] . .php;。最稳妥的修法是根本不让用户输入碰路径$allowedPages [home, books, users]; $page $_GET[page] ?? home; if (!in_array($page, $allowedPages, true)) { $page home; } include __DIR__ . /../pages/ . $page . .php;白名单固定目录前缀用户的输入再花哨也出不了圈。5. 刀要常磨PHP 8.3 的类型系统与开发调试环境5.1 从PHP7到PHP8.3面向对象越来越稳很多老开发嫌弃PHP多半停留在PHP5、PHP7早期的印象。实际上PHP 8之后语言层面已经补上了面向对象最需要的东西特别适合把刀磨得更锋。首先是严格类型。在文件头部声明declare(strict_types1);后再写类型约束函数传参和返回值就不允许静默转换。比如function sum(int $a, int $b): int {}你传字符串进去直接抛TypeError而不是悄悄转成0很多低级bug在入口就现形了。然后是构造器属性提升这是PHP 8最提升幸福感的语法。以前写一个包含初始化的类要先声明属性、再写构造函数参数、再在构造函数里一个个赋值三个步骤冗余又啰嗦。现在一行搞定class Book { public function __construct( public readonly int $id, public readonly string $title, public readonly string $author, ) {} }readonly表示只读属性初始化之后就不能再改天然表达了这条记录的数据不可变这一层语义省下去一堆写setter的冲动。搭配联合类型int|float、match表达式和enum枚举很多以前要靠注释和约定来维持的规则现在都能写进语法里。这就是我对面向对象的进阶理解面向对象不是在class里堆方法而是让你能把业务约束表达进类型系统让语言帮你守住边界。5.2 Windows下PHP版本选择与运行环境搜索里有php 8.3下载phpstudy升级php版本windows server php环境搭建说明很多人卡在第一步环境跑不起来后面全是白搭。Windows下PHP分两种包TSThread Safety和NTSNone Thread Safe。用Apache或IIS跑PHP时一般选TS版本因为Apache在Windows下需要线程安全模式用PHP内置开发服务器或FastCGI模式可以选NTS。很多人随便下载一个版本扩展装不上、运行报错往往就是TS/NTS和服务器模式不匹配导致的。所以第一件事就是确认你用的是哪一档。用phpstudy这类集成环境升级PHP版本时别只顾着下载新版本还要确认扩展是不是配套的。PHP 8.3和PHP 8.2在扩展目录上是分开的ext文件夹里没有对应扩展启动就会提示找不到DLL。升级完成后重点检查三样php -m能列出预期扩展、php -v版本号正确、CLI和Web端PHP版本一致。这两个版本不一致是很多诡异报错的元凶。5.3 在VS Code里把PHP跑起来含调试VS Code写PHP其实体验相当不错关键是把两个东西配好语言服务插件和调试器。第一步装插件PHP Intelephense然后打开设置把php.validate.executablePath指向你的php.exe路径。这样类名跳转、方法提示、参数提示全都活了写面向对象代码时体验和Java/Python比完全不落下风。第二步配调试。下载对应版本的Xdebug扩展在php.ini里加zend_extensionxdebug xdebug.modedebug xdebug.start_with_requestyes xdebug.client_host127.0.0.1 xdebug.client_port9003然后在VS Code里创建launch.json用PHP类型的调试配置监听9003端口。之后在PHP代码里打上断点F5启动调试浏览器访问对应页面程序就会停在断点上。这里有个容易出错的点是Xdebug版本必须和PHP版本严格匹配很多人安装了却不生效排查到最后都是版本不匹配。怎么用PHP在浏览器控制台里输出变量这个需求其实不用控制台调试时用Xdebug打断点看变量最直观。如果是临时打印推荐error_log(json_encode($data))写到日志文件或者var_dump后加die()而不是乱丢echo污染页面输出。正规项目里把调试输出打印到响应主体里是非常不推荐的做法会破坏接口数据格式。6. 这头牛怎么下锅图书管理系统过程式到面向对象重构记录最后拿一个出现率极高的需求完整走一遍图书管理系统带数据库很多学校的期末大作业就是这个题目。我用它演示一次真正的面向对象重构路线。6.1 原始过程式代码的病根期末作业常见的过程式写法大概长这样$conn mysqli_connect(127.0.0.1, root, , library); $action $_POST[action] ?? list; if ($action add) { $title $_POST[title]; $author $_POST[author]; mysqli_query($conn, INSERT INTO books (title, author) VALUES ($title, $author)); } elseif ($action delete) { $id (int)$_POST[id]; mysqli_query($conn, DELETE FROM books WHERE id $id); }这段代码功能上没毛病但它把所有事都揉在一锅粥里数据库连接、请求解析、SQL拼接、状态流转全在一个文件里。它的病根不是没用class而是职责没有分离。一旦要加借阅记录用户登录分类管理这个文件就会膨胀成一个没人敢动的怪兽。6.2 第一步把数据行变成Book模型重构的第一刀先定义数据的样子。没有模型的情况下一本书就是一个关联数组有了模型书就是一个有行为约束的对象class Book { public function __construct( public readonly int $id, public readonly string $title, public readonly string $author, private bool $available true ) {} public function isAvailable(): bool { return $this-available; } public function markBorrowed(): void { $this-available false; } }available是私有状态只借出和归还方法才能修改它这就是前面说的封装。以后想在借书时记录时间、推消息都只需要改这一个方法。6.3 第二步仓库与服务分离模型定义好后数据库访问应该单独封装成一个仓库类class BookRepository { public function __construct(private PDO $pdo) {} public function findAll(): array { $stmt $this-pdo-query(SELECT id, title, author FROM books); $rows $stmt-fetchAll(PDO::FETCH_ASSOC); return array_map( fn(array $row) new Book((int)$row[id], $row[title], $row[author]), $rows ); } public function save(string $title, string $author): int { $stmt $this-pdo-prepare(INSERT INTO books (title, author) VALUES (?, ?)); $stmt-execute([$title, $author]); return (int)$this-pdo-lastInsertId(); } }PDO 预处理语句这一步也顺便解决了SQL注入问题比原来直接拼接字符串安全得多。BookRepository只负责和数据库打交道业务判断、计算、编排逻辑则放到Service层class BookService { public function __construct(private BookRepository $repository) {} public function createBook(string $title, string $author): Book { if (mb_strlen($title) 1 || mb_strlen($title) 100) { throw new InvalidArgumentException(书名长度不合法); } $id $this-repository-save($title, $author); return new Book($id, $title, $author); } }到这里控制器不再需要知道任何SQL和业务规则它只负责拿到参数、调用服务、把结果返回出去。6.4 第三步控制器、路由与统一JSON出口控制器本身不需要做太多事情这也是我一直强调的控制器薄一点Service厚一点Model轻一点。class BookController { public function __construct( private BookService $service, private BookRepository $repository ) {} public function list(): string { $books $this-repository-findAll(); return json_encode([code 0, data $books], JSON_UNESCAPED_UNICODE); } public function add(array $request): string { try { $book $this-service-createBook($request[title], $request[author]); return json_encode([code 0, data $book], JSON_UNESCAPED_UNICODE); } catch (InvalidArgumentException $e) { return json_encode([code 1, message $e-getMessage()], JSON_UNESCAPED_UNICODE); } } }入口文件里只做两件事解析请求、分发给对应的方法。这就是最简路由$route $_GET[route] ?? book/list; $controller new BookController( new BookService(new BookRepository(new PDO(mysql:host127.0.0.1;dbnamelibrary, root, ))) ); if ($route book/list) { header(Content-Type: application/json; charsetutf-8); echo $controller-list(); } elseif ($route book/add) { header(Content-Type: application/json; charsetutf-8); echo $controller-add($_POST); }你看这里就把依赖装配集中在入口处。以后想换数据库、加日志、加缓存只改这个组装点控制器、服务、仓库三个层次的内部代码完全不用动。这就是依赖注入和分层带来的直观收益。6.5 重构之后的体会从过程式改造成面向对象代码行数并没有变少可能还更多。但三个变化是实打实的第一每个类只看名字就知道它是干什么的第二改业务规则时不用全项目搜索只需要改对应类里那一个方法第三想测试时可以分别mock掉仓库层和服务层单独验证某一段逻辑。我实际带人重构过好几次类似的作业最深的体会是面向对象最难的不是语法而是定对象边界这件事。同样一个图书系统有人会把用户、图书、借阅记录全塞进一个大类有人会拆成五六个小类互相协作。后者一开始麻烦但越往后维护越轻松。庖丁解牛的目无全牛说的就是这个意思——你眼里不再是一头完整的牛而是一块块筋膜、一条条骨缝刀自然就游刃有余了。写PHP也一样看得清类与类之间的接缝代码的刀口就不会崩。