直接做技术的人,多少都经历过那段“明明能跑,就是不敢改”的日子。我最早写代码,清一色的C语言,几百行一个文件,从上往下顺着一口气写完,当时觉得逻辑挺清楚。直到有一天,产品经理提了个需求,说要改一下用户积分规则,我翻了半天代码,发现改一个计费函数,结果牵扯到七个地方的全局变量,最后修完还冒出来一个诡异的数据错乱。那一刻我才意识到,不是我代码写得不好,是我的思维方式压根不适合复杂业务。
后来转到Java和Python项目里,跟着老同事一顿重构下来,才算把“面向对象”这四个字真正吃透。今天这篇文章,我不打算讲那种教科书式的定义,而是想顺着自己踩过的坑,聊聊从面向过程到面向对象到底是怎么变过来的,为什么说这个转变不是学一门新语法,而是改一套思维方式。无论你是刚入门想搞清楚两者区别的新手,还是写了两三年业务代码却总觉得在“用Java写C”的同行,这篇文章应该都能给你一些启发。
1. 内容整体设计与思路拆解
1.1 两种范式的底层逻辑差异
先别急着背概念。面向过程和面向对象的本质区别,其实一句话就能讲明白:面向过程关心“做什么”,面向对象关心“谁来做”。
面向过程的代码,像是一张菜谱。洗菜、切菜、下锅、翻炒、装盘,每一步都写得明明白白,程序就是按顺序执行这一串步骤。数据(比如菜的种类、重量)被扔在旁边的篮子里,哪个步骤需要了,就拿出来用一下。这种模式的优点是直观、效率高、贴近计算机底层的运行方式,所以C语言、汇编这类偏底层的语言,天然就是这种思路。
面向对象则像是请了一个厨师团队。每个厨师各管一摊,有人只负责切菜,有人只负责掌勺,有人只负责摆盘。你要做什么菜,不用自己动手,而是告诉对应的厨师:“帮我把这条鱼处理一下”。厨师自己知道怎么处理鱼,鱼的状态(新不新鲜、多重、什么品种)都存在厨师那儿。对外界来说,你不需要关心“处理鱼”到底要几步,你只需要知道“这个厨师能处理鱼”。
这两者的区别,看起来只是代码组织方式不一样,但落到实际项目里,就会带来一个非常关键的分水岭:当需求发生变化时,你要改的地方是局部的还是全局的。
面向过程的代码里,数据和操作是分离的。数据躺在结构体里,操作散落在各个函数里。一旦数据格式变了,或者某个操作逻辑变了,你可能要把所有用到这个数据的函数全翻一遍。这就是我最初改积分规则改到崩溃的根本原因。
面向对象的代码里,数据和操作被绑在了一起,放进了一个“类”里。类对外只露出几个接口,内部怎么存数据、怎么算,外部一概不用关心。将来需求变了,只要类的内部接口约定不变,外面调用的地方一行都不用改。
1.2 从“先做需求分析”变为“先做对象建模”
我观察过很多转面向对象的开发者,他们的一个典型问题是:拿到需求,脑子里第一反应还是“这个功能要分几步”,而不是“这个系统里有哪几个角色”。
这个习惯不纠正,写出来的东西就算有class关键字,本质上也还是面向过程。比如我见过有人写订单模块,建了一个OrderService类,然后把所有逻辑全塞在service方法里——下单、查库存、算运费、扣积分,一个方法三百行。这其实就是把以前的一堆函数搬了个家,换个地方继续堆。
真正的转变,是从需求分析阶段就开始的。拿到一个需求,先不要急着想流程,而是先问自己几个问题:
- 这个系统里有哪几类核心对象?
- 每个对象拥有什么属性(数据)?
- 每个对象能执行什么行为(操作)?
- 对象和对象之间是什么关系?
以“用户下单”这个需求为例。面向过程的思路是:接收前端参数 → 调库存接口 → 算价格 → 生成订单 → 返回结果,清清楚楚五步。面向对象的思路则是:先识别出“用户”、“商品”、“购物车”、“订单”这几个对象,然后逐个分析——用户有地址和余额,商品有库存和价格,订单有自己的状态和金额明细。下单这件事,变成了“用户给订单提供地址”、“商品扣减库存”、“订单计算总价”这些对象之间的交互。
你会发现,面向过程的重点是流程,面向对象的重点是结构和关系。流程会随业务变化频繁调整,而结构和关系相对稳定得多。把一个系统的内核建在稳定的一方上,将来修改时的成本就低得多。
1.3 编程语言选型对理解的影响
很多人纠结Java和Python到底哪一个更适合学面向对象。我的看法是:Java更适合“看到”面向对象,Python更适合“用到”面向对象。
Java是纯面向对象语言,几乎一切皆对象,一个最简单的程序也得先定义一个类。这种强制约束会让初学者被迫按照面向对象的框架去思考,虽然起步时会觉得啰嗦,但好处是规矩,容易养成习惯。
Python则多了一些灵活,它允许你直接写一个脚本、几个函数,不建类也能跑。另一个是Python中的dict、list这种内置结构极其强大,很多在Java里必须先定义类才好传递的数据,在Python里塞进一个字典就完事了。这样一来,容易养成“随手就是字典,反正能跑”的习惯,等代码一长,维护就成了灾难。
所以我给新人的建议是:入门阶段用Java把面向对象的语法和设计原则刷扎实,日常写小工具、做数据清洗之类的就用Python,但尽量也按类和对象的方式来组织,不要仗着脚本语言灵活就怎么方便怎么来。
2. 核心细节解析与实操要点
2.1 类和对象:模板与实例之间到底隔了什么
类和对象的关系,网上最常见的比喻是“图纸和房子”。我在实际教学里发现,这个比喻容易让人误以为类就是“一份文件”,对象就是“被这份文件造出来的东西”,于是很多人第一次写的时候,会困惑“我这个类里定义的变量,怎么在别的方法里就找不到了”。
这里得把底层机制稍微说透一点。类,本质上是定义了一组数据和操作的模板,它在程序加载时只存在一份。对象则是按照这个模板在内存里开辟出来的独立空间,每个对象有自己独立的属性数据,但它们的方法代码是共享同一份的。
我用角色扮演来类比:类就像招聘时贴出来的“岗位职责说明书”,上面写着“这个岗位负责什么、需要哪些能力”。对象就是按这份说明书招进来的员工,每个员工相同的岗位、相同的职责,但各自有各自的办公桌、各自的档案。
在写代码的时候需要注意几个细节:
- 类属性(也叫静态属性)归所有对象共享,改一个,全部变。实例属性归每个对象独立,改一个不影响其他。这个不分清楚,很容易出现“改了A的数据,B莫名其妙也跟着变了”的诡异bug。
- 构造方法不是“创建对象”。严格说,对象的内存空间是在调用
new关键字时才分配的,构造方法只是分配完内存后的一个初始化入口。所以如果你在构造方法里写一大堆复杂的业务逻辑,其实是不合适的,因为它的职责是初始化,不是处理业务。 - 方法里访问自己的属性,一定要明确区分局部变量和实例变量。好多bug就出在这,比如写了个
price = price * discount,结果发现参数把实例变量覆盖了。Java里可以靠this来区分,Python里则是self。
2.2 封装:不只是把变量设为private
一提到封装,很多人条件反射就是“私有变量 + getter/setter”,这个理解没错,但有点浅。
真正的封装,目标是隐藏复杂性、保护数据的有效性。getter/setter只是实现手段之一,而且在现代工程实践中,无脑给所有字段都配getter/setter其实是一种反模式,因为那等于把内部细节全暴露出去了,别人还是可以直接操作你的数据,只不过绕了一层壳子。
比较合理的封装策略有这么几个要点:
- 能不给外部访问的,就不给。一个对象的内部状态,如果外部只是需要知道“结果”,那就提供一个行为方法,而不是把数据晒出去让外面自己算。
- setter里要加校验。比如年龄字段,直接在setter里判断一下范围,比在外部业务层到处校验要靠谱得多。
- 返回值尽量用不可变对象或副本。比如返回一个内部
List,如果直接把引用给出去,外部就可以绕过所有校验随意改写。Collections.unmodifiableList或者复制一个新列表再返回,都会更安全。 - 面向接口而非面向实现。这是更高一层的封装,外部依赖的是抽象接口,内部具体是用ArrayList还是LinkedList,将来随便换,调用方无感知。
以我之前做一个订单系统为例,订单对象内部有个status字段,它并不是简单地暴露一个setStatus(),而是提供confirm()、cancel()、pay()这样的行为方法,方法内部去做状态流转合法性校验。比如已经取消的订单就不能再支付,这个规则放在订单对象自己的方法里,而不是散落在service层到处都是。
2.3 继承:正确的使用姿势和致命的误区
继承是面向对象三要素里最容易被误用的一个。很多人喜欢把所有能复用的逻辑全塞进一个基类,然后子类一边继承一边各种覆写,最后基类几百个方法,子类覆写了一半,整个继承体系变成了一个巨型沼泽。
这里要说一下我后来领悟到的原则:继承只用于“is-a”关系,不只是为了省代码。
比如猫和狗都有吃和叫的行为,把它们抽一个动物基类出来是合理的,因为猫真的是动物。但如果你只是为了复用一段计算优惠的逻辑,就建一个BaseOrder类让各种订单去继承,这就需要仔细考虑了——它们share的是行为实现,而不是概念本身,更合适的方式是用组合(把一个优惠计算器对象挂进来)或者接口。
另外,继承还有一个隐藏的维护成本:父类和子类之间的强耦合。父类改了构造方法,所有子类全要跟着变;父类加了一个方法,有些子类还没法提供合理解释。这个耦合一旦蔓延,重构成本极高。我在实际项目里见过一个BaseController里堆了几十个公共方法,后续接手的人根本不敢删任何东西,因为不知道哪个子类在哪天覆写了它。
我的经验是:继承深度超过三层就要警惕,能组合就别继承,能用接口就别用抽象类。很多时候,把公共逻辑抽到一个独立的辅助类或者工具类里再通过组合引进来,比硬建继承关系要稳得多。
2.4 多态:写代码时最划得来的一笔投资
多态这个词,如果从理论角度解释,可以绕晕一群人。但落到实际编码,多态体现为一句很朴素的话:父类型的引用指向子类型的对象,调用同一个方法,得到不同的行为。
再通俗点,就是“同一个动作,不同对象做出来不一样”。动物都会叫,但猫叫是喵,狗叫是汪。你不必在调用处判断“当前这个动物是猫还是狗”,你只需要对着动物这个类型喊一声“叫”,它自己就会按自己的方式叫出来。
多态最大的价值,是让调用方和实现方彻底解耦。举一个我实际项目里的例子:优惠券系统。最开始只有满减券一种,代码这么写的:
if (couponType == 1) { // 满减逻辑 } else if (couponType == 2) { // 折扣逻辑 }后来产品经理说,再加一种“无门槛立减券”,好,我再加一个else if。再后来又加了“首单券”、“会员券”,这个分支越堆越长,改一次全测一次,天天提心吊胆。
面向对象的写法是这样的:定义一个Coupon抽象类,里面有个方法calculatePrice()。满减券、折扣券、无门槛券各自继承并实现自己的计算逻辑。业务层完全不用管它是什么券,拿到一个Coupon对象,“你算一下价格就行”。
public abstract class Coupon { public abstract double calculatePrice(double originalPrice); } public class FullReductionCoupon extends Coupon { // 实现满减逻辑 @Override public double calculatePrice(double originalPrice) { // ... } } public class DiscountCoupon extends Coupon { // 实现折扣逻辑 @Override public double calculatePrice(double originalPrice) { // ... } }后续再加新券种,只需要新建一个类,不影响其他任何代码。这就是多态带来的好处:面对变化,你只需要扩展,而不用修改已有的稳定代码。
2.5 面向对象分类:对象粒度怎么切才合理
热词里有个“面向对象分类envi”,这个词有点串领域了,按理说ENVI是遥感图像处理软件,“面向对象分类”在遥感领域指的是基于图像分割区域(对象)进行分类,而不是基于像素逐点分类。这个和我们编程里的面向对象不是同一个概念,但借这个词我想顺带说一点:任何一个领域,只要涉及“分类”,核心难题都一样——边界怎么划。
在编程里,对象划分太粗,会导致一个类又臭又长,职责混乱;划分太细,会导致类满天飞,对象之间互相调用绕来绕去,代码根本看不懂。
我常用的一个经验法则:一个类应该有一个清晰的“职责主语”。比如User就是干用户的事,Order就是干订单的事,PaymentService就是干支付的事。如果你发现写User类的时候,里面出现了大量和“商品评价”相关的逻辑,那说明这块内容应该拆出去,放到Review类里。
另外,尽量让对象的状态和行为匹配。如果一个类里有一堆了字段,但某个字段只有极少数方法会用到,这个字段大概率放错了地方。相同生命周期的字段和方法,应该待在一起。
3. 实操过程与核心环节实现
3.1 案例背景:从函数堆到对象模型的真实重构
讲了这么多理论,看一个完整的实操案例感受会更直观。我用一个尽量精简但仍保留真实感的场景:用户下单并计算最终实付金额。
需求大概是这样:
- 用户是普通用户,订单可以打折;是VIP用户,折扣力度更大
- 订单包含多个商品,每个商品有单价和数量
- 满200元额外减20元
- 需要计算订单总金额和实付金额
先看面向过程版本的典型写法(用Python伪代码写,方便展示思路):
def calc_order_price(user_level, order_items, total_price): discount = 0.9 if user_level == "vip" else 1.0 final_price = total_price * discount if final_price >= 200: final_price -= 20 return final_price这个函数把所有规则全部耦合在一个方法体里,将来要加一个新的用户等级、改促销规则,或者为不同订单类型设置不同折扣,就必须改这个函数,影响面不可控。而且,total_price是从哪里传进来的?如果计算total_price的逻辑也变了,这里又是另一个改动点。
3.2 重构过程:从三个核心对象开始建模型
用面向对象的思想来重新分析这个需求,我提炼出三个核心对象:
User:用户,有等级(普通/VIP)Product:商品,有单价Order:订单,包含商品明细(商品+数量),负责计算本单总价OrderCalculator:负责计算最终实付金额,处理折扣和满减规则
注意,我把Order和OrderCalculator拆开了,原因是“订单本身的数据”和“订单的计费策略”是两个变化频率不同的东西。订单数据相对稳定,计费策略却经常变化,拆开之后,将来改规则不动订单结构。
具体代码如下:
class User: def __init__(self, user_level: str): self.user_level = user_level class Product: def __init__(self, name: str, price: float): self.name = name self.price = price class OrderItem: def __init__(self, product: Product, quantity: int): self.product = product self.quantity = quantity def subtotal(self) -> float: return self.product.price * self.quantity class Order: def __init__(self, user: User, items: list): self.user = user self.items = items def total_price(self) -> float: return sum(item.subtotal() for item in self.items) class OrderCalculator: def __init__(self, order: Order): self.order = order def calculate(self) -> float: total = self.order.total_price() if self.order.user.user_level == "vip": total *= 0.85 else: total *= 0.95 if total >= 200: total -= 20 return max(total, 0)和面向过程的版本比起来,代码行数确实多了不少,但每一段代码的职责都非常单一。将来改需求,定位和改动范围都很明确:改折扣,去OrderCalculator;改订单数据结构,去Order和OrderItem;改用户等级体系,去User。
3.3 进一步优化:多态与策略模式让规则不再散落
现在的版本已经比函数堆好很多了,但还有一个小问题:OrderCalculator里的if user_level == "vip"和if total >= 200其实还是一种“按类型分支”的做法。如果用户等级从2种变成5种,促销规则从1种变成5种,这个类又会变成一个巨大的分支机器。
这时候,多态就该上场了。我会把折扣规则抽出来作为一个策略接口,每种规则一个类:
class DiscountStrategy: def apply(self, total: float, user: User) -> float: raise NotImplementedError class VipDiscount(DiscountStrategy): def apply(self, total: float, user: User) -> float: return total * 0.85 if user.user_level == "vip" else total * 0.95 class FullReductionPromotion(DiscountStrategy): def __init__(self, threshold: float, reduction: float): self.threshold = threshold self.reduction = reduction def apply(self, total: float, user: User) -> float: return max(total - self.reduction, 0) if total >= self.threshold else totalOrderCalculator再改造一下,接收一个策略列表,依次应用即可。新增规则只需要新增一个策略类,完全不碰已有的类。这就是我前面说的面向对象的终极价值——对扩展开放,对修改关闭。
3.4 实操现场:哪些细节最容易翻车
重构过程中有几个细节是要特别注意的,我在实际写代码时会盯死这些点:
- 浮点运算精度。算金额,千万不要用
float,用Decimal(Python)或BigDecimal(Java),否则会出现“满200减20,结果199.9999999不满足”这种见鬼的问题。 - 折扣套叠加的先后顺序。是先折后满减,还是先满减再折,两种玩法结果不一样。这种业务规则最好在代码里显式用注释标出来,或者干脆做成可配置的策略顺序。
- 负数的保护。满减和折扣套下来,价格可能是负数,最后一定要
max(total, 0)兜底。很多人写促销代码会漏掉这个,生成一堆负金额订单,对账的时候哭都来不及。 - 对象的不可变性。算价过程中不要直接修改订单里的原始单价或数量,应该只基于快照计算。否则并发环境下,一个请求正在算价,另一个请求把商品数量改了,最终价格就错乱了。
3.5 代码复盘:三个关键的权衡时刻
复盘这个过程,有三个地方是典型的面向对象设计抉择点:
第一处:OrderItem为什么要单独建一个类,而不是直接在Order里用字典存“商品id + 数量”?
我的考虑是,OrderItem承载了“单项小计”的行为,如果只是字典,以后想加“单件折扣”“单项税费”就无处安放。单独建类,等于提前为变化预留了位置。
第二处:折扣策略是挂在User上还是挂在Calculator上?
我最后选择了放在Calculator上,因为折扣规则本质上是订单计算策略的一部分,它依赖用户等级但不等同于用户自己。如果把折扣方法挂在User上,用户对象就得知道自己所有可能的优惠规则,这就把两个变化频率不同的东西耦合了。
第三处:为什么不把所有计算逻辑都塞进Order?
Order负责计算自己的总价还行,但把VIP折扣、满减也放进去,它就变成了一个什么都知道的“上帝对象”。上帝对象是面向对象设计里很忌讳的坏味道,它会随着业务增长越来越庞大,最后谁也动不了。
4. 常见问题与排查技巧实录
4.1 新人困惑:为什么我写了class还是会写出面向过程的代码
这是几乎每一个转型期开发者都会遇到的问题。我带的实习生阿凯就曾拿着自己的代码来找我,说:亮哥,我照着你的方式写了类,但为什么组长还是说我写的是面向过程?
我看了一眼他的代码,问题很典型:他的类里面没有私有属性,所有字段全是公开的,然后外部到处直接order.status = 3,而不是order.pay()。更典型的是,他把一百多行业务逻辑全塞在OrderService.createOrder一个方法里,里面的临时变量有二十多个。
这其实是面向对象思维还没有真正建立起来的标志。类变成了一个“存放函数的容器”,而不是“有状态、有行为的实体”。这里的排查建议是:
- 检查类里的方法是不是都在操作自己的属性,如果一个方法里大量使用了传入的参数而不是自己对象的状态,那它本质上是独立的函数,只是碰巧放在类里而已。
- 检查外部代码是不是大量直接读写对象的字段。如果是,说明封装没做到位,类没有把内部细节关住。
- 检查类的职责。用一句话描述这个类是干什么的,如果这句话里出现了“以及”“而且”“另外”,那就是职责过重,该拆了。
4.2 常见错误之一:滥用继承导致的维护噩梦
再讲一个反面教材。有一个项目里要做四种通知方式:短信、邮件、站内信、钉钉。开发同学图省事,建了一个BaseNotifier父类,里面写了sendMessage()、sendEmail()、sendDingTalk()三个方法,然后短信类只继承但不用邮件方法,邮件类只继承但不用钉钉方法。结果每个子类都继承了一大堆自己用不到的方法,代码阅读性极差,而且父类一旦改了一个方法的签名,四个子类全部要跟着编译。
这个问题的根源是:四种子类之间并没有真正的“is-a”关系,它们只是“都具备发送消息这个能力”。正确的做法是定义一个Notifier接口,声明一个send(Message message)方法,然后四个类各自实现,互不干扰。
排查这类问题的方式很简单:如果一个子类必须覆写父类某些方法,或者必须把某些方法标记为“不支持”或“空实现”,那这个继承关系八成是错的。遇到这种情况,果断把继承改成接口加组合,代码会清爽很多。
4.3 常见错误之二:为了提高性能硬凑面向对象
也有反方向的坑。我曾经在一个数据批处理项目里,看到有人用面向对象的方式封装了每一行的数据,每个单元格都建了个对象,处理一亿行数据,跑了四十分钟没跑完,被业务方骂得狗血淋头。
面向对象不是万能的,它对性能是有一定损耗的,对象创建、方法调用、引用传递都有开销。在纯粹的批处理、数据运算、高频循环场景里,面向过程反而更合适。比如Python里处理大规模数值计算,正确姿势是用numpy的向量化操作,而不是给每个数据点建一个类再跑循环。
所以面向对象不是“银弹”,它的价值主要体现在业务逻辑复杂的系统里,而不是所有场景都值得套用。该用过程式的地方,大胆地写过程式;该用对象的地方,认真地建模。真正的高手是在两种思维之间自如切换,而不是标榜自己只会面向对象。
4.4 问题速查表:转型期最常见的典型症状
| 症状 | 根本原因 | 解法 |
|---|---|---|
类里方法全是static,不依赖任何状态 | 本质还是函数式写法 | 找出数据归属,把方法和数据绑在一起 |
| getter/setter泛滥,外部直接用对象当数据结构 | 封装缺失 | 用行为方法替代暴露字段,校验放入setter |
| 一个方法超过100行,注释都有十几行 | 职责过重 | 拆方法,拆类,让每个方法只干一件事 |
| 继承层次深,子类大量空实现或抛异常 | 继承关系错误 | 换接口加组合,去掉无意义的继承 |
| 对象之间互相调用链太长,A调B、B调C、C又调A | 关系设计混乱 | 重新梳理依赖方向,引入中间层解耦 |
| 为了一个简单的计算功能也强行设计类 | 过度设计了 | 简单的就用函数,复杂了再抽象 |
这个表我建议存下来,写代码迷茫的时候对一下,比自己闷头硬想要高效得多。
4.5 避坑经验:设计面向对象时的四条铁律
最后分享几条我在实际项目里总结出来的“铁律”,算是我吃亏吃出来的教训:
第一条,不要一开始就追求完美设计。先跑通业务,再考虑重构。很多人的问题不是在“没有设计”,而是在“连业务都没跑通就开始设计”,结果设计出来的模型根本经不起业务验证。
第二条,命名别偷懒。类名、方法名、变量名是代码的自文档。calculatePrice和calc,OrderCalculator和OC,短期看后者打字少一分钟,长期看前者让所有人少掉一天的头发。
第三条,别害怕删除代码。面向对象的设计优势之一,就是当某个对象不再符合需求时,可以相对独立地被替换掉。很多人碍于“好不容易写的”心理,舍不得删旧代码,于是新老逻辑并存,越搞越乱。
第四条,多读优秀的开源代码。框架如Spring Boot、Django、Flask,都是极佳的面向对象设计范本。读的时候带着“这个类为什么这么设计?这个接口为什么这么抽?”的疑问去读,比只看CRUD的业务代码学得快十倍。
5. 转向面向对象后的实战考验
5.1 对象设计与数据库表结构的博弈
直接把面向对象的模型套到数据库表上,很容易出现“阻抗不匹配”问题。之前做订单系统的重构时,我最难受的点就在这里:
内存里的订单对象,天然适合用嵌套结构表达,订单里有商品列表,商品里有商品属性,层层嵌套,读起来非常自然。但关系型数据库是扁平的表结构,存储时需要把嵌套的对象“拍平”,存到多张表里,读出来时再拼回对象。
这个转化过程如果只在查询和保存时做,还好。糟糕的是有人把数据库表的设计思路直接搬进代码,结果出现大量只有getter/setter的“贫血模型”——类成了单纯的数据管道,没有任何业务行为,全部逻辑还是堆在service里。
我的经验是:数据库表结构追求的是数据一致性,对象模型追求的是业务表达力,两者天然不完全一致,不要强行互相迁就。在写业务代码时,以对象模型为主,数据库层负责持久化适配。不要因为数据库表设计成那样,就让对象模型也照着那样“分表拆类”。
5.2 并发与事务对对象设计的反向影响
除了数据库,还有一个经常被忽略的地方是并发和事务。在这块吃过的亏,让我养成了一个习惯:设计对象方法时,除了想“方法逻辑对不对”,还会想“多线程环境下会不会有问题”。
比如,订单的库存扣减如果放在Product对象的方法里,product.reduceStock(2),看起来封装得很好,但多线程并发下如果没有做锁控制,两个请求同时读到库存是10,各自扣1,最后库存变成了9而不是8。这就是对象方法在并发下暴露的问题。
面向对象设计并不是只管代码组织,还要考虑运行时的并发语义。实际操作中,我一般这么做:
- 对临界资源的操作,在对象内部做同步控制,不要把锁的职责扔给外部调用方。否则每个调用方都忘了加锁,bug就来了。
- 事务的边界要放在服务层,而不是对象内部。对象的方法内部尽量别自己开会话自己开事务,否则多个对象组合完成一个业务时,事务没办法统一提交和回滚。
5.3 什么时候应该“退回去”写面向过程
被面向对象的好处感动完之后,我也得泼盆冷水:不是所有代码都需要面向对象。这个问题我纠结过很久,后来总结出一句话:如果你的数据是死的、逻辑是线性简单的,那用面向过程写反而更清爽;只有当状态和操作足够复杂、变化足够频繁时,面向对象才是划算的。
举个例子:写一个把图片缩放的脚本,输入一个文件路径,输出一个缩放后的文件。这个场景里没有复杂的对象关系,也没有易变的需求,用几个函数按顺序流水账写下来,简洁高效,强行设计出一堆ImageResizer、ImageScaler、ImageProcessor接口,反而累赘。
但是如果你做一个图片处理平台,支持多种滤镜、多种尺寸规格、多种导出格式,那面向对象的价值就出来了——每种滤镜一个类,每种导出格式一个类,新增需求时互不影响,扩展能力直接拉满。
所以正确的态度,不是“面向对象永远优于面向过程”,而是在复杂度到来之前保持简单,在复杂度到来之时及时转向对象建模。这个度,只能靠实战去练,纸上谈兵学不会。
我在写这篇文章的过程中,又翻了一下自己三年前写的一个项目,底层是一个爬虫脚本,最初用函数写了两千行,后来数据源从三个变到二十个,我终于受不了,重构成了策略模式加工厂模式,新增一个数据源只需要加一个类、注册一行配置。那一次重构让我彻底明白了:转变不是说学会一门面向对象的语言就算完成,而是从拿到需求的第一刻起,脑子里想的就已经是“有哪些对象、它们之间如何协作”,这才会真正见效。
希望你也能在日常代码里多留一些“这个类真的承担了它的职责吗”的自我审视,多拆几次、多想几轮。面向对象这套思想,一旦真的融入你的编码习惯,你会发现代码不再是一个“改一处崩三处”的危险品,而是一个可以随需求长大、能让人安心维护的结构体。