直接写正文。
先说明一下我的习惯。接手“day05_面向对象进阶”这种打卡式的标题,我最怕看到的就是讲义搬运工:把封装、继承、多态三大特性重新抄一遍,配上两个玩具代码,结尾来一句“继承是多态的基础,多态是继承的体现”。这套话不是说错了,而是说了等于没说。读者真正缺的不是概念定义,而是“遇到什么样的需求,该用什么样的设计;这么设计之后,下次需求变了,我改哪里”。
所以这篇文章我不按教材目录走。我按自己带项目、带新人的经验来讲:先聊清楚OOP到底在解决什么问题,再逐个拆封装、继承、多态的边界和坑,最后用一个小案例把前面的东西串起来。这样读下来,你是真的能把代码写得更像样,而不是只会念“三大特性”咒语。
1. 大多数人学完三大特性后,其实只会念咒语——写个能跑的类不难,难的是看清它到底在解决什么问题
1.1 先从一个真实的小项目场景说起
我带过不少实习生,也看过不少刚学完OOP基础就来干活的同学写的代码。通常第一个真实任务长这样:要写一个订单模块,订单要有订单号、用户、商品列表、总价,还要能计算折扣、格式化输出。
很多同学是这么写的——建一个Order类,里面放一堆字段,然后一个方法从头写到尾:
python复制class Order:
def __init__(self, order_id, user, items):
self.order_id = order_id
self.user = user
self.items = items # 商品价格列表
self.total = 0
def calculate(self):
for item in self.items:
self.total += item['price'] * item['count']
if self.user['vip_level'] >= 2:
self.total *= 0.85
return f"{self.order_id}: {self.total}"
这段代码能跑吗?能跑。但这和我用五个字典加五个函数写出来的东西,除了把函数塞进了类里,本质上有什么区别?没有。这就是典型的“不会OOP硬写OOP”——类变成了装函数的袋子,封装、继承、多态一个都没真正用上。
1.2 “类是模板”这句话没错,但光记住这句话还不够
教材喜欢说“类是对象的模板,对象是类的实例”。这句话对,但特别容易误导人。很多人在这种表述下形成的认知是:类的作用就是把相关的数据和函数捆在一起。
捆在一起只是第一步。捆在一起的真正目的是隐藏细节、暴露接口、固定边界。而这三点,恰恰是基础教程里最容易被忽略的。
我给你打个比方。你买一台洗衣机,不会关心里面的电机怎么转、排水泵什么时候启动,你只需要看面板上的按钮和指示灯。洗衣机的“类设计”做得好不好,取决于厂商有没有把那些复杂的机械结构、电子逻辑都封装在机身里,而不是取决于它有没有按钮。
代码里的类也一样。一个设计良好的类,对外暴露的方法就是“面板按钮”,内部的数据和实现细节就是“电机和电路”。使用者只管按按钮,不需要知道内部怎么转。这个边界画清楚了,你的类才真正有了“封装”的味道,而不只是一堆字段和函数的集合。
那怎么判断自己是不是真理解了?我有个很简单的自测方法:把类里的所有属性改成私有(Python里加双下划线,C++里放private),然后看调用方代码会不会炸。如果炸了,说明你之前的调用方在直接操作内部数据,这个类的封装是漏风的。如果没炸,恭喜,你的类至少做到了对外接口稳定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 封装不是私有化,而是划定责任边界:getter/setter是最容易“一学就会,一会就错”的地方
2.1 getter/setter的滥用,本质上是对“封装”二字的误解
很多学过Java的同学对封装形成了一种肌肉记忆:属性私有化,然后无脑生成getter和setter。这一套动作练得极其熟练,但用错了地方。
我见过这样的代码:
java复制public class User {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
这套代码的问题在哪?表面上看,属性是private的,好像封装了。但实际上,调用方仍然可以随意调用setName("随便改")、setAge(-5),对内部数据想做啥就做啥。private并没有拦住任何东西,getter/setter只是给私有属性开了两扇门而已。
封装的核心不是“属性不能直接访问”,而是“对象对自己的数据负有责任”。什么时候允许改、改成什么值才合法、改完之后要同步更新哪些关联数据,这些都应该由对象自己说了算,而不是把决策权交给每一个调用方。
继续用洗衣机类比。一台好洗衣机不会把“注水阀开度”和“电机转速”直接暴露给你调,你只需要按“标准洗”或“强力洗”。同理,一个设计良好的User类,应该暴露的是updateProfile、changePassword这类有业务含义的方法,而不是setName、setAge这种单纯的赋值器。
2.2 在Python和C++里分别怎么把握封装的度
不同语言对封装的实现方式差别挺大,这点初学者最容易懵。我分别说说。
Python这边,没有真正的private关键字,约定俗成是用单下划线_name表示“保护成员,外部不要动”,双下划线__name做名称改写,防止子类意外覆盖。但Python封装的重点其实不在访问控制,而在属性拦截——通过@property把一个方法伪装成属性来用:
python复制class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
# 读取时可以加监控、日志
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("余额不能为负")
self._balance = value
这样调用方写account.balance = 100,体验上跟直接改属性一样,但实际走的是带校验的setter方法。校验逻辑被收敛在一个地方,而不是散落在各处——这才是封装该有的样子。
C++那边,用的是private、protected、public三级访问控制。初学者容易犯的毛病是两个极端:要么把所有成员都放public,图省事;要么把所有属性都private、再配套写一堆getter/setter,图“标准”。
我的建议是:成员变量基本都放private,但不要机械地给每个私有变量配getter/setter。设计对外接口时先问自己一句:调用方真的需要直接读这个变量吗?还是他需要的是某个业务能力?
举个例子,一个Counter类,内部有个count变量。与其暴露getCount和setCount,不如暴露increment()和reset()。这样count的修改逻辑永远只存在于类内部,永远不会出现调用方把count设成-1这种鬼事。这就是把“数据”变成“状态”,把“赋值器”变成“行为”的过程。
封装这个事,我踩过的最深的一个坑,是早期写一个支付模块时,把订单状态设计成了public字段,结果到处都是order.status = "PAID"这种代码。后来要加状态机校验,我只能满项目搜status =,一行一行改。如果一开始就提供order.pay()、order.cancel()这样的方法,把状态流转收敛在类内部,改起来就是加一个方法的事。这教训让我之后再也不敢把状态类字段直接暴露出去。
3. 继承是把双刃剑:组合优先不是口号,是踩过坑才懂的结论
3.1 一个教科书式的继承设计,为什么在需求变化时崩了
继承是OOP里最容易被“用过头”的特性。先看一个特别经典的错误示范——动物分类:
python复制class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} is eating")
def run(self):
print(f"{self.name} is running")
class Dog(Animal):
def bark(self):
print("Woof!")
class Bird(Animal):
def fly(self):
print(f"{self.name} is flying")
看起来挺合理,对吧?“Dog is an Animal”,“Bird is an Animal”,继承关系没错。但需求一多就完了:
- 如果要加一个Penguin,它不会飞,但会游泳,怎么办?在Bird下重写
fly为空实现?可以,但很丑。 - 如果要加一个Ostrich,它不会飞但跑得飞快,怎么办?“不会飞”的
fly重写在各种鸟里重复出现。 - 如果要加一个Duck,既会飞又会游泳,还能叫?继承树越来越复杂。
这个例子的本质问题在于:“动物”这个概念太宽泛,“会飞”“会游泳”“会叫”是能力,而不是分类维度。把能力塞进继承树,必然导致子类被迫继承不该有的东西。
3.2 is-a关系判断标准,以及什么时候狠心拆继承
判断一个继承设计是否合理,我常用三个标准:
- 子类是否真的能完全替代父类?——里氏替换原则。如果你写
Bird类型的变量去接收一个Penguin实例,然后调用fly(),结果报错或者空转,那这个继承就有问题。 - 父类里的每一个public方法,对子类是不是都有意义?如果子类需要重写父类方法为一个空实现或抛异常,说明这个继承关系是错的。
- 需求变化时,会不会出现“为了给A加功能,被迫动B”的情况?继承是强耦合,父类一改,所有子类都会受影响。
真正的is-a关系应该是:Student is a Person,Rectangle is a Shape,SavingsAccount is a BankAccount。这些关系中,子类在语义上和结构上都能当父类用。
那Penguin和Bird呢?严格说,生物分类学上Penguin确实是Bird。但在代码设计里,Bird这个类如果带着fly()方法,那这个Bird类本身就已经是“有飞行能力的鸟”的抽象了,Penguin不该继承它。
所以,当你发现继承树开始用“重写空实现”“抛异常”来硬凑的时候,就是该拆继承的时候了。
3.3 组合/委托模式的落地写法
“组合优先于继承”,这句话很多书上都写过,但没解释清楚为什么。我拿上面那个动物例子,用组合重构:
python复制class Flyable:
def fly(self):
print(f"{self.name} is flying")
class Swimmable:
def swim(self):
print(f"{self.name} is swimming")
class Quackable:
def quack(self):
print("Quack!")
class Duck:
def __init__(self, name):
self.name = name
self.fly_behavior = Flyable()
self.swim_behavior = Swimmable()
self.quack_behavior = Quackable()
def fly(self):
self.fly_behavior.fly()
这样Duck拥有飞行、游泳、叫三种能力,Penguin只需要组合Swimmable,Ostrich组合Flyable但改成跑得快的行为。能力可以自由组合,互不干扰。这比在不断膨胀的继承树里打补丁,不知道高到哪里去了。
组合的本质是**“对象拥有对象”(has-a)**。继承是“对象是一种”(is-a)。在真实业务里,has-a关系比is-a关系常见得多,但新人写代码时却总条件反射地用继承。我自己早期写报表模块时也干过这事:某个基类BaseReport写了模板方法,结果不同的报表有的要排序、有的要过滤、有的要汇总,最后基类里塞满了if self.type == ...。后来改成组合策略模式,每个报表持有自己的数据处理器,清爽得不行。
这里给个实用判断法则:先试着用组合描述,如果组合写起来非常别扭,再考虑继承。大多数情况下你都会发现组合是更顺的那条路。
4. 多态的本质是延迟决定:从函数指针、虚函数到Python动态分发
4.1 多态到底“多”在哪里:一次方法调用的背后
我见过很多初学者背定义:“多态是指同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。”但你要是问他,该怎么设计一个支持多态的代码,他大概率会写一个if/else分支判断类型。
下面两种写法,你觉得哪种算多态?
python复制# 写法A:if/else分发
def make_sound(animal):
if isinstance(animal, Dog):
print("Woof!")
elif isinstance(animal, Cat):
print("Meow!")
elif isinstance(animal, Duck):
print("Quack!")
# 写法B:子类重写
class Dog:
def make_sound(self):
print("Woof!")
class Cat:
def make_sound(self):
print("Meow!")
写法B才是多态。原因在于:调用方只知道animal有一种make_sound的能力,但具体怎么发声,是延迟到运行时由实际对象决定的。
在写法A里,每增加一种新动物,你都要回来改make_sound函数,加一个elif。而写法B里,新增一个Sheep类实现make_sound,所有调用方的代码一行都不用动,直接传入Sheep实例就行。
我们看下C++里的机制。C++的virtual函数是这么工作的:每个包含虚函数的类,在编译期会生成一张虚函数表(vtable),里面存放该类所有虚函数的实际地址。每个对象里有一个隐式的虚表指针(vptr)指向这张表。调用animal.make_sound()时,编译器不是直接定死一个函数地址,而是通过对象vptr找到vtable,再从vtable里取出对应槽位的函数地址来调用。这就是“延迟决定”的底层实现。
Python没有虚函数表的概念,但动态语言的特性天然实现了多态:animal.make_sound()到底调用谁,是运行时在对象所属类里查找方法名决定的。只要你有make_sound方法,管你是Dog还是Cat,都能传进来调用。这也就是大家常说的“鸭子类型”。
4.2 面向接口编程的正确姿势
理解了多态的机制,就该聊“面向接口编程”了。这个词听起来玄乎,其实核心就一句话:调用方依赖抽象,不依赖具体实现。
还是用代码说话。假设你有两种消息通知方式:邮件和短信。如果不面向接口,你会写:
python复制class EmailNotifier:
def send(self, message):
print(f"send email: {message}")
class SMSNotifier:
def send(self, message):
print(f"send sms: {message}")
def notify(user, message):
if user.notify_preference == 'email':
EmailNotifier().send(message)
elif user.notify_preference == 'sms':
SMSNotifier().send(message)
下次加个钉钉通知、加个站内信,notify函数就要继续长胖。如果面向接口:
python复制class Notifier:
def send(self, message):
raise NotImplementedError
class EmailNotifier(Notifier):
def send(self, message):
print(f"send email: {message}")
class SMSNotifier(Notifier):
def send(self, message):
print(f"send sms: {message}")
def notify(notifier: Notifier, message):
notifier.send(message)
调用方notify只认Notifier接口,完全不关心你传进来的是Email还是SMS。将来加微信通知,只需要新增一个类,别的地方不动。
C++里对应的写法是抽象基类加纯虚函数,Java/C#里是接口,Python里用abc.ABC或者干脆靠鸭子类型。形式上不同,思想一样。
4.3 过度设计警告:多态不是越多越好
讲了这么多多态的好处,我得泼一盆冷水:多态不是免费的。
多态增加的是类的数量和调用的间接层。每多一个抽象,就多一处需要理解“到底哪个实现会被调用”的地方。如果系统里只有两三种固定行为,没有新增扩展的需求,你硬造一个接口、一堆实现类,纯属给后来人添堵。
我见过最夸张的代码:一个只有首页轮播图的小模块,搞了BannerService接口加五个实现类,每个实现类里就一个方法返回固定的JSON。这种抽象没有任何收益,只有维护成本。
判断要不要用多态的标尺很简单:这个行为是不是真的存在多种可能?未来是不是真的有可能新增变体? 两个答案都是“是”,才值得抽象。如果你现在只看到一种实现,那别提前设计接口,先把具体类写出来,等第二种实现真的出现时再抽接口。过早抽象和过早优化一样,都是过度工程。
5. 抽象类和接口:把“契约”写清楚,比把“实现”写漂亮更重要
5.1 抽象类管“骨架”,接口管“能力”
很多语言初学者分不清抽象类和接口的区别。我有个特别直白的类比:
- 抽象类像一份“半成品模板”:它已经帮你做好了部分工序,留了几个空位让你填。比如
BaseReport抽象类实现了数据获取和格式化输出的公共流程,但中间有一部parse_data需要子类自己实现。 - 接口像一份“能力合同”:它不提供任何实现,只规定“有这个能力的东西必须会做这些事”。比如
Serializable接口规定所有实现类必须能把自己的数据序列化成字符串。
从设计意图上看,抽象类强调的是代码复用——子类共享父类已实现的部分;接口强调的是能力约束——实现类必须满足某种行为约定。
Python里抽象类用abc模块:
python复制from abc import ABC, abstractmethod
class BaseParser(ABC):
def __init__(self, raw_data):
self.raw_data = raw_data
def parse(self):
"""模板方法:规定了处理流程的骨架"""
cleaned = self._clean_data()
return self._parse_into_model(cleaned)
def _clean_data(self):
# 公共实现:去掉空行、去空格等
return [line.strip() for line in self.raw_data if line.strip()]
@abstractmethod
def _parse_into_model(self, cleaned_lines):
"""子类必须实现:把清洗后的数据解析成业务对象"""
pass
C++里对应的是纯虚函数:
cpp复制class BaseParser {
public:
virtual ~BaseParser() = default;
// 非虚公共接口,定义流程骨架
std::vector<Model> parse() {
auto cleaned = cleanData();
return parseIntoModel(cleaned);
}
protected:
virtual std::vector<Model> parseIntoModel(std::vector<std::string> lines) = 0;
private:
std::vector<std::string> cleanData() {
// 公共清洗逻辑
}
};
这个设计里有几个点值得注意:parse是非虚的公共接口,外部只能调用它;parseIntoModel是纯虚函数,由子类决定具体解析逻辑。这样既复用了公共的清洗逻辑,又强制每个子类实现自己的差异化解析。
5.2 设计原则在项目里到底怎么用:一个插件化结构的例子
把抽象和接口用到实际业务中,最典型的场景是插件化结构。
假设你在做一个多数据源报表系统,支持从MySQL、CSV、API三种数据源拉数据。一开始大家都往一个类里加if source_type分支,越加越乱。用接口重构后:
python复制class DataSource(ABC):
@property
@abstractmethod
def source_type(self) -> str:
"""数据源类型标识,用于注册"""
@abstractmethod
def fetch(self) -> list:
"""拉取原始数据"""
@abstractmethod
def normalize(self, raw_data: list) -> list:
"""转换成统一的数据结构"""
class MySQLSource(DataSource):
source_type = "mysql"
def fetch(self):
# 连接MySQL,执行查询
pass
def normalize(self, raw_data):
# 把数据库行转成统一结构
pass
class CSVRSource(DataSource):
source_type = "csv"
def fetch(self):
# 读CSV
pass
def normalize(self, raw_data):
pass
然后写一个注册表,按source_type自动分发:
python复制class DataSourceRegistry:
_sources = {}
@classmethod
def register(cls, source_cls):
cls._sources[source_cls.source_type] = source_cls
@classmethod
def create(cls, source_type, *args, **kwargs):
source_cls = cls._sources.get(source_type)
if not source_cls:
raise ValueError(f"Unsupported source type: {source_type}")
return source_cls(*args, **kwargs)
# 注册内置数据源
DataSourceRegistry.register(MySQLSource)
DataSourceRegistry.register(CSVRSource)
新增一种数据源(比如MongoDB),只需要新写一个类实现DataSource接口,然后调一行DataSourceRegistry.register(...)。原来所有调用方代码不需要动。这就是“开闭原则”:对扩展开放,对修改关闭。
这里你可能会问:接口定义得太细会不会过?我也踩过这个坑。刚开始我恨不得把每个方法都抽象一层,结果数据源的差异太大,有的数据源根本没有“分页”概念,有的没有“增量更新”,强制统一接口反而让实现类写了一堆raise NotImplementedError。
后来我的策略是:接口只定义所有实现类都有的、稳定不变的行为;差异性行为就不要放进接口了,用来做策略或者在接口里提供默认实现。比如normalize可以给个默认实现,子类有特殊需求再覆盖。这样接口的约束力够用,又不会过度僵化。
6. 一个从“能跑”到“好改”的类设计实战:用户订单模块的三次重构
6.1 第一版:写满了if/else的“面条代码”
为了把前面所有内容串起来,我用一个典型的订单模块来实操。需求:用户下单,系统根据用户等级、优惠券、促销活动计算最终价格,有订单日志和支付回调处理。
第一版是最常见的“流水账”写法:
python复制def checkout(user, cart, coupon, promotion):
total = 0
for item in cart:
total += item['price'] * item['count']
if user['vip_level'] == 1:
total *= 0.95
elif user['vip_level'] == 2:
total *= 0.9
elif user['vip_level'] == 3:
total *= 0.85
if coupon and coupon['type'] == 'full_reduction':
if total >= coupon['threshold']:
total -= coupon['discount']
elif coupon and coupon['type'] == 'cash':
total *= 0.8
if promotion['type'] == 'double_eleven':
total *= 0.5
elif promotion['type'] == 'new_user':
total -= 20
total = max(total, 0)
order = {
'user_id': user['id'],
'total': total,
'status': 'CREATED',
'items': cart,
}
save_order(order)
send_notification(user['id'], f"订单创建成功,金额: {total}")
return order
这版有什么问题?非常典型:
- 用户等级折扣、优惠券、促销全写在
checkout里面,任何一个规则变化都要改这个函数。 - 新增一种优惠类型,就得在这里再加一个
elif,而且分支之间相互影响,比如VIP折扣和促销折扣是否叠加?看不出来,代码也没说清楚。 - 不可测试。想测试“只用了优惠券,不打折”的场景,得构造整个user、coupon、promotion字典,非常痛苦。
6.2 第二版:能用继承但耦合严重的“半成品”
学完OOP之后,很多人会把第一版重构成第二版——开始用类了,但用歪了。最常见的写法是搞一个Discount基类,然后每个折扣类型继承它:
python复制class Discount:
def apply(self, total):
return total
class VipDiscount(Discount):
def __init__(self, level):
self.level = level
def apply(self, total):
return total * (1 - (0.05 * self.level))
class CouponDiscount(Discount):
def __init__(self, coupon):
self.coupon = coupon
def apply(self, total):
if self.coupon['type'] == 'full_reduction':
if total >= self.coupon['threshold']:
return total - self.coupon['discount']
return total
然后把checkout改成:
python复制def checkout(user, cart, coupon, promotion):
total = sum(item['price'] * item['count'] for item in cart)
discounts = []
discounts.append(VipDiscount(user['vip_level']))
discounts.append(CouponDiscount(coupon))
discounts.append(PromotionDiscount(promotion))
for discount in discounts:
total = discount.apply(total)
total = max(total, 0)
...
比第一版强多了——每种折扣规则收敛到自己的类里,互不干扰。但问题也来了:
- 每个折扣类都在原始total上做操作,但折扣的优先级没有表达出来。实际上VIP折扣和促销折扣有先后顺序吗?很多业务规则是有的。这版代码没法体现。
PromotionDiscount基本是CouponDiscount的翻版,又要再写一遍full_reduction逻辑。- 万一某个折扣不是“直接乘系数”型,而是“满减”型,逻辑写在
apply里看起来是统一了,但语义上很别扭。
这版的核心问题在于:它用继承来组织“折扣类型”,但折扣的应用顺序和叠加规则是组合关系,不是继承关系。
6.3 第三版:依赖抽象、组合优于继承的成品
第三版我采用“策略模式 + 责任链”的组合:每个折扣规则实现统一接口,规则的组合和顺序由一个策略引擎负责。
python复制class DiscountRule(ABC):
@abstractmethod
def apply(self, context: "DiscountContext") -> None:
"""对context中的当前金额进行修改,并记录日志"""
class DiscountContext:
def __init__(self, user, cart, coupon, promotion):
self.user = user
self.cart = cart
self.coupon = coupon
self.promotion = promotion
self.current_total = sum(
item['price'] * item['count'] for item in cart
)
self.applied_rules = []
def apply_discount(self, discount):
original = self.current_total
self.current_total = discount(self)
self.applied_rules.append({
'rule': discount.__name__,
'before': original,
'after': self.current_total,
})
class VipRule(DiscountRule):
def apply(self, context: DiscountContext):
if context.user['vip_level'] >= 1:
rate = 1 - 0.05 * context.user['vip_level']
context.apply_discount(lambda ctx: ctx.current_total * rate)
class FullReductionRule(DiscountRule):
def __init__(self, threshold, discount):
self.threshold = threshold
self.discount = discount
def apply(self, context: DiscountContext):
if context.current_total >= self.threshold:
context.apply_discount(lambda ctx: ctx.current_total - self.discount)
class PromoDoubleElevenRule(DiscountRule):
def apply(self, context: DiscountContext):
if context.promotion['type'] == 'double_eleven':
context.apply_discount(lambda ctx: ctx.current_total * 0.5)
规则引擎:
python复制RULE_PRIORITY = [
VipRule,
lambda ctx: FullReductionRule(ctx.coupon['threshold'], ctx.coupon['discount']),
PromoDoubleElevenRule,
]
class CheckoutService:
def checkout(self, user, cart, coupon, promotion):
ctx = DiscountContext(user, cart, coupon, promotion)
for rule in RULE_PRIORITY:
rule_instance = rule(ctx) if callable(rule) else rule
rule_instance.apply(ctx)
ctx.current_total = max(ctx.current_total, 0)
order = Order.create(
user_id=user['id'],
total=ctx.current_total,
applied_rules=ctx.applied_rules,
)
order.save()
order.notify_creation()
return order
这一版的设计思路,其实每一步都在用前面讲的内容:
- 封装:所有状态体现在
DiscountContext里,金额的修改统一走apply_discount方法,每个规则只和context交互,不直接操作外部字段。 - 面向接口:规则都实现
DiscountRule接口,引擎只认接口,不认具体规则类型。 - 组合优于继承:规则的优先级用列表表达,规则对象之间没有继承关系,纯粹是组合。
- 多态:不同的规则在运行时被逐一调用,新增一种规则不修改引擎代码。
再往后说,如果规则变复杂,还可以进一步用责任链模式把规则串起来,让每个规则自己决定“是否继续往上传”。但对大多数业务规模,这种简简单单的策略列表已经完全够用了。
测试也变简单了。想测“只用优惠券不用VIP”的场景,直接构造只有FullReductionRule的列表跑一遍就行:
python复制def test_full_reduction_only():
user = {'id': 1, 'vip_level': 0}
cart = [{'price': 100, 'count': 1}]
coupon = {'type': 'full_reduction', 'threshold': 80, 'discount': 20}
promotion = {'type': 'none'}
service = CheckoutService([FullReductionRule(80, 20)])
order = service.checkout(user, cart, coupon, promotion)
assert order.total == 80
这才是能长期维护的代码。不是因为它用了多少高级特性,而是因为它把变化点隔离得足够干净——需求一变,你永远知道该去改哪个类,而不是在那个几百行的checkout函数里大海捞针。
说实话,面向对象进阶这条路,最难的不是记住“封装继承多态”这六个字,而是建立起“封装边界”“组合优先”“面向抽象”这些设计嗅觉。有这个嗅觉,你会条件反射地拒绝那种把类当函数袋子的写法;没有它,你学了再多设计模式也只是在写复杂的面条代码。我写这篇文章,最重要的就是帮你把这些东西串起来——原理、取舍、踩坑、实战,一个链路走下来。下一次你接手新项目或者重构老代码的时候,试着用这里面的思路去审视一遍你的类,感受会很不一样。
