恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
别再写贫血模型了:把对象当人,用职责协作重构OOP代码
首页
资讯中心
/
别再写贫血模型了:把对象当人,用职责协作重构OOP代码
别再写贫血模型了:把对象当人,用职责协作重构OOP代码
发布时间:2026/10/11 14:47:56
前阵子又看到乔布斯那句关于面向对象编程的旧话大意是说对象应该像活生生的人一样有自己的生活、自己的职责、也知道怎么跟别人打交道。最早学编程那几年我把这句话当名人名言看读完就忘。后来在好几个项目的业务代码里摸爬滚打写坏了也看坏了无数个类才慢慢回过味来当年课上教我们背的那套“封装、继承、多态”语法上全对方向上却可能从一开始就偏了。这篇文章就是想把这件事聊透。我会先解释“对象像人”这个隐喻到底有多准再拆一拆教科书那套教法到底哪里带偏了人然后给出一个能直接上手的重构案例以及几个判断“伪面向对象”的代码味道。适合那些觉得自己明明学了OOP一写大型项目却还是满脑子if-else和getter链的开发者。1. 对象像人一样这个隐喻到底在说什么乔布斯那番话最值钱的部分不是“对象”这两个字而是“像人一样”。你可以拿一个你熟悉的人来对照同事、朋友、快递员。你跟快递员打交道的唯一方式是把包裹交给他说“帮我送到这个地址”。你不会去追问他的肌肉怎么发力、他的导航用的是哪个App、他今天心情如何。你只关心一件事消息能不能被正确处理。面向对象编程里的对象本质上也应该是这样。一个对象拥有自己的内部状态这些状态在正常情况下不允许外界直接窥探和修改它对外暴露的是一组能力——能接收什么消息、会给出什么响应。别人不需要知道它内部怎么存数据、怎么算结果只需要知道“找它能办什么事”。但翻翻大多数代码大家写的对象完全不是这么回事。很多类被当成了纯数据容器字段是public或者半public的getter/setter一行接一行逻辑全部堆在外部Service里。一个对象该有的“反应能力”一点没有像一本摊开的账本谁都能翻、谁都能改。等出bug的时候一查发现几十处都在直接改同一个对象的状态谁也说不清是哪里改错的。把对象当“人”来看这个隐喻能立刻折射出四个工程问题第一人有隐私也有边界。你不能随便摸别人的钱包你也就不该随便访问别的对象的内部集合。第二人都是通过语言协作的而不是通过解剖对方实现的。你请人帮忙时只需要发一条消息代码里就该是“调用一个有意义的行为方法”而不是“掏出对方的一个字段做判断”。第三人有自己的立场自己能决定要不要答应你的请求。对象同样应该自己守护自己的约束规则而不是被外部逻辑强行修改成非法状态。第四人有专长分工不同。一个对象不负责全流程它只负责自己那一摊其他的事情委托给合适的对象。这套思路听起来不复杂但几乎每一个反例都在说明同一件事我们学OOP的时候最早被灌输的其实是“类和对象长什么样”而不是“对象和对象之间怎么说话”。后者才是“面向对象”的“面向”两个字真正落脚的地方。2. 教科书的三板斧是如何把封装、继承、多态学拧的大多数中文编程教材讲OOP结构惊人地一致先讲类和对象的概念然后逐一介绍封装、继承、多态配上一两个“学生”“动物”“形状”的例子。这套结构本身没问题问题出在它把三个抽象概念全部下沉成了语法教学。2.1 封装学成了私有字段漏掉了不变量保护很多初学者理解的封装就是“private关键字加getter/setter”。你问他封装的好处他能背出“隐藏内部实现、保护数据安全”但一打开他写的类清一色全是这种class BankAccount: def __init__(self): self._balance 0 def get_balance(self): return self._balance def set_balance(self, value): self._balance value这个类的平衡任何人都能直接改成任意数字。你说它封装了吗语法上封装了_balance确实是私有字段。但语义上完全裸奔账户余额“不能为负数”“超过某额度需要风控审批”这类规则一个都没有落地在对象里。真正的封装保护的不是字段而是不变量。所谓不变量就是无论外部怎么调用对象都必须保持成立的一组规则。比如余额类的不变量是“余额始终大于等于零且所有变动都必须有流水记录”。实现方式应该是这样class BankAccount: def __init__(self, holder): self._holder holder self._balance 0 self._transactions [] def deposit(self, amount): if amount 0: raise ValueError(存款金额必须为正) self._balance amount self._transactions.append((deposit, amount)) def withdraw(self, amount): if amount 0: raise ValueError(取款金额必须为正) if amount self._balance: raise ValueError(余额不足) self._balance - amount self._transactions.append((withdraw, amount))这才叫“像个活人”——任何想动余额的请求都必须经过账户自己点头。而不是把余额字段掏出来让外面的代码自己算。很多人工作了好几年还在写setter满天飞的类真不是不够努力是第一步就被教歪了。顺便说一句setter并不是绝对不能用。对于真正独立的值对象比如一个纯配置项setter也无伤大雅。关键是你要能区分“这是一个值”和“这是一个有行为规则的对象”。前者允许外界直接修改后者必须把行为收回来。2.2 继承学成了代码复用器漏掉了类型抽象教材里讲继承最常见的例子是猫和狗都是动物它们都会叫所以让Animal类提供一个speak()的默认实现猫狗分别重写。这个例子本身没问题但它让很多人形成了一种根深蒂固的误解继承的目的就是代码复用——把公共代码放父类子类共享。这个误解的代价我在代码评审里见过太多次。为了复用两个方法硬让ClassB继承ClassA实际上两个类八竿子打不着。继承带来的不只是方法还有父类的所有字段、所有约束、所有生命周期规则。当父类被改动时所有子类都受影响这种耦合在项目后期就是定时炸弹。继承的本质是表达“is-a”关系子类不仅能复用父类代码更关键的是子类承诺了“你可以像对待父类一样对待我”。这涉及多态、涉及类型抽象、涉及接口设计代码复用只是顺带的福利不是主要目的。所以现在业界普遍强调组合优先于继承不是贬低继承而是提醒人在你为了省几行代码选择继承之前先想想这两个类是否真的有类型上的父子关系。没有的话宁可把公共逻辑抽成独立的小类通过组合把能力聚合起来。2.3 多态学成了重写方法漏掉了晚绑定和消息分发多态是三个概念里被教得最薄的一个。很多人以为多态就是“子类重写父类方法调用的时候走子类实现”——语法层面确实如此但这个理解把多态变成了一个死板的机制忽略了它真正的思想价值。用“对象像人”的思路重新看多态你会给不同的人发同样的消息但每个人的回应方式不一样。你大喊一声“下班了”程序员收拾键盘健身的人去换衣服带娃的人打开家长群。消息一样收消息的每个人按照自己的方式响应。这才叫多态。class NotificationSender: def send(self, user, content): user.receive(content) class EmailUser: def receive(self, content): self._send_email(content) class SmsUser: def receive(self, content): self._send_sms(content)发送方只需要知道“这个对象能收消息”完全不关心它底层是邮件还是短信。至于最终是邮件通道还是短信通道是运行时由接收者自己决定的。这种晚绑定消息的身份在运行时才解析才是多态的核心。你可以在完全不知道具体类型的情况下让系统自动选择正确的行为。理解到这一层再去看策略模式、观察者模式、命令模式你会有一种豁然开朗的感觉——这些设计模式没有一个不是在“发消息给不知道是谁的对象让它自己干该干的事”。3. 把对象当人协作从问数据到下令做事学歪了OOP的代码一个标志性特征就是满屏的getter链。比如这样city order.get_customer().get_address().get_city()如果你把对象当作人来看这行代码有多荒谬你问一个人今天吃了什么正确的做法是直接问他“你中午吃了啥”而不是把他的胃剖开伸头进去看。get_customer().get_address()这串调用相当于线上剖胃——每一层get都在强行进入别人的私有空间。3.1 Tell, Dont Ask一句话值得贴在显示器边框上有经验的开发者总结过一条原则叫Tell, Dont Ask命令不要询问。意思是一个对象应该“命令”另一个对象去做事而不是先“询问”一连串的数据然后自己代替它做决定。同样拿上面的例子说如果你只是需要把城市名显示在页面上那拆出来也无妨。但如果你的代码是这样的if order.get_customer().get_address().get_city() in restricted_cities: order.get_customer().get_email().send_warning()你本质上是把Customer和Address当成没有脑子的数据结构所有的判断逻辑都堆在外部调用方手里。一旦规则多了这个外部调用方就会膨胀成上帝类。更合理的做法是把“这个用户能不能下单”这件事分包给Customer自己if order.customer.can_place_order(): order.confirm()Customer自己知道自己的地址是否在受限城市、自己的账户是否被冻结、自己的级别是否够资格。外部只是发了一条消息判断和响应都交给了对方。3.2 德米特法则别跟陌生人要东西德米特法则Law of Demeter还有个别称叫“最少知识原则”核心就一句一个对象只应该和它的直接朋友说话不要隔着人跟陌生对象交流。什么叫直接朋友你创建的对象、你通过参数接收的对象、你自身的组成部件。而customer.get_address().get_city()里的city就是陌生对象——你是通过customer间接拿到了address又通过address拿到了city。这一段路径上不仅有知识泄露还有高耦合任何一层的内部结构变化都会波及调用方。把对象当作“人”来思考德米特法则就特别容易理解你买火车票只需要找售票窗口不应该为了买到票去翻售票员的内口袋找他背后的调度系统。3.3 职责不是字段才是类的灵魂最后这一点可能是最反直觉的设计类的时候不要先想它有什么字段要先想它负责什么。传统教学喜欢让你先找名词和动词名词是类动词是方法。这导致大家上来就定义“订单有订单号、有金额、有状态”——字段表先画好再往里面塞操作。于是类在出生那天就只是个容器。如果你从职责出发订单要负责“确认”“取消”“应用优惠码”“计算应付金额”。那么订单类里自然会出现这些方法它内部有没有订单号、用什么数据结构存订单项反而都是可以随时替换的细节。这个顺序一旦反过来代码写出来的味道截然不同。4. 亲手重构一个订单系统把过程式OOP改成协作式OOP理论说多了容易飘下面用一段真实的代码演进过程把前面的思路贯穿一遍。我用Python写一个简化版的订单提交场景尽量贴近业务中常见的模样。4.1 先看一段典型的贫血模型代码下面这段代码没学过OOP的人也能看懂因为它本质上就是面向过程——只不过穿了类的外衣。class Customer: def __init__(self, name, level, address, status): self.name name self.level level # NORMAL / VIP / BLOCKED self.address address self.status status class Order: def __init__(self, customer, amount): self.customer customer self.amount amount self.discount 0 self.status CREATED class OrderService: def submit(self, order): if order.status ! CREATED: raise Exception(只有新建订单才能提交) if order.customer.status BLOCKED: raise Exception(该客户已被封禁) if order.customer.level VIP: order.discount 0.2 elif order.customer.level NORMAL: order.discount 0.05 else: order.discount 0 order.payable order.amount * (1 - order.discount) order.status SUBMITTED return order这段代码的问题一眼就能看出来Customer和Order全都是一堆裸露的字段没有任何业务行为。OrderService像一个大保姆把判断、计算、状态变更全部攥在手里。这种设计一开始写起来很爽逻辑全在Service里想怎么看就怎么看。但随着需求增多你会不断往Service里加if今天加“满减”明天加“优惠券”后天加“黑名单城市限制”。几个月后submit方法变成几百行谁也不敢动。4.2 识别问题对象在当容器逻辑全在上帝手里在动手重构之前先做一次体检。这里的体检结论很明确第一Customer没有对自己负责。它自己是VIP还是BLOCKED它应该比谁都清楚还应该能回答“我能不能下单”“我能拿多少折扣”。现在这些知识全跑到Service的if分支里去了。第二Order没有对自己负责。自己能不能提交、自己的应付金额怎么算本来应该由Order自己决定。现在它像个被摆布的木偶躺在那里等Service改它的字段。第三整个协作关系没有消息。Service直接操作了所有对象的内部没有任何一个“对象被请求去做某事”的表达。4.3 重构把权力交还给对象重构的核心就是把散落在Service里的行为按职责“归还”给每个对象。第一步让Customer拥有回答问题的能力class Customer: def __init__(self, name, level, address, status): self.name name self.level level self.address address self.status status def can_place_order(self): return self.status ! BLOCKED def discount_rate(self): if self.level VIP: return 0.2 if self.level NORMAL: return 0.05 return 0.0第二步让Order掌握自己的状态流转和金额计算class Order: def __init__(self, customer, amount): self.customer customer self.amount amount self.discount_rate 0.0 self.status CREATED def submit(self): if self.status ! CREATED: raise Exception(只有新建订单才能提交) if not self.customer.can_place_order(): raise Exception(该客户无法下单) self.discount_rate self.customer.discount_rate() self.status SUBMITTED property def payable(self): return self.amount * (1 - self.discount_rate)第三步Service彻底退化成瘦入口class OrderService: def submit(self, order): order.submit() return order你看submit方法里除了调用order.submit()之外什么都不用干。以后新增一种客户等级、新增一个折扣规则改的都是Customer内部以后要限制某些地区的客户下单改的也是Customer这个类。OrderService这个“上帝”再也没有存在的必要了。4.4 重构之后需求变更时改哪里一眼就明白我们拿三个典型需求来检验重构前后的差别。需求变更重构前重构后新增一个客户等级“GOLD”折扣率0.3在OrderService的submit里再插一个elif在Customer.discount_rate()里加一个分支新增“最近30天有投诉记录的客户禁止下单”在OrderService里再加一段if还得查询投诉数据在Customer.can_place_order()里加一个判断多注入一个查询接口新增“订单总额超过1000元额外打95折”在OrderService里改payable的计算在Order.payable里加一个规则或者在Order内部调用一个Promotion对象重构前每次变更都要回到主流程里找地方插针重构后变更点基本就是某个对象自己的内部实现。这就是把对象当做人来建模的最直接回报——每个类自己管好自己的事外部不需要关心你那点内部变化。5. 五种疑似OOP、本质还是面向过程的代码味道光会例子还不够得培养出“闻味道”的能力。下面这五种代码味道是项目中反复出现的伪面向对象特征。你在自己代码里如果中了两条以上那基本可以断定你用的不是OOP而是把C语言的思维套上了一层类的壳。味道一getter链比业务逻辑还长。a.getB().getC().getD()这种代码一旦出现意味着整条链上的对象都在裸奔。正确的做法是让拥有数据的那一端提供一个完整的行为或者把整条链封装进一个语义化方法里让调用方只说“我要什么”不说“我怎么拿”。味道二类名以Manager、Service、Util、Helper结尾里面全是静态方法。我不是说工具类绝对不该有但当一个项目里绝大多数业务逻辑都堆在“OrderService”“CustomerManager”里而“Order”“Customer”只剩字段时你实际是在面向过程编程。判断标准很简单如果把这个Service类删掉那些业务对象还剩多少行为如果什么都不剩它们就只是结构体。味道三instanceof和类型判断到处飞。看到这种代码if isinstance(payment, CreditCardPayment): ... elif isinstance(payment, PaypalPayment): ...本质上是把多态白白废掉又回到过程式的分支逻辑上。正确做法是让每个支付方式自己响应一个处理消息比如payment.process()外部一行调用就完了。味道四调用方问了一堆细节替对方做决定。比如判断“这个订单能不能退款”外部代码先把订单状态取出来、再取出退款截止日期、再取出用户支付方式然后自己计算。这全是Tell Dont Ask的反面。判断能不能退款应该直接问订单对象“你能退款吗”它自己最清楚自己的状态和约束。味道五领域对象全面消失DTO/VO一统天下。不少团队为了所谓解耦把数据库表结构直接映射成实体再把实体转成DTO传给别人整个过程中没有任何一个对象有行为。表面上看分层清晰实质上是让数据在一个又一个容器之间搬运。领域规则无处安放最后全落在各层的Mapper和Converter里。这五种味道有一个共同根源大家不是在用“对象”编程而是在用“带类型的数据结构”编程。数据结构当然有价值但那是另一个话题。面向对象的立身之本从来是行为与消息的协作网络。6. 重新校准学习路线真想学对OOP就从这几件事下手聊到这里你也应该意识到了学错OOP不是智力问题而是路线问题。如果你愿意重新校准一下学习路径我有几个建议都是自己走过来的经验按顺序做效果比较好。先理解消息传递再理解语法糖。找最早期的面向对象语言资料看它们怎么描述“消息”。你会发现早期文献里几乎没有“调用方法”这个说法全是“向某对象发送消息”。方法调用只是消息传递在主流语言里的一种实现形式。带着这个视角再回来看Java、Python、C你会自动理解为什么多态重要、为什么接口比实现类重要、为什么面向接口编程。用“发消息”而不是“取数据”的方式写一周代码。给自己定一个硬性规矩除非是纯粹的值对象否则任何对象都不允许向外暴露getter需要数据的话就设计一个有语义的行为方法命令对方把结果算好给你。这一周会非常痛苦因为你会发现很多地方不知道该怎么写、该把职责放到哪个对象上。但熬过这周你写代码时脑子里就自动开始画“谁给谁发消息”的图了。刻意练习职责拆解。随便找一个你写过的小项目把一个几百行的Service类拆开。先列出它所有职责然后逐个问这件事应该由哪个领域的对象自己负责拆完再合并同类项你会得到一组更小、更内聚的类。这个过程比读十本设计模式书都管用。用一句话验收每一个类。每写完一个类逼自己用一句话说清楚“这个类负责什么它接收哪些消息会给谁发消息。”如果这句话说不出来说明这个类大概率是拼凑出来的。我见过太多三百行的类写的人自己都解释不清它到底是干嘛的。最后再分享一条个人体会我后来带团队做代码评审时几乎不再跟人争“这里该不该用继承”“那里是不是套了工厂模式”这类具体问题。我只看一件事——代码里对象与对象之间是不是在好好“说话”。凡是消息清晰、职责分明的设计哪怕不套任何设计模式后续需求变更时也稳得很。凡是字段裸露、逻辑外置的设计就算把设计模式目录背得滚瓜烂熟也掩盖不住它骨子里的过程式本质。乔布斯那句话没骗人对象确实像人。你什么时候觉得自己是在给一群分工明确的人派活而不是在一堆数据结构之间搬砖你就真正开始面向对象了。