设计模式
设计模式是软件设计中常见问题的典型解决方案。 每个模式就像一张蓝图, 你可以通过对其进行定制来解决代码中的特定设计问题。
设计模式分类
不同设计模式的复杂程度、 细节层次以及在整个系统中的应用范围等方面各不相同。 我喜欢将其类比于道路的建造: 如果你希望让十字路口更加安全, 那么可以安装一些交通信号灯, 或者修建包含行人地下通道在内的多层互通式立交桥。
最基础的、 底层的模式通常被称为惯用技巧。 这类模式一般只能在一种编程语言中使用。
最通用的、 高层的模式是构架模式。 开发者可以在任何编程语言中使用这类模式。 与其他模式不同, 它们可用于整个应用程序的架构设计。
此外, 所有模式可以根据其意图或目的来分类。 三种主要的模式类别:
- 创建型模式提供创建对象的机制, 增加已有代码的灵活性和可复用性。
- 结构型模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。
- 行为模式负责对象间的高效沟通和职责委派。
创建型模式
工厂模式: 工厂方法模式是一种创建型设计模式, 其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。
工厂方法模式优缺点:
优点:1. 你可以避免创建者和具体产品之间的紧密耦合。 2. 单一职责原则。你可以将产品创建代码放在程序的单一位置, 从而使得代码更容易维护。 3. 开闭原则。 无需更改现有客户端代码, 你就可以在程序中引入新的产品类型。缺点:
1.应用工厂方法模式需要引入许多新的子类, 代码可能会因此变得更复杂。 最好的情况是将该模式引入创建者类的现有层次结构中。
1 |
|
- 抽象工厂模式: 抽象工厂模式是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。
抽象工厂模式优缺点:
优点:
1.你可以确保同一工厂生成的产品相互匹配。
2.你可以避免客户端和具体产品代码的耦合。
3.单一职责原则。 你可以将产品生成代码抽取到同一位置, 使得代码易于维护。
4.开闭原则。 向应用程序中引入新产品变体时, 你无需修改客户端代码。
缺点:
1.由于采用该模式需要向应用中引入众多接口和类, 代码可能会比之前更加复杂。
1 | /** |
生成器模式: 生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。
生成器模式优缺点:
优点:1.你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。 2.生成不同形式的产品时, 你可以复用相同的制造代码。 3.单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。缺点:
由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。
1 | /** |
- 原型模式: 原型模式是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类。
原型模式优缺点:
优点:
1.你可以克隆对象, 而无需与它们所属的具体类相耦合。
2.你可以克隆预生成原型, 避免反复运行初始化代码。
3.你可以更方便地生成复杂对象。
4.你可以用继承以外的方式来处理复杂对象的不同配置。
缺点:
1.克隆包含循环引用的复杂对象可能会非常麻烦。
1 | /** |
单例模式: 单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
单例模式的优缺点:
优点:1.你可以保证一个类只有一个实例。 2.你获得了一个指向该实例的全局访问节点。 3.仅在首次请求单例对象时对其进行初始化。缺点:
1.违反了_单一职责原则_。 该模式同时解决了两个问题。 2.单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。 3.该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。 4.单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。
1 |
|
结构性模式
适配器模式: 适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。
适配器模式优缺点:
优点:1._单一职责原则_你可以将接口或数据转换代码从程序主要业务逻辑中分离。 2.开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。缺点:
1.代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。
1 |
|
桥接模式: 桥接模式是一种结构型设计模式, 可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构, 从而能在开发时分别使用。
桥接模式优缺点:
优点:1.你可以创建与平台无关的类和程序。 2.客户端代码仅与高层抽象部分进行互动, 不会接触到平台的详细信息。 3.开闭原则。 你可以新增抽象部分和实现部分, 且它们之间不会相互影响。 4.单一职责原则。 抽象部分专注于处理高层逻辑, 实现部分处理平台细节。缺点:
1.对高内聚的类使用该模式可能会让代码更加复杂。
1 | /** |
组合模式: 组合模式是一种结构型设计模式, 你可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。
组合模式优缺点
优点:1.你可以利用多态和递归机制更方便地使用复杂树结构。 2.开闭原则。 无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分。缺点:
1.对于功能差异较大的类, 提供公共接口或许会有困难。 在特定情况下, 你需要过度一般化组件接口, 使其变得令人难以理解。
1 |
|
装饰模式: 装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。
装饰模式优缺点:
优点:1.你无需创建新子类即可扩展对象的行为。 2.你可以在运行时添加或删除对象的功能。 3.你可以用多个装饰封装对象来组合几种行为。 4.单一职责原则。 你可以将实现了许多不同行为的一个大类拆分为多个较小的类。缺点:
1.在封装器栈中删除特定封装器比较困难。 2.实现行为不受装饰栈顺序影响的装饰比较困难。 3.各层的初始化配置代码看上去可能会很糟糕。
1 |
|
外观模式: 外观模式是一种结构型设计模式, 能为程序库、 框架或其他复杂类提供一个简单的接口。
外观模式的优缺点:
优点:1. 你可以让自己的代码独立于复杂子系统。缺点:
2.外观可能成为与程序中所有类都耦合的上帝对象。
1 |
|
享元模式: 享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。
享元模式的优缺点:
优点:1. 如果程序中有很多相似对象, 那么你将可以节省大量内存。缺点:
1. 你可能需要牺牲执行速度来换取内存, 因为他人每次调用享元方法时都需要重新计算部分情景数据。 2. 代码会变得更加复杂。 团队中的新成员总是会问: “为什么要像这样拆分一个实体的状态?”。
1 | /** |
代理模式: 代理模式是一种结构型设计模式, 让你能够提供对象的替代品或其占位符。 代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。
代理模式优缺点:
优点:1.你可以在客户端毫无察觉的情况下控制服务对象。 2.如果客户端对服务对象的生命周期没有特殊要求, 你可以对生命周期进行管理。 3.4.即使服务对象还未准备好或不存在, 代理也可以正常工作。 开闭原则。 你可以在不对服务或客户端做出修改的情况下创建新代理。缺点:
1.代码可能会变得复杂, 因为需要新建许多类。 2.服务响应可能会延迟。
1 | /** |
行为模式
责任链模式: 责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。
责任链模式优缺点:
优点:1.你可以控制请求处理的顺序。 2.单一职责原则。 你可对发起操作和执行操作的类进行解耦。 3.开闭原则。 你可以在不更改现有代码的情况下在程序中新增处理者。缺点:
1. 部分请求可能未被处理。
1 |
|
命令模式: 命令模式是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。
命令模式优缺点:
优点:1. 单一职责原则。 你可以解耦触发和执行操作的类。 2. 开闭原则。 你可以在不修改已有客户端代码的情况下在程序中创建新的命令。 3. 你可以实现撤销和恢复功能。 4. 你可以实现操作的延迟执行。 5. 你可以将一组简单命令组合成一个复杂命令。缺点:
1. 代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次。
1 |
|
迭代器模式: 迭代器模式是一种行为设计模式, 让你能在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素。
迭代器模式优缺点:
优点:1. 单一职责原则。 通过将体积庞大的遍历算法代码抽取为独立的类, 你可对客户端代码和集合进行整理。 2. 开闭原则。 你可实现新型的集合和迭代器并将其传递给现有代码, 无需修改现有代码。 3. 你可以并行遍历同一集合, 因为每个迭代器对象都包含其自身的遍历状态。 4. 相似的, 你可以暂停遍历并在需要时继续。缺点:
1. 如果你的程序只与简单的集合进行交互, 应用该模式可能会矫枉过正。 2. 对于某些特殊集合, 使用迭代器可能比直接遍历的效率低。
1 |
|
中介者模式: 中介者模式是一种行为设计模式, 能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。
中介者模式优缺点:
优点:1. 单一职责原则。 你可以将多个组件间的交流抽取到同一位置, 使其更易于理解和维护。 2. 开闭原则。 你无需修改实际组件就能增加新的中介者。 3. 你可以减轻应用中多个组件间的耦合情况。 4. 你可以更方便地复用各个组件。缺点:
1. 一段时间后, 中介者可能会演化成为上帝对象。
1 |
|
备忘录模式: 备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
备忘录模式优缺点
优点:1. 你可以在不破坏对象封装情况的前提下创建对象状态快照。 2. 你可以通过让负责人维护原发器状态历史记录来简化原发器代码。缺点:
1. 如果客户端过于频繁地创建备忘录, 程序将消耗大量内存。 2. 负责人必须完整跟踪原发器的生命周期, 这样才能销毁弃用的备忘录。 3. 绝大部分动态编程语言 (例如 PHP、 Python 和 JavaScript) 不能确保备忘录中的状态不被修改。
1 |
|
观察者模式: 观察者模式是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。
观察者模式优缺点:
优点:1. 开闭原则。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。 2. 你可以在运行时建立对象之间的联系。缺点:
1. 订阅者的通知顺序是随机的。
1 |
|
状态模式: 状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。
状态模式优缺点:
优点:1. 单一职责原则。 将与特定状态相关的代码放在单独的类中。 2. 开闭原则。 无需修改已有状态类和上下文就能引入新状态。 3. 通过消除臃肿的状态机条件语句简化上下文代码。缺点:
1. 如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作。
1 |
|
策略模式: 策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。
策略模式优缺点
优点:1. 你可以在运行时切换对象内的算法。 2.你可以将算法的实现和使用算法的代码隔离开来。 3.你可以使用组合来代替继承。 4.开闭原则。 你无需对上下文进行修改就能够引入新的策略。缺点:
1.如果你的算法极少发生改变, 那么没有任何理由引入新的类和接口。 使用该模式只会让程序过于复杂。 2.客户端必须知晓策略间的不同——它需要选择合适的策略。 3.许多现代编程语言支持函数类型功能, 允许你在一组匿名函数中实现不同版本的算法。 这样, 你使用这些函数的方式就和使用策略对象时完全相同, 无需借助额外的类和接口来保持代码简洁。
1 |
|
- 模板方法模式
模板方法模式优缺点
优点:
缺点:1.你可仅允许客户端重写一个大型算法中的特定部分, 使得算法其他部分修改对其所造成的影响减小。 2.你可将重复代码提取到一个超类中。1. 部分客户端可能会受到算法框架的限制。 2.通过子类抑制默认步骤实现可能会导致违反_里氏替换原则_。 3.模板方法中的步骤越多, 其维护工作就可能会越困难。
1 | /** |
访问者模式: 访问者模式是一种行为设计模式, 它能将算法与其所作用的对象隔离开来。
访问者模式优缺点:
优点:1. 开闭原则。 你可以引入在不同类对象上执行的新行为, 且无需对这些类做出修改。 2. 单一职责原则。 可将同一行为的不同版本移到同一个类中。 3. 访问者对象可以在与各种对象交互时收集一些有用的信息。 当你想要遍历一些复杂的对象结构 (例如对象树), 并在结构中的每个对象上应用访问者时, 这些信息可能会有所帮助。缺点:
1. 每次在元素层次结构中添加或移除一个类时, 你都要更新所有的访问者。 2. 在访问者同某个元素进行交互时, 它们可能没有访问元素私有成员变量和方法的必要权限。
1 |
|