理解面向对象编程(Object-oriented programming)
目录
统一语言(减少理解偏差)
用语 | 解释 |
---|---|
对象 | 具有状态、行为和身份的实体。 |
概念模型 | 用一组概念来描述一个系统,或用任何代替的形式来描述一个概念,以期能进一步了解或说明事物的运作原理。 |
不变性 | 又叫“固定规则”。泛指一些在对象生命周期内保持不变的条件。 |
行为 | 对象对外承担的职责。 |
贫血(领域)模型 | 一种面向对象编程的反模式,其中领域对象包含很少或根本不包含业务逻辑,如验证、计算、规则等。因此,业务逻辑被嵌入到程序本身的架构中,使重构和维护更加困难且耗时。 |
反模式 | 在实践中经常出现但又低效或是有待优化的设计模式。 |
编程模式 | 编写代码的方式。 |
1. 关于编程范式(编程风格)的定义
基于某种概念模型和编程语言来组织代码。目的是为了使代码能更清晰地表达这种概念模型。 例如使用面向过程、面向对象、面向逻辑等任意一种概念来描述系统。
2. 什么是面向对象编程(OOP)?
程序被组织成许多组相互协作的对象,每个对象代表了某个类的一个实例,而类则属于一个通过继承关系而形成的层次结构1。
3. 面向对象的3大特性
对象其实就是将状态属性和行为捆绑在一起,然后对状态属性进行访问控制。这就是封装。其目的是避免不变性遭受外界破坏。有个典型的Bug叫“aliasing bug2”,可以了理解一下。
在OOP中行为才是重要的,而不是属性或者数据。因为封装特性的缘故,外界不能(亦不应该)直接访问对象的内部的属性,而只能是透过行为来改变对象的状态。因此,在对继承结构进行抽象时,更推荐以行为作为优先考量因素。但要想得到一个良好的继承结构绝非易事,因为一不小心可能就会加重代码的耦合性。建议参考“IS-A原则”和“里氏替换原则”。
多态是一种让代码可以运用抽象结构的能力(由运行时实现)。简单说,它是一种使得代码在运行期间可以根据具体对象类型来决定行为(准确点说是“方法体”)的代码执行机制。关于多态的实现,可参考“虚方法/虚函数”相关的资料。
4. 一个非常重要的概念:引用传递
关于 Smalltalk:
(面向对象)消息传递这一概念最早在Smalltalk编程语言中被实现,而Smalltalk的属性只能是私有的。也就说在Smalltalk中,外界仅能通过方法调用来实现对象之间的消息传递(从这一点来看,Java其实拥有更好的灵活性,但这使得它在面向对象方面并不那么纯粹)
4.1 定义
消息传递是一种在计算机上调用行为(运行程序)的技术,调用程序通过发送消息给一个进程(可能是对象或演员),依赖该进程及其支持基础设施来选择并运行适当的代码[维基百科]
4.2 理解
在OOP中,对象之间会透过消息传递来产生程序价值3。例如对象A需要对象B的协助才来完成某项职责。那么上下文应该将对象B传递给对象A,然后再由对象A透过消息传递的形式来驱使对象B去履行相关部份的职责。而不是在上下文中直接访问对象B的内部属性然后再设置或传递给对象A。因为这样很容易导致对象变成贫血领域模型,从而丧失面向对象的优势。
譬如当你试图绕过包含不变性逻辑的方法来直接访问对象属性时,就会导致一些不可预计的异常发生。简单说,在OOP中对象之间的交互仅应该透过消息传递这一抽象概念来实现。此外,消息传递本身还反映了对象的封装性和抽象性。因为对象A只需通过消息传递来要求对象B履行相关的职责,而并不需要关心对象B履行职责的细节过程,在此期间对象B的状态亦由它自己来负责维护。
消息指的是方法调用(,如果画过UML时序图可能会更好理解一些)。而传递则强调交互对象之间的紧密性。具体而言,它使得交互对象双方的行为被融合在了一起。譬如消息传递的过程应该在对象A方法中调用对象B方法,而不是在上下文中分别进行调用。此外,消息的类型大体可分为两种:双向消息和单项消息。区别在于后者的场景并不关心响应结果(在异步编程中常见)。此外,不直接称其为方法调用,是因为消息传递更能够凸显其过程的抽象性。这使得设计者能够更关注程序本身的行为,而不是行为之下的实现细节。
虽然消息传递在广义上是指对象之间的交互行为,但从代码设计层面来看,它实际上是一种编程模式。它强调对象在交互期间应该相互接触,通过依赖目标对象及其行为来实现目的,而非采取扁平化的过程式指令集来实现。
理解例子:在图书管理系统中,User用borrowingBook方法表示用户可以借书这一概念。而Book则有borrow方法表示自己可以被借出的概念。此时若根据消息传递编程模式来设计就会得到如下代码:
// 消息传递 ✅
var user = ...
var book = ...
user.borrowingBook(book)
👉 内部调用 book.borrow(this.userId)
// 扁平的过程式指令集 🙅
var user = ...
var book = ...
user.borrowingBook(book.bookId())
book.borrow(user.userId())
4.3 价值
- 能很好地模拟现实世界中个体之间的交互行为。这使得代码具有更好的表达能力。
- 促进封装和解耦。消息传递其实反映了职责的分离,正因对象有各自的职责才需要进行交互,这使得设计者对于职责的划分更加警惕。其次,消息传递主要包含参数和调用两部分。前者可依赖抽象进行建模,让交互对象双方实现松耦合。后者亦可以透过多态特性进一步对设计进行解耦。
- 为多态特性提供了实现所需的环境。因为消息传递本身只是一种交互抽象,这使得程序运行时可以透过虚方法的概念来实现方法的动态调用(根据对象的具体类型来决定)。
5. 面向对象的价值
编程范式的价值在于能够更好的表达某种概念模型。从编程范式而言,理解OOP的多态其实是最重要的,因为用得好的话代码就可以很容易地做到松耦合。从建模层面而言,面向对象的价值则是有与之对应的技术工具4。这使得你除了可以用面向对象概念来对现实业务领域进行分析设计之外,还可以继续使用OOP将领域模型进行编码落地。此外,面向对象概念模型可以较为容易地被人理解和接受,其 OOP 代码亦可以做到较高的灵活性5。
拓展
函数式编程(FP)和OOP两个并不冲突,可以结合使用。FP只是强调引用透明性(如纯函数)、依赖运行时进行赋值操作、高阶函数、闭包等特性。