起一个 OOP 导航笔记,整理一下目前见过的 OOP 知识。
什么是 面向对象(Object Oriented Programming, OOP),如果感兴趣可以先从基本面向对象知识开始,也就是三大特征——封装、继承、多态。这里假设读者已经对 OOP 有了基本了解。
OOP 作为一个很有年代感的编程范式,源自于早年间大型软件项目的实践经验,虽然已经可以看见极端 OOP 因为带来的繁琐性而不可取(点名 Java),但是在大型软件项目中因为其高效地多态而依旧受到欢迎。OOP 的大部分技巧如设计模式本质上是代码复用的技巧,可以说,学 OOP 其实是在学如何高效复用代码。
就写这么多吧,下面会注明一些 OOP 技巧
基本概念
基本的概念基本的核心的概念就是 封装、继承、多态 。
因为这三个概念不算复杂并且络上的教程比较多了,所以这里不会详细展开讲,而是默认读者已经了解过后,选择了从功能作用的视角去重新描述。
不过在正式讨论之前,需要先了解 OOP 诞生的背景,这对理解基础概念以及返璞归真有所帮助。
OOP 诞生之前就是结构体和函数流行的时代,那个时候人们都意识到了有些函数和结构体是强关联的(或者换句话说,这些函数脱离了结构体就毫无意义)。随着工程量变大,只是靠名字和文档来辨别哪个函数对应哪个结构体变得很麻烦。
为此,将函数和结构体关联起来,使得结构体成为「带有方法的结构体」——被成为是 「类」 的东西诞生了。
在发明了「类」之后,人们发现如果只是这样依旧存在很多问题:结构体上的一些变量完全是为了结构体上的方法之间互相协作完成任务而出现的,但是外部的人却能访问和修改来干扰结构体的正常工作。
为此,private 和 public 这种关键字的出现正是 「封装」 的体现。封装就目的而言,本质是为了降低类内部的复杂度,让外部(即使用类的人)需关心类内部具体是怎么实现的——这很大程度提高了分享代码给他人使用的效率,也是 OOP 能比传统面向过程更能驾驭大工程的原因。
封装是关注类本身,而 继承 和 多态 则是源自于 对类代码的复用 而产生的概念。继承能够让新创建的类重复利用另一个类(通常被称为父类)的代码(即属性和方法)。
多态的出现则是为了应对因为 封装和继承 导致代码之间 耦合 而降低灵活性的问题。
所以总而言之,OOP 的本质是「将数据和行为绑定」,封装、继承、多态都来自于对这种绑定的严格保证和高效利用。
SOLID 设计原则
基础之上的基础。
只要为了写好 OOP 很多时候会很自然的遵守这些原则中的大部分。或者说,并不是为了写好代码而遵循原则,而是在好代码会自然遵循了这些原则——即便不知道这些原则,只要代码写好了同样也会自动遵循。
其中,最重点也是新手最容易违背的就是单一职责。经验来说,很多情况下其他原则都是为了在实现第一条原则的时候顺便遵循的良好实现。
此外,另一条很有用的则是依赖倒置。一个 OOP 新手很常犯的错就是让不同层次的类直接互相调用,导致一个类的改动牵动了另一个几乎无关的类,正确的做法应该是让两者依赖于接口,使用接口隔离依赖链条。
设计模式
遵循设计原则,对实践问题的优质解答。
设计模式可以看作 OOP 的预制菜。很多经典设计模式来自于对真实生产问题的优质解答。你可以不用这些经典的设计模式,可以自己寻找优质的设计。但是,现成的历经时间和前人多次检验后的答案摆在这里——为什么不参考呢?
关注对象的创建机制,使系统独立于对象的创建过程的模式:
| 模式 | 简述 |
|---|---|
| 单例 | 确保一个类只有一个实例,并提供全局访问点。 |
| 工厂方法 | 定义创建对象的接口,由子类决定要实例化的类。 |
| 抽象工厂 | 创建一系列相关或相互依赖的对象,无需指定具体类。 |
| 建造者 | 将复杂对象的构建与表示分离,同样的构建过程可以创建不同的表示。 |
| 原型 | 通过复制现有实例来创建新对象,避免重复初始化开销。 |
处理类或对象的组合,通过组合关系形成更大的结构的模式:
| 模式 | 简述 |
|---|---|
| 适配器 | 将一个类的接口转换成客户希望的另一个接口,使不兼容的类能一起工作。 |
| 桥接 | 将抽象部分与实现部分分离,使它们可以独立变化。 |
| 组合 | 将对象组合成树形结构以表示“部分-整体”层次,使客户端统一对待单个对象和组合对象。 |
| 装饰 | 动态地给对象添加额外职责,比继承更灵活。 |
| 外观 | 为子系统中的一组接口提供一个统一的高层接口,简化使用。 |
| 享元 | 运用共享技术有效支持大量细粒度对象,减少内存占用。 |
| 代理 | 为其他对象提供一种代理以控制对这个对象的访问。 |
关注对象之间的职责分配和通信,描述算法与对象间职责的分配的模式:
| 模式 | 简述 |
|---|---|
| 责任链 | 将请求沿着处理链传递,直到有一个处理器处理它,解耦发送者和接收者。 |
| 命令 | 将请求封装为对象,从而支持请求排队、日志记录、撤销等操作。 |
| 解释器 | 定义语言的文法表示,并提供解释器处理句子(较少用于通用编程)。 |
| 迭代器 | 提供一种方法顺序访问聚合对象中的元素,而不暴露其内部表示。 |
| 中介者 | 用一个中介对象封装一系列对象的交互,使对象之间不再显式引用,降低耦合。 |
| 备忘录 | 在不破坏封装的前提下捕获并外部化对象的内部状态,以便之后恢复。 |
| 观察者 | 定义对象间的一对多依赖,当被观察者状态改变时,所有依赖者得到通知并自动更新。 |
| 状态 | 允许对象在内部状态改变时改变其行为,看起来像是修改了它的类。 |
| 策略 | 定义一系列算法,将每个算法封装起来并使其可以互换,让算法独立于使用它的客户端。 |
| 模板方法 | 在父类中定义算法的骨架,将某些步骤延迟到子类实现,不改变算法结构即可重定义步骤。 |
| 访问者 | 表示一个作用于某对象结构中的各元素的操作,使可以在不改变各元素类的前提下定义新操作。 |
Game Programming Patterns || 游戏编程模式(这本的中译版)
架构
软件开发的定式。
在设计模式上更高一层的则是架构——作为软件根基的设计模式。
更大层面的 OOP 则是架构。一个好的软件架构对于软件的可维护性、可拓展性、健壮性是有帮助的。而前人已经在 OOP 上探索很多,有不少经典好用的架构模式。
反过来,如果要作为软件开发的参与者,同样也需要知道软件的架构,这样才能更好的进行开发而不是拉出兼容性差或复用效率低、难以阅读的石山代码。
范式的范式
源自于代码复用,发展于实践,最终还是回到本质的代码复用问题。
有很多更加本质甚至脱离 OOP 的理念,它们虽然是为了解决 OOP 的不足,但是本质上是对代码的更有效复用问题的理念上的回答。
组合优于继承
如题.
面向接口编程 Interface Oriented Programming
IOP 强调定义和实现分离,通过接口进行约束。
实现类的时候不需要知道另一个类怎么实现,只需要知道接口上有什么方法。
其实遵循了依赖倒置原则——高层模块不依赖低层模块,而是二者都依赖于抽象(接口).
相关的还有 接口驱动开发(Interface Driven Development)。
面向切面编程 Aspect Oriented Programming
AOP 强调的是逻辑上的「切面」,也就是所谓的注意点分离。
比如说我要写一个函数。如果要考虑实现、日志、安全、报错后怎么处理……等等这些问题,就会导致这个函数职责过多,变得臃肿。因此完全可以在逻辑上将日志、安全、报错拆出去,让函数只关注本应有的实现。
AOP 依赖注解和代码生成。
比如说游戏开发的多人联机框架例如 mirror,可以看到这种注解:
public class Player: NetworkBehaviour {
[SyncVar] public int health = 100; // 自动同步字段并且只能在服务器端被修改.
}这就是一种 AOP 的实现,使用框架的用户无需关注具体怎么进行网络通信,只用关注游戏逻辑怎么写,在写完逻辑之后用注解让框架自动生成网络通信的代码,达到注意点分离的效果。