设计模式
文章目录
深入设计模式
面向对象程序设计
抽象、多态、继承、封装
对象之间的关系
- 依赖:对类 B 进行修改会影响到类 A 。
- 关联:对象 A 知道对象 B。类 A 依赖于类 B。
- 聚合:对象 A 知道对象 B 且由 B 构成。类 A 依赖于类 B。
- 组合:对象A 知道对象B、由B 构成而且管理着B 的生命周期。类 A 依赖于类 B。
- 实现:类A 定义的方法由接口B 声明。对象A 可被视为对象B。类 A 依赖于类 B。
- 继承: 类A 继承类B 的接口和实现, 但是可以对其进行扩展。对象 A 可被视为对象 B。类 A 依赖于类 B。
设计模式简介
设计模式是软件设计中常见问题的典型解决方案。它们就像能根据需求进行调整的预制蓝图,可用于解决代码中反复出现的设计问题。
算法更像是菜谱:提供达成目标的明确步骤。而模式更像是蓝图:你可以看到最终的结果和模式的功能,但需要自己确定实现步骤。
模式包含的内容
- 意图部分简要地描述问题和解决方案。
- 动机部分进一步解释问题并说明模式会如何提供解决方案。
- 结构部分展示模式的各个部分和它们之间的关系。
- 在不同语言中的实现提供流行编程语言的代码,让读者更好地理解模式背后的思想。
模式的分类
所有模式可以根据其意图或目的来分类。
- 创建型模式提供创建对象的机制,增加已有代码的灵活性和可复用性。
- 工厂方法
- 抽象工厂
- 生成器
- 原型
- 单例
- 结构型模式介绍如何将对象和类组装成较大的结构,并同时保持结构的灵活和高效。
- 适配器
- 桥接
- 组合
- 装饰
- 外观
- 享元
- 代理
- 行为模式负责对象间的高效沟通和职责委派。
- 责任链
- 命令
- 迭代器
- 中介者
- 备忘录
- 观察者
- 状态
- 策略
- 模板方法
- 访问者
谁发明了设计模式
《设计模式: 可复用面向对象软件的基础》: https://refactoringguru.cn/gof-book
软件设计原则
优秀设计的特征
- 代码复用
- 扩展性
组合优于继承
继承通常只有在程序中已包含大量类,且修改任何东西都非常困难时才会引起关注。此类问题的清单:
-
子类不能减少超类的接口;
-
在重写方法时,你需要确保新行为与其基类中的版本兼容;
-
继承打破了超类的封装;
-
子类与超类紧密耦合;
-
通过继承复用代码可能导致平行继承体系的产生。
继承通常仅发生在一个维度中。
组合是代替继承的一种方法。继承代表类之间的“是”关系(汽车是交通工具),而组合则代表“有”关系(汽车有一个引擎)。
SOLID原则
《敏捷软件开发: 原则、模式与实践》: https://refactoringguru.cn/principles-book
-
单一职责原则Single responsibility principle
修改一个类的原因只能有一个。
尽量让每个类只负责软件中的一个功能,并将该功能完全封装(隐藏)在该类中。
主要目的:减少复杂度。
-
开闭原则Open/closed principle
对于扩展,类应该是"开放"的;对于修改,类则应该是“封闭”的。
本原则的主要理念是在实现新功能时能保持已有代码不变。
-
里氏替换原则Liskov substitution principle
当你扩展一个类时,记住你应该要能在不修改客户端代码的情况下将子类的对象作为父类对象进行传递。
子类必须保持与父类行为的兼容。在重写一个方法时,要对基类行为进行扩展,而不是将其完全替换。
替换原则:用于预测子类是否与代码兼容,以及是否能与其超类对象协作的一组检查。
与有着多种解释方式的其他设计模式不同,替换原则包含一组对子类(特别是其方法)的形式要求。
- 子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象;
- 子类方法的返回值类型必须与超类方法的返回值类型或其子类别 相匹配;
编程语言世界中的另一个反例是动态类型:基础方法返回一个字符串,但重写后的方法则返回一个数字。
-
子类中的方法不应抛出基础方法预期之外的异常类型;(这条规则源于一个事实:客户端代码的
try-catch
代码块针对的是基础方法可能抛出的异常类型。因此,预期之外的异常可能跟会穿透客户端的防御代码,从而使整个应用崩溃。)对于绝大部分现代编程语言,特别是静态类型的编程语言(Java和C#等等),这些规则已内置于其中。如果违反了这些规则,你将无法对程序进行编译。
-
子类不应该加强其前置条件;
-
子类不能削弱其后置条件;
-
超类的不变量必须保留;
扩展一个类的最安全做法是引入新的成员变量和方法,而不要去招惹超类中已有的成员。当然在实际中,这并非总是可行。
-
子类不能修改超类中私有成员变量的值。
有写编程语言允许通过反射机制来访问类的私有成员,还有一些语言(Python和JavaScript)没有对私有成员进行任何保护。
-
接口隔离原则Interface segregation principle
客户端不应该被强迫依赖于其不使用的方法。
尽量缩小接口的范围,使得客户端的类不必实现其不需要的行为。
-
依赖倒置原则Dependency inversion principle
高层次的类不应该依赖于低层次的类。两者都应该依赖于抽象接口。抽象接口不应依赖于具体实现。具体实现应该依赖于抽象接口。
通常再设计软件时,你可以辨别出不同层次的类。
- 低层次的类实现基础操作(例如磁盘操作、传输网络数据和连接数据库等)。
- 高层次的类包含复杂业务逻辑以指导低层次类执行特定操作。
依赖倒置原则建议:
- 作为初学者,最好使用业务术语来对高层次类依赖的低层次操作接口进行描述。例如,业务逻辑应该调用名为
openReport(file)
的方法,而不是openFile(x)
、readBytes(n)
和closeFile(x)
等一系列方法。这些接口被视为是高层次的。 - 现在你可基于这些接口创建高层次类,而不是基于低层次的具体类。这要比原始的依赖关系灵活得多。
- 一旦低层次的类实现了这些接口,他们将依赖于业务逻辑层, 从而倒置了原始的依赖关系。
依赖倒置原则通常和开闭原则共同发挥作用:无需修改已有类就用不同的业务逻辑类扩展低层次的类。
创建型模式
创建型模式提供了创建对象的机制,能够提升已有代码的灵活性和可复用性。
工厂方法
亦称 虚拟构造函数、Virtual Constructor、Factory Method。
工厂方法是一种创建型设计模式,其在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型。
工厂方法模式建议使用特殊的工厂方法代替对于对象构造函数的直接调用(即使用new
运算符)。不用担心,对象仍将通过new
运算符创建, 只是该运算符改在工厂方法中调用罢了。工厂方法返回的对象通常被称作“产品”。
所有产品必须使用同一接口。
结构
适用场景
- 当在编写代码的过程中,如果无法预知对象确切类别及其依赖关系时,可使用工厂方法;
- 如果希望用户能扩展软件库或框架的内部组件,可使用工厂方法;
- 如果希望复用现有对象来节省系统资源,而不是每次都重新创建对象,可使用工厂方法。
优缺点
优点:
- 可以避免创建者和具体产品之间的紧密耦合;
- 单一职责原则。可以将产品创建代码放在程序的单一位置,从而使得代码更容易维护;
- 开闭原则。无需更改现有客户端代码,就可以在程序中引入新的产品类型。
缺点:
- 应用工厂方法模式需要引入许多新的子类,代码可能会因此变得更复杂。最好的情况是将该模式引入创建者类的现有层次结构中。
与其他模式的关系
- 在许多设计工作的初期都会适用 工厂方法(较为简单,而且可以更方便地通过子类进行定制),随后演化为 抽象工厂、原型或生成器(更灵活但更复杂);
- 抽象工厂模式通常基于一组 工厂方法,但可以使用 原型模式来生成这些类的方法;
- 可以同时使用 工厂方法和迭代器让子类集合返回不同类型的迭代器,并使得迭代器与集合相匹配;
- 原型并不基于继承,因此没有继承的缺点。原型需要对被复制对象进行复杂的初始化。工厂方法基于继承,但它不需要初始化步骤;
- 工厂方法是 模板方法的一种特殊形式。同时,工厂方法可以作为一个大型模板方法中的一个步骤。
抽象工厂
抽象工厂是一种创建型设计模式,它能创建一系列相关的对象,而无需指定其具体类。
结构
生成器
原型
单例
结构型模式
适配器
桥接
组合
装饰
外观
享元
代理
行为模式
责任链
命令
迭代器
中介者
备忘录
观察者
状态
策略
模板方法
访问者
文章作者 fzhiy
上次更新 2023-04-08