5、封装的艺术:什么是封装,为什么需要封装,通过私有属性与getter/setter实现数据保护,实战案例:银行账户类

从“一团乱麻”到“井井有条”

各位同学,咱们今天聊封装。

先问大家一个问题:你写代码的时候,有没有遇到过这种情况?一个类里,十几个属性随便改,今天张三把余额设成负数,明天李四把账户名改成空字符串。最后整个程序跑起来,数据全乱套了。

我早年做项目时就吃过这个亏。一个订单系统,订单金额字段直接暴露在外面。结果有个同事在某个角落里写了个 order.amount = -100,整个报表系统直接崩了。从那以后,我养成了一个习惯:能藏起来的,绝不露出来

这就是封装的核心思想——把数据藏起来,只留几个“窗口”让别人用

什么是封装?说白了就是“黑盒子”

封装,英文叫 Encapsulation。听起来高大上,其实很简单:把数据和操作数据的方法打包在一起,对外只暴露必要的接口

你想想看,你家里的电视机,你会直接去碰显像管吗?不会。你只会按遥控器上的按钮。电视机把复杂的电路藏起来了,只给你几个按键。这就是封装。

在面向对象编程里,封装就是:

  • 属性私有化:不让外部直接访问内部数据
  • 提供公共方法:通过 getter/setter 来间接访问
  • 隐藏实现细节:外部不需要知道内部怎么算的

核心原则:高内聚,低耦合。内部怎么折腾都行,对外接口保持稳定。

为什么需要封装?三个理由就够了

我总结了三个最实在的理由,你听听看:

  1. 保护数据完整性:防止外部乱改数据。比如银行账户余额,不能是负数,不能随便被减掉。
  2. 降低复杂度:调用者不需要知道内部实现。你调用 account.deposit(100),至于里面怎么记账、怎么更新日志,你不用管。
  3. 便于维护和修改:内部实现改了,只要接口不变,外部代码不用动。我有个项目,数据库从 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) 副本,才解决问题。

封装的核心逻辑图

下面这张图展示了封装的核心思想:外部只能通过“窗口”(公共方法)访问内部数据,内部实现细节完全隐藏。

外部世界(调用者) 封装内部(黑盒子) 私有属性 __owner, __balance, __transactions 公共方法(窗口) deposit(), withdraw(), get_transactions() @property: balance, owner 调用 调用 操作 外部只能通过公共方法访问内部数据,无法直接修改私有属性

封装带来的好处,用代码说话

咱们对比一下,没有封装和有封装的代码:

没有封装(直接暴露属性) 有封装(私有属性 + 方法)
account.balance = -100
account.owner = ""
account.balance -= 500
# 数据随意改,毫无约束
account.deposit(100)
account.withdraw(50)
# 内部校验,数据安全
# 余额不会变负数
# 账户名不会变空

你想想看,哪个更靠谱?

我的习惯:写类的时候,先把所有属性设为私有,然后问自己“这个属性外部真的需要直接访问吗?” 90% 的情况下,答案都是“不需要”。

总结一下

封装不是什么高深的技术,它就是一种“藏”的艺术。把数据藏好,把实现细节藏好,只留几个干净、安全的接口给别人用。

这样做的好处,短期看是代码更安全,长期看是项目更抗造。我见过太多项目,前期图省事不封装,后期改需求时改到崩溃。

嗯,封装这件事,越早做越好。


公众号:蓝海资料掘金营,微信deep3321