5、封装的艺术:什么是封装,为什么需要封装,通过私有属性与getter/setter实现数据保护,实战案例:银行账户类
从“一团乱麻”到“井井有条”
各位同学,咱们今天聊封装。
先问大家一个问题:你写代码的时候,有没有遇到过这种情况?一个类里,十几个属性随便改,今天张三把余额设成负数,明天李四把账户名改成空字符串。最后整个程序跑起来,数据全乱套了。
我早年做项目时就吃过这个亏。一个订单系统,订单金额字段直接暴露在外面。结果有个同事在某个角落里写了个 order.amount = -100,整个报表系统直接崩了。从那以后,我养成了一个习惯:能藏起来的,绝不露出来。
这就是封装的核心思想——把数据藏起来,只留几个“窗口”让别人用。
什么是封装?说白了就是“黑盒子”
封装,英文叫 Encapsulation。听起来高大上,其实很简单:把数据和操作数据的方法打包在一起,对外只暴露必要的接口。
你想想看,你家里的电视机,你会直接去碰显像管吗?不会。你只会按遥控器上的按钮。电视机把复杂的电路藏起来了,只给你几个按键。这就是封装。
在面向对象编程里,封装就是:
- 属性私有化:不让外部直接访问内部数据
- 提供公共方法:通过 getter/setter 来间接访问
- 隐藏实现细节:外部不需要知道内部怎么算的
核心原则:高内聚,低耦合。内部怎么折腾都行,对外接口保持稳定。
为什么需要封装?三个理由就够了
我总结了三个最实在的理由,你听听看:
- 保护数据完整性:防止外部乱改数据。比如银行账户余额,不能是负数,不能随便被减掉。
- 降低复杂度:调用者不需要知道内部实现。你调用
account.deposit(100),至于里面怎么记账、怎么更新日志,你不用管。 - 便于维护和修改:内部实现改了,只要接口不变,外部代码不用动。我有个项目,数据库从 MySQL 换成 PostgreSQL,就因为封装得好,只改了内部方法,外部调用一行没动。
我的经验:封装做得好的代码,改起来特别舒服。封装做得差的,改一个地方,十个地方跟着报错。
Python 里的封装:私有属性与 getter/setter
Python 没有真正的“私有”关键字。它靠的是命名约定:
_name:单下划线开头,表示“受保护的”,外部可以访问,但约定不要直接访问__name:双下划线开头,表示“私有的”,Python 会做名称修饰(name mangling),外部直接访问会报错
但注意,这只是约定,不是强制。Python 的哲学是“大家都是成年人”,靠自觉。
那怎么实现 getter/setter 呢?有两种方式:
方式一:传统 getter/setter 方法
class BankAccount:
def __init__(self, owner, balance=0):
self.__owner = owner
self.__balance = balance
def get_balance(self):
return self.__balance
def set_balance(self, amount):
if amount < 0:
raise ValueError("余额不能为负数")
self.__balance = amount
def get_owner(self):
return self.__owner
这种方式最直观,但写起来有点啰嗦。每个属性都要写两个方法。
方式二:使用 @property 装饰器(Python 风格)
class BankAccount:
def __init__(self, owner, balance=0):
self.__owner = owner
self.__balance = balance
@property
def balance(self):
return self.__balance
@balance.setter
def balance(self, amount):
if amount < 0:
raise ValueError("余额不能为负数")
self.__balance = amount
@property
def owner(self):
return self.__owner
用 @property 的好处是:外部调用时像访问属性一样自然,但内部可以加校验逻辑。
注意:@property 的 setter 方法名必须和 getter 方法名一致。比如上面都是 balance。
实战案例:银行账户类
咱们来写一个完整的银行账户类。这个类要满足:
- 账户名不能为空
- 余额不能为负数
- 存款金额必须大于0
- 取款金额不能超过余额
- 记录每笔交易日志
class BankAccount:
def __init__(self, owner, initial_balance=0):
if not owner or not isinstance(owner, str):
raise ValueError("账户名不能为空且必须是字符串")
if initial_balance < 0:
raise ValueError("初始余额不能为负数")
self.__owner = owner
self.__balance = initial_balance
self.__transactions = [] # 交易记录
@property
def owner(self):
return self.__owner
@property
def balance(self):
return self.__balance
def deposit(self, amount):
"""存款"""
if amount <= 0:
raise ValueError("存款金额必须大于0")
self.__balance += amount
self.__transactions.append(f"存款: +{amount}")
return self.__balance
def withdraw(self, amount):
"""取款"""
if amount <= 0:
raise ValueError("取款金额必须大于0")
if amount > self.__balance:
raise ValueError("余额不足")
self.__balance -= amount
self.__transactions.append(f"取款: -{amount}")
return self.__balance
def get_transactions(self):
"""获取交易记录(只读)"""
return list(self.__transactions) # 返回副本,防止外部修改
def __str__(self):
return f"账户名: {self.__owner}, 余额: {self.__balance}"
你看,所有属性都是私有的。外部只能通过 deposit()、withdraw() 来操作余额。而且每次操作都会记录日志。
避坑指南:我曾经在项目里直接返回了内部列表 self.__transactions,结果外部代码直接往列表里追加数据,导致日志被篡改。后来我改成返回 list(self.__transactions) 副本,才解决问题。
封装的核心逻辑图
下面这张图展示了封装的核心思想:外部只能通过“窗口”(公共方法)访问内部数据,内部实现细节完全隐藏。
封装带来的好处,用代码说话
咱们对比一下,没有封装和有封装的代码:
| 没有封装(直接暴露属性) | 有封装(私有属性 + 方法) |
|---|---|
|
|
你想想看,哪个更靠谱?
我的习惯:写类的时候,先把所有属性设为私有,然后问自己“这个属性外部真的需要直接访问吗?” 90% 的情况下,答案都是“不需要”。
总结一下
封装不是什么高深的技术,它就是一种“藏”的艺术。把数据藏好,把实现细节藏好,只留几个干净、安全的接口给别人用。
这样做的好处,短期看是代码更安全,长期看是项目更抗造。我见过太多项目,前期图省事不封装,后期改需求时改到崩溃。
嗯,封装这件事,越早做越好。
公众号:蓝海资料掘金营,微信deep3321