
这周正好讲到面向对象的三大特征。接触过不少同学看概念时都觉得简单——封装不就是私有变量吗继承不就是 extends 吗多态不就是重载重写吗可一旦落到自己写代码还是老一套把判断堆成 if-else 金字塔所有逻辑挤在一个类里甚至自己都说不清这些数据到底归谁管。作为一个被历史代码毒打过、也亲手写过烂设计再推倒重来的开发者我越来越觉得这套理论最容易被低估也最容易被误解。只记住什么是封装、什么是继承、什么是多态这几个名词而不去搞清它们到底在解决什么问题那这些概念就只是面试题的答案不是写代码的工具。下面我把三大特征拆开揉碎从需求场景、底层原理、代码落地到常见坑点逐层讲清楚。不管你是刚开始学类语法、准备面试还是已经在项目里写过一堆想重构的代码应该都能找到一些能直接拿来用的思路。1. 先别急着背概念三大特征到底在解决什么问题1.1 从一段写起来很爽、维护起来想哭的代码说起假设要做一个消息通知模块最初的诉求很简单给用户发一封邮件。你会很自然地写出这样一个函数。def send_email(user, message): # 连接邮件服务器 # 组装 HTML 模板 # 发送并记录日志 ...但需求马上就会变。市场部要发短信客服要发站内通知运营还要做应用推送。绝大多数人的处理方式是在同一个函数里加参数、加分支def send_notification(user, channel, message): if channel email: # 发邮件的几十行逻辑 ... elif channel sms: # 对接短信网关 ... elif channel wechat: # 调企微、公众号接口 ...这段代码功能上能跑通但每一次改动都让人头皮发麻。加一个新的渠道就得在同一个函数里再堆一种分支改一个渠道的消息模板解析就要担心会不会影响另外两个渠道。等到所有渠道的模板、重试策略、失败处理、日志格式全部纠缠在一起一百行的函数变成五百行谁都不敢再去动这个文件。你会发现代码最初的自由换来的恰恰是后来的寸步难行。这就是没有特征约束时代码自然演化的结果。面向对象三大特征不是哪本教科书拍脑袋定出来的教条它们是对代码为什么会腐烂这个问题从三个角度的回答。理解了这个前提后面所有知识点都会变得顺理成章。1.2 封装、继承、多态分别回答什么问题我习惯把这三个特征理解为三把不同用途的尺子而不是三个必须依次背诵的知识模块。封装回答的是数据和行为归谁管、谁能碰。解决的是状态散落、逻辑互相踩踏的问题。继承回答的是多个类型之间的共同点怎么抽出来复用。解决的是重复代码不断膨胀的问题。多态回答的是调用方能不能只面向抽象接口编程、不用关心具体类型。解决的是分支条件爆炸、扩展要靠改旧代码的问题。写代码时你大概率不会喊一句我要用三大特征了而是在遇到具体痛点时自然地抽出对应的尺子。发现自己写的类一堆公开字段、外部随处可改、改出问题还没人负责这时该抽出的尺子是封装发现两个类之间有大段一模一样的代码该考虑的是继承看到一组连续 if-else 在做同一件事的不同渠道或不同类型该考虑的是多态。把三大特征当成一套动态的设计决策框架而不是三个孤立的知识点很多后续的面试题和代码评审意见都能在这个框架里对上号。2. 封装不是让你把所有字段都设成 private2.1 第一层隐藏内部状态保证对象不变式先讲一个我在面试时经常用来区分懂封装和背概念的例子银行账户。很多人一说到封装第一反应是把字段定义成 private再加 getter/setter于是写出了这种代码public class BankAccount { private double balance; public double getBalance() { return balance; } public void setBalance(double balance) { this.balance balance; } }这其实只是用 private 包了一层外表数据仍然可以被外部任意改写。真正的封装要管的不是能不能访问而是访问的方向会不会破坏对象的一致性。account.setBalance(-100)这种调用在现实世界里根本不应该发生但在这个代码里它畅通无阻。看看下面这个收敛的版本class BankAccount: def __init__(self, initial_balance: float): self._balance 0.0 self.deposit(initial_balance) property def balance(self) - float: return self._balance def deposit(self, amount: float) - None: if amount 0: raise ValueError(存款金额必须为正数) self._balance amount def withdraw(self, amount: float) - None: if amount 0: raise ValueError(取款金额必须为正数) if amount self._balance: raise ValueError(余额不足) self._balance - amount这里有几个容易忽略但很重要的细节。第一余额字段没有提供 setter外部只能通过 deposit 和 withdraw 两个受控方法改变状态所有校验逻辑因此收口到了这两个入口。余额永远不会变成负数这就是所谓的不变式invariant。第二构造函数直接调用 deposit 而不是简单赋值初始化参数也走同一套校验逻辑防止绕过规则造出一个非法账户。第三balance 以只读属性的方式暴露出去查询和修改被明确区分开。封装的第一层本质上就是状态变化必须有闸门而不是搞一堆看似 private 的字段再配一堆公开 setter。相比之下那种全字段私有 全字段 setter的写法只是把门牌挂上了门锁根本没装。2.2 第二层行为归口防止散弹式修改第二层封装更隐蔽很多人要到项目后期才体会得到。它指的不只是藏字段而是把某个职责相关的所有行为都收进一个类的内部让外部依赖一个稳定接口而不是依赖内部细节的排列组合。举个例子。电商项目里积分计算规则原来写在下单模块运营活动模块也要计算积分客服查询积分时又有一段换算逻辑。三个地方各算各的一旦积分规则调整就得满项目找这些零散片段漏改一处就出生产事故。正确做法是定义一个 UserPoints 类把获取可用积分扣减积分查询积分明细这些行为全部收口到类的方法里其他模块只调用方法不直接读写积分字段。这样设计的直接收益是积分规则变了改动只局限在 UserPoints 内部其他模块的代码一行都不用动。我见过大量代码异味都源于封装层破洞——同一个实体的字段散落在好几个模块里被各自操作改需求时根本不知道去哪里改。封装的价值不是整洁癖而是让修改成本变得可预期。说得直白一点这就像公司的财务制度不会让每个员工直接去动公司账户所有报销都通过财务部门走流程。哪里出了问题追责和修改都只在财务这个口子发生。分布式地改一堆字段最后一定会在某个加班的深夜变成大型灾难现场。2.3 Python 没有 private怎么守住封装第一层的 BankAccount 例子用的是 Python但 Python 并没有 Java 那套 private/protected/public 访问控制也没有 C 的 friend 机制。这也是很多刚接触 Python 的人困惑的点语言层面根本没有私有那封装怎么谈Python 的规则实际上是靠约定立起来的。单下划线前缀_balance表示这是内部实现请勿外部使用双下划线__balance会触发名字改写name mangling让外部不经意间访问不到但真要绕过还是能绕过。它更像是一层提示而不是一堵墙。所以我在团队里长期执行的规则是三条以下划线开头的属性一律视为私有外部只准调用公开方法用 property 控制读写有校验逻辑的字段绝不直接暴露 setter公开 API 要少而稳定内部实现怎么重构都不应该影响调用方。语言层面的不设防在单人项目里无所谓在团队项目里就得靠这些约定把边界立起来。否则迟早有人会在业务代码里写出account._balance -100这种操作前面做的所有校验瞬间变成摆设。封装这道防线既靠语法也靠纪律。提示不要一见封装就把所有字段配一套 getter/setter。字段允许外部直接读取和改写那换成属性毫无意义只会增加几十行噪音代码。先想清楚这个字段到底允不允许外部直接看到再决定要不要暴露。2.4 封装不只是藏数据还藏流程还有一层容易被忽略封装同样适用于方法。一个类内部动辄五六个步骤的流程如果全部暴露成公开方法外部调用方就必须理解这些步骤的先后关系改动时牵一发动全身。更好的做法是只公开一个稳定入口把内部步骤降级为私有方法。外部拿到的是一个黑盒只负责调用完全不需要知道里面怎么编排。这种黑盒思维是整个面向对象设计的底层共识也是后面继承、多态能够成立的基础——调用方越不了解内部细节设计就越有弹性。3. 继承用得好是复用用不好是灾难3.1 继承的本质抽出共同的骨架继承的适用前提是is-a关系子类确实是一种特殊的父类。比如信用卡支付是一种支付方式企鹅是一种鸟在这种关系下父类定义公共骨架子类负责填充差异。先看一个支付场景。from abc import ABC, abstractmethod class Payment(ABC): def __init__(self, amount: float): if amount 0: raise ValueError(支付金额必须大于 0) self._amount amount abstractmethod def pay(self) - str: ... class CreditCardPayment(Payment): def pay(self) - str: return f信用卡支付 {self._amount} 元 class AlipayPayment(Payment): def pay(self) - str: return f支付宝支付 {self._amount} 元这里有几个关键点值得展开。第一父类 Payment 中可以承载所有子类共用的逻辑比如金额合法性校验、日志记录、支付超时处理必须由子类决定的差异行为就用抽象方法占住位置。这样共同行为和差异行为被放在清晰的位置分别管理。第二不要把抽象方法里的NotImplementedError当占位符就完事。直接抛 NotImplementedError 是运行时才报错做实验可以上工程最好用 ABC 模块让未实现的方法在类构造阶段就被约束住错误暴露得越早修复成本越低。第三继承出来的子类要能顺畅替换父类这就是里氏替换原则LSP父类对象出现的地方换成任意子类对象都不应该出问题。上面的 CreditCardPayment 和 AlipayPayment 都能被当作 Payment 使用这种可替换性正是后面多态得以成立的基础。如果子类覆写方法时悄悄改变了父类约定的语义比如把支付金额必须大于 0变成允许负数那多态就是一场灾难。3.2 构造顺序和 super()一个容易被忽略的细节在 Java 和 C 里子类构造时父类构造函数会自动先执行这是语言帮你做的。但在 Python 里子类的__init__不会被自动调用你得显式调用super().__init__()而且调用时机完全由你自己决定。class BasePayment: def __init__(self, amount: float): print(BasePayment.__init__) self._amount amount class CreditCardPayment(BasePayment): def __init__(self, amount: float, card_number: str): super().__init__(amount) # 先初始化父类字段 self._card_number card_number如果你的子类初始化逻辑依赖父类已经建立好的状态super().__init__()就必须放在子类代码最前面如果子类要先做自己的校验再初始化父类就先写校验再调用 super。这个顺序问题在实际项目中很常见尤其那些名字里带 Builder、Manager 的类初始化顺序错了经常会出现属性不存在或者状态被悄悄覆盖的诡异现象。另外提醒一下多继承里的方法解析顺序MRO。Python 在处理菱形继承时会按 C3 线性化算法决定调用顺序哪怕简单的多继承场景执行顺序也不总是直观。我的经验是能避免多继承就尽量避免它的收益通常没有想象中那么大付出的可读性代价却实实在在。真正需要组合多个能力时优先用后面的组合方案。3.3 为什么组合在很多时候比继承更稳我在代码评审里看过太多为了复用而继承的设计。最典型的就是各种动物分类树class Animal: def eat(self): ... def run(self): ... class Bird(Animal): def fly(self): ... class Penguin(Bird): def __init__(self): self.can_fly False看似合理但一旦加入游泳下蛋鸣叫这些特征分类很快就会乱套鸭子会游泳也会飞鸵鸟不会飞但跑得快蝙蝠会飞却不是鸟。如果硬用继承表达这种感觉就只能不断造父类组合或者往基类堆一堆只有少数子类才需要的空方法。继承因此从复用工具变成了设计枷锁。继承带来的问题根源在于它把类型分类和代码复用混在一起了。分类是概念层面的复用是行为层面的两者并不总同构。于是业界反复强调组合优于继承composition over inheritance。组合的思路是把可复用的行为拆成独立组件用拥有关系代替is-a关系。class Flyable: def fly(self): print(正在飞行) class Swimmable: def swim(self): print(正在游泳) class Duck: def __init__(self): self.fly_behavior Flyable() self.swim_behavior Swimmable() def fly(self): self.fly_behavior.fly() def swim(self): self.swim_behavior.swim()对比动物分类树组合的好处是权责清晰、改动隔离如果飞行算法变了只需要替换 Flyable 的实现Duck 类完全不用动。不用为了一个能力去继承一整套父类字段和方法。3.4 什么时候依然值得继承说了一堆继承的坑也不要矫枉过正。真正具备两个条件时继承依然是高效方案一是is-a关系非常稳定不会因为后续需求突然翻转二是父类确实承载了很大一部分共用逻辑比如基础设施类的模板方法Template Method、统一生命周期管理这类场景。我自己的判断标准是如果未来可能新增的不是新的子类型而是原有类型的另一种变化那继承很可能就是一个会在扩展时爆炸的设计。相反如果自然分类本身就贴合领域模型比如图形系统里的矩形、圆形、三角形继承就非常顺手。别把组合优于继承理解成永远禁止继承它说的是在行为复用场景下组合优先在稳定的分类场景下继承依然是正常选项。4. 多态让调用方只认接口不认实现4.1 从类型判断满天飞说起回到开头那个通知模块。if-else 版本最大的问题不是代码长得难看而是每次新增渠道都要回到源头修改。一段时间之后函数会变成这样def send_notification(user, channel, message): if channel email: ... elif channel sms: ... elif channel wechat: ... elif channel dingtalk: ... elif channel push: ...把这段代码反向推导一遍假设每种渠道已经被封装成独立的发送器类每个类都提供统一的send(user, message)方法那调用方关心的事情其实只剩一个——它能发就行。至于里面是连着邮件服务器还是走短信网关调用方完全不需要知道。这就是多态要提供的效果。要写成代码其实很简单class EmailSender: def send(self, user, message): print(f给 {user.email} 发送邮件{message}) class SmsSender: def send(self, user, message): print(f给 {user.phone} 发送短信{message}) def notify(sender, user, message): sender.send(user, message) # 调用方不关心具体类型注意两个版本的本质差异。if-else 版本里调用方必须完成识别类型 选择分支 调用对应逻辑三步多态版本里调用方只需要做最后一步。类型分支被拆散进各个类自己的 send 方法里每个渠道的代码都回到了自己该在的地方。新增渠道时主流程一行不用改只需要多一个类实现 send。这就是多态对付分支爆炸的方式。4.2 动态绑定多态底层在发生什么理解多态不能只停留在代码风格层面还要知道运行时到底怎么工作。当代码执行到sender.send(user, message)这一句时编译器或解释器只知道 sender 在声明层面大概是什么类型但真正调用的 send 方法是在运行那一刻根据 sender 对象的实际类型决定的。这个机制叫动态绑定dynamic dispatch。在 Java 里普通方法默认就是虚方法天然支持动态绑定在 C 里只有加了 virtual 关键字的方法才参与动态绑定在 Python 里没有虚不虚的说法一切方法调用天然是动态的。这个概念如果用生活类比你给一家店打电话说我要一杯咖啡电话那头的执行者到底是店员、店长还是自动售货机你并不关心你只需要对着点单这个动作操作接电话的具体对象在运行时才确定。动态绑定是多态区别于重载的核心。重载是在同一个类里定义多个同名方法靠参数个数和类型在编译期区分重写是子类重新实现父类方法靠运行时对象类型区分。真正支撑面向对象三大特征里可替换性的是后者。4.3 Python 的鸭子类型和 ProtocolJava 和 C 要实现多态通常要求子类显式继承抽象类或者实现接口。而 Python 因为有鸭子类型在语言层面允许你绕过继承直接实现多态只要一个对象有 send 方法不管它有没有继承 Send 相关父类都可以传进 notify。class WeComSender: # 没有继承任何父类 def send(self, user, message): print(f通过企业微信发送给 {user.user_id}) notify(WeComSender(), user, 你好)这种灵活性的代价是如果传进来的对象没有 send 方法要等到运行时才会报 AttributeError。所以有些对接口边界要求严格的团队会引入 typing.Protocol 做结构化子类型检查让长得像接口的对象也有正式契约可查。from typing import Protocol class MessageSender(Protocol): def send(self, user, message) - None: ... def notify(sender: MessageSender, user, message) - None: sender.send(user, message)这样既保留了鸭子类型的自由又让类型检查器能在编码阶段提示问题。不过无论是哪种写法多态的前提都是一样的所有实现要遵守同一个语义。send 在邮件实现里是发邮件在短信实现里就是发短信行为语义不同但动作一致。如果一个类里的 send 干的事和其他实现完全不是一回事那就不是多态而是踩踏公共方法名。提示多态的价值在于调用方不关心实现。因此设计接口时接口的粒度应该以调用方的需求为准而不是以尽量覆盖所有功能为准。一个接口方法越聚焦实现它的类就越容易替换。4.4 从抽象方法到接口设计思想的演化多态落地依赖一个稳定的类型契约。早期设计习惯把契约放在抽象基类里比如前面 Payment 的抽象方法。抽象基类的好处是除了约束方法签名还能顺便承载公共实现比如金额校验、日志记录。但坏处也很明显抽象基类容易被当成装杂物的大箱子今天加一个公共字段明天加一个公共方法后天发现这个方法只对部分子类有意义于是又塞进一堆 if isinstance 判断。接口的设计初衷就是切断这个趋势接口只声明做什么不负责怎么做。Java 里这样表达public interface MessageSender { void send(User user, Message message); } public class EmailSender implements MessageSender { Override public void send(User user, Message message) { // 邮件实现 } }接口本身没有可继承的公共字段没有已经写好的方法体只有契约。实际项目里我更倾向于接口负责描述能力组合负责承载实现细节这也是现代框架里面向接口编程成为标配的原因。所谓面向接口编程重点不是语言里多了 interface 关键字而是让调用方只依赖抽象描述、不依赖具体实现。这样实现类可以随便换调用方一行都不用改。封装解决了内部怎么变都行的问题多态解决了外部怎么换都不慌的问题两者正好是配套的。5. 三大特征怎么一起干活一个实际重构案例5.1 需求背景一个不断膨胀的运费模块理论讲再多不如看一个完整的演进过程。假设业务方提了一个需求订单要按照配送方式计算运费。旧代码长这样def calc_shipping_fee(order, delivery_type): base 10 if delivery_type standard: if order[total_weight] 10: base 5 elif delivery_type express: base 20 if order[region] remote: base 30 elif delivery_type same_day: base 50 if order[total_price] 199: base 20 return base这个函数要不了几周就会变成几百行因为每种配送方式都有各自的时效、重量、地区、价格门槛判断。更麻烦的是订单的字段被到处直接读取运费计算逻辑和订单本身的数据形状紧密耦合改一个字段名要牵连无数处。5.2 用封装给订单数据建一道防线第一步把订单变成一个封装好的类class Order: def __init__(self, total_weight: float, total_price: float, region: str): self._total_weight total_weight self._total_price total_price self._region region property def total_weight(self): return self._total_weight property def total_price(self): return self._total_price property def region(self): return self._region def is_free_shipping(self) - bool: return self._total_price 199此时运费计算模块不再需要知道订单内部字段怎么存储只需要调用公开方法比如 is_free_shipping。订单怎么判断免运费是一次性写死在属性里还是通过优惠活动动态计算都是订单自己的事。这一步做完后面所有配送子类的 calculate 方法签名都稳定了传 Order返回 float。5.3 用继承抽取出运费的共同流程第二步把计算运费这个流程框架提出来。大多数配送方式都遵循一个流程读取订单信息、计算基础运费、按规则加价、应用优惠、返回最终价格。把流程放进基类把差异点交给子类。这一步完全可以用策略对象加函数组合实现不一定要继承这里先用继承演示因为它和三大特征的官方叙述更贴合。class DeliveryCalculator: def calculate(self, order: Order) - float: fee self.base_fee(order) fee self.surcharge(order) fee self.apply_discount(order, fee) return round(fee, 2) def base_fee(self, order: Order) - float: raise NotImplementedError def surcharge(self, order: Order) - float: return 0.0 def apply_discount(self, order: Order, fee: float) - float: return fee class StandardDelivery(DeliveryCalculator): def base_fee(self, order: Order): return 15.0 if order.total_weight 10 else 10.0 class ExpressDelivery(DeliveryCalculator): def base_fee(self, order: Order): return 20.0 def surcharge(self, order: Order): return 30.0 if order.region remote else 0.0 class SameDayDelivery(DeliveryCalculator): def base_fee(self, order: Order): return 50.0 def apply_discount(self, order: Order, fee: float): return fee - 20.0 if not order.is_free_shipping() else fee注意 surcharge 和 apply_discount 在基类里给了默认实现子类只覆写自己有差异的部分。没有差异的子类直接沿用父类默认行为这就是模板方法模式Template Method和继承配合的典型用法。5.4 用多态替换调用端的 if-else第三步把调用端的分支判断去掉。原来主流程要知道用户选的是 standard 还是 express现在只需要持有一个 DeliveryCalculator 抽象引用运行时具体是谁由创建处决定。def build_calculator(delivery_type: str) - DeliveryCalculator: mapping { standard: StandardDelivery(), express: ExpressDelivery(), same_day: SameDayDelivery(), } return mapping[delivery_type] def checkout(order: Order, delivery_type: str) - float: calculator build_calculator(delivery_type) return calculator.calculate(order)注意这里的 build_calculator 相当于一个工厂它依然有分支但分支已经收敛到一个位置了。业务模块中的 checkout 不再出现配送方式的判断。之后新增一种配送方式时只需要新建一个 DeliveryCalculator 子类并在 mapping 里注册一条。原有类和 checkout 一行都不用改。这恰恰体现了软件设计里著名的开闭原则对扩展开放对修改关闭。而开闭原则能成立靠的正是封装、继承、多态三个机制共同作用——封装让订单内部稳定继承把共同流程固定住多态让新增类型不需要动旧代码。5.5 重构后的收益以及一个新问题重构之后怎么算运费和用哪种方式算运费被彻底分离。测试时可以针对每个子类单独写单元测试也可以对同一个配送方式换不同订单数据测边界再也不用靠重构前那个巨大 if-else 去枚举各种组合。代价也真实存在类数量变多第一次阅读时追踪调用链路的成本提高了。这是多态化改造的通用代价换来的是未来修改的局部成本大幅降低。实际工程里最怕的不是代码跳转多一层而是改一处旧逻辑要连带触碰十几个分支。提示如果你只是加了一种配送方式不要急着把那组 if-else 改成多态。多态的价值在于分支频繁扩展和类型组合多样化这两个前提。一个永远不扩展的分支用 if-else 反而是最清晰的写法。过度设计同样是债。6. 面试与实战中常见的理解误区6.1 重载也是多态到底该怎么回答这是个经典面试题。很多教材把重载称为编译时多态或静态多态于是学生背下来面试时回答多态包含重载和重写。这个说法在学术语境里可以接受但面试官如果想考察你对面向对象三大特征的理解通常期待的是运行时多态也就是通过继承或接口实现、在运行时根据对象的实际类型完成动态绑定。我的建议是分两层回答先说明重载是编译期根据参数列表选择的静态绑定重写是运行期根据实际对象选择的动态绑定再补一句三大特征里说的多态通常指后者。这样既展示了知识面又不容易被追问细节时露怯。在 Python 语境下还有一个额外问题Python 原生不支持传统意义的方法重载同名方法只保留最后一个定义。如果你见过从 Java 转 Python 的人写代码会发现他们很容易下意识写多个同名函数然后第二个把第一个覆盖掉。正确的 Python 做法是用默认参数、可变参数或关键字参数来表达不同的调用形式。6.2 常见误区对照表用一张表把评审里经常出现的问题整理出来误区正解封装 私有字段 getter/setter封装是数据 行为归口getter/setter 只是实现手段之一继承 代码复用代码复用是继承的副产品is-a 关系才是继承的合法性前提多态 重载重载是静态绑定接口 实现的运行时动态绑定才是这里说的多态组合优于继承 禁止继承组合是行为复用场景下继承的替代方案is-a 关系稳定时继承依然合适加了接口就一定是好设计接口也要控制数量一个不再扩展的接口很可能只是过度设计Python 没有 private所以不能封装语言约束不如规约约束property、受控方法加下划线约定足够用这些误区在概念题里可能不会被立刻发现但在代码评审里会直接变成难以维护的设计。背概念能过面试能持续把正确设计写出来才算真正掌握。6.3 怎么判断自己真的把三大特征用懂了我个人有一个很简单的检验方法找一个正在写的模块问自己三个问题。第一个问题如果要修改这个模块里的一项内部规则需要改动几个地方答案在两个以上说明封装没做好这项规则相关的数据和行为没有收口到一个位置。第二个问题如果要新增一个类型需要改动几个已有类如果除了新增类还要改调用方、改分支判断说明多态没有发挥效用。第三个问题如果复用一段逻辑时必须在继承一个庞大父类和复制粘贴一小段代码之间二选一说明中间缺了一层抽象这层抽象往往是接口或组合组件。这三个问题不是为了让三大特征几个字出现在代码里而是为了让封装修状态、继承管共性、多态管变化成为你设计时的自然方向。我在实际项目里的经验是真正用顺手的团队不是写类的时候想着我要用特征而是在处理某个改动人太多的地方这个痛点时顺着三大特征的自然指引把它们补起来。最后再分享一个小技巧。学习三大特征不要只在上课时练语法最好的练习是找一个有五年历史的旧模块尝试在不改变对外行为的前提下把最丑的那段 if-else、最散的那组字段、最深的那棵继承树分别用封装、多态、组合的思想重构一遍把重构前后的 diff 保存下来过三个月再回头看你会对这三个特征有完全不一样的理解。