软件设计

OOP 抽象(Abstraction)与封装(Encapsulation)总是混淆?看完这篇就明白

学习面向对象(OOP)时,总会遇到一个必须跨越的难关。

3 分钟阅读
OOP 抽象(Abstraction)与封装(Encapsulation)总是混淆?看完这篇就明白 封面图

抽象与封装总是分不清?这篇文章帮你一次理清

学习面向对象(OOP)时,总会遇到一个必须跨越的难关。

那就是抽象与封装。

这两个概念很容易听起来几乎一样。面试中被问到“抽象和封装有什么区别?”时,也很容易含糊回答。

先说结论。

抽象是决定“展示什么”的设计视角,而封装是决定“如何隐藏”的实现视角。

掌握这句话,就已经解决了一半。接下来用示例把剩下的内容彻底巩固下来。

什么是抽象?

抽象是隐藏复杂的内部实现,只提取并展示必要的本质。

想想汽车。开车时,我们不需要知道发动机如何让燃料爆炸。

方向盘、踏板、挡位。只要了解这个接口,就能开车。

也就是说,思考向驾驶员暴露什么,就是抽象。

用代码可以这样表示。

protocol Coffee {
    func brew()
}

struct Americano: Coffee {
    func brew() {
        print("冲煮美式咖啡")
    }
}

let menu: Coffee = Americano()
menu.brew()
// 输出:冲煮美式咖啡

调用方只知道本质brew()。至于如何研磨咖啡豆、将水调到多少度,都不在关注范围内。

这就是抽象。


什么是封装?

封装是将数据与处理这些数据的功能绑定在一起,并阻止外部直接访问。

核心是隐藏和保护。

想想银行账户。如果外部可以随意修改余额,后果会很严重。

因此要隐藏余额,只允许通过存款、取款等规定的渠道进行修改。

class Account {
    private var balance = 0

    func deposit(_ amount: Int) {
        guard amount > 0 else { return }
        balance += amount
    }

    func getBalance() -> Int {
        return balance
    }
}

let acc = Account()
acc.deposit(1000)
print(acc.getBalance())
// 输出: 1000

我们用private阻止了balance。外部只能通过规定的入口deposit修改值。

像这样隐藏内部实现并控制访问,就是封装。


那么,两者的决定性区别是什么?

总结如下。

区分 抽象 封装
目的 隐藏复杂性,只暴露本质 隐藏并保护数据
视角 设计视角(是什么) 实现视角(怎么做)
问题 “应该展示什么?” “应该如何隐藏?”
手段 接口、抽象类 访问控制修饰符(如 private)

下面指出最容易混淆的地方。

两者都涉及隐藏某些东西,所以容易混淆。

但它们隐藏的对象不同。

抽象隐藏复杂流程(逻辑),让使用变得简单;而封装隐藏数据(状态),以便安全保护。

咖啡协议与账户类:画出它们的关系,一下就理清了
咖啡协议与账户类:画出它们的关系,一下就理清了

什么时候该关注哪一个?

实际工作中,两者不是分开使用,而是一起使用,只是关注点不同。

  • 设计新功能的接口时 → 关注抽象
  • 需要保证类内部状态的完整性时 → 关注封装
  • 协作时思考要向他人暴露的 API → 抽象
  • 安全管理值、避免 bug 时 → 封装
写代码时只要贴着这句话,感觉就会不一样
写代码时只要贴着这句话,感觉就会不一样

面试时会这样问

Q. 请解释抽象和封装的区别。

关键在于视角不同。抽象是隐藏不必要的细节、只展现本质的设计概念;封装则是隐藏数据、限制访问的实现概念。再补充说明抽象通过接口实现,封装通过访问控制修饰符实现,会更完整。

Q. 为什么需要封装?

如果外部直接修改对象状态,数据完整性很容易被破坏。让状态只能通过规定的方法修改,就能将验证逻辑集中在一处,也更容易维护和调试。


这两个概念不是对立的,而是一对搭档。如果今天的示例让你理解了它们,下次写代码时只要想一次:“我现在展示了什么,又隐藏了什么?”OOP 就会清晰得多。

延伸阅读