设计模式| 结构型模式

时间:2022-06-10
本文章向大家介绍设计模式| 结构型模式,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

GOF23种设计模式中结构型模式,共七种:

- 适配器模式、
- 装饰器模式、
- 代理模式、
- 外观模式、
- 桥接模式、
- 组合模式、
- 享元模式。
下面逐一讲解:

其他同系列的文章还有: 面向对象编程中的六大原则 设计模式| 创建型模式 设计模式| 结构型模式 设计模式| 行为型模式 (上) 设计模式| 行为型模式 (下) 欢迎阅读,评论!!!

1.适配器模式

不兼容结构的协调——适配器模式

适配器模式的作用是解决两个对象间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个对象可以一起工作。

举个生活中的例子:港式插头转换器,港式的电器插头比大陆的电器插头体积要大一些。如果从中国香港买了一个 Mac book,
我们会发现充电器无法插在家里的插座上,为此而改造家里的插座显然不方便,
所以我们需要一个转换插头。

相信在开发中遇到过这样的情况:原本两个类是毫无联系的,但是有些时候我们想让他们进行交互协作,
当然最直接暴力的就是修改各自的源码,但没有源码呢,

通常适配器模式所涉及的角色包括三种:

目标(Target)——客户端期望使用的接口或类(新接口)
被适配者(Adaptee)——一个现存需要适配的接口。(旧接口)
适配器(Adapter)——负责将Adaptee的接口转换为Target的接口。适配器是一个具体的类,这是该模式的核心。

1、适配器模式的使用

A. 适配器必须实现原有的旧的接口(现有的可利用的接口)。 B. 适配器对象中持有对新接口的引用,当调用旧接口时,将这个调用委托给实现新接口的对象来处理,也就是在适配器对象中组合一个新接口(组合的形式)。 这就是适配器模式的魅力:不改变原有接口,却还能使用新接口的功能。

2、适配器模式的优点

*  通过适配器模式可以让两个原本没有任何关系的类能够在一起协调运行,而无需改动原来的代码,极大的增加了可扩展性。
*  将目标类和适配者类解耦
*  增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性,符合开闭原则

3、适配器模式的缺点

过多的使用适配器,可能会导致系统逻辑非常零乱,不易整体进行把握,降低代码的可读性,
并增加了一些冗余的代码,如果可以对系统进行重构,尽量不要使用使用适配器模式。

如果一定要与外观、装饰者模式严格区分的话, 装饰者模式主要是添加新的功能,而适配器模式主要做的是转换工作。 适配器将一个对象包装起来以改变其接口, 装饰者将一个对象包装起来以增加新的行为和责任, 适配器模式的核心在于“转换”——尽量通过适配器把现有资源转为可用的目标资源。

关于适配器写的不错的文章

2.装饰器模式

扩展系统功能——装饰器模式

无论是在现实生活还是编程世界中,除了本质还需要一些“外饰”才能达到最好的效果。
尤其是在项目开发过程中,很多业务都充满了很多的不确定性,来自业务本身、产品、项目特性的,
如果一味的使用继承来扩展的话,可能会相当不灵活,今天介绍的装饰器模式就是多层继承的替代方案。

又例如,某一天小王去度假了,但是他写的函数需要增加一些功能,此时由我接手,
但是我并不想修改他的源代码,此时可以使用装饰者模式。装饰者模式是能够在不改变对象自身的基础上,
在程序运行期间给对象动态地添加功能。

装饰模式(Decorator Pattern)是一种常见的结构型模式,又叫包装模式(Wrapper),又叫包装器模式(Wrapper),它可以动态地给一个对象添加一些额外的职责功能。 装饰模式是对客户端以透明的方式扩展对象的功能,是继承关系的一个替代方案。

即对于客户端来说并不会觉得对象在装饰前后有何不同,而且装饰模式可以在不用创造更多子类的情况下将对象的功能加以扩展,
更关键在于这种扩展是完全透明的。通常装饰器模式中会涉及到两大类角色——被装饰对象和装饰者,
又可以细分为四种具体角色:

Component抽象构件——定义我们最核心的对象的一个接口或者是抽象类,
                  即最原始的对象(必然有一个最基本、最核心、最原始的接口或抽象类充当Component抽象构件)
ConcreteComponent 具体构件——定义一个要被装饰器装饰的对象,Component 的具体实现,即被装饰者。
抽象Decorator装饰角色 ——一般是一个抽象类(便于根据不同的装饰逻辑去实现其子类)且一定会持有一个private变量指向
                     Component抽象构件的引用(它里面不一定只有抽象的方法)维护对组件对象和其子类组件的引用,
                     但是要注意的是所谓装饰者仅仅是发挥锦上添花的作用,核心的本质功能还是应该由构件提供,
                     相当于是把在构件的基础上进行升级,所以需要继承构件
具体装饰器角色(ConcreteDecorator)——通常是一群子装饰器组合共同为组件添加新的功能

抽象装饰器核心代码

#import "Component.h"  //抽象构件
@interface Decorator : Component
//装饰对象需要装饰的原始对象
@property(nonatomic, strong)Component *component;
@end

-(void)operation{
    if (self.component) {
        [self.component operation];
    }
}

1、装饰器模式的优点

*   虽然装饰模式与继承关系的都是为了要在基类的基础上扩展对象的功能,
    但是装饰模式可以提供比继承更多的灵活性,装饰模式允许系统动态决定“贴上”或者除掉一个“装饰”而不需要改变代码的层次;
    而继承关系是静态的,如果想要增加或者减少一个功能只能通过增加继承层次或降低层次。

*   使用者可以随时根据具体的业务逻辑灵活组织所需要的功能,通过使用不同的具体装饰类以及这些装饰类的组合即可

*   装饰者类可以在被装饰者的行为前面或后面加上自己的行为,甚至取代被装饰者的行为

*   通过使用装饰器模式,我们可以实现不改变原有代码,开放现有代码的方式来实现更多的功能。

*   装饰类和被装饰类是可以独立发展且不会相互耦合。即Component类无须知道Decorator类,
    Decorator类是从外部来扩展Component类的功能,而Decorator也不用知道具体的构件。 

2、装饰器模式的缺点

使用装饰模式会产生比使用继承关系更多的对象和复杂的逻辑,降低了可读性,
因此,合理使用装饰类,控制其数量,以便降低系统的复杂度

3、装饰器模式的适用场景及注意事项

在不影响其他对象的情况下,需要以动态、透明的方式给随时给被装饰对象添加或者移除某些功能时而不影响原有逻辑,
想通过灵活组合对象的方式动态地改变被装饰对象的行为

需要扩展一个类的功能或给一个类增加或随时移除附加功能。

当需要以多层次的继承关系才能实现扩充时,可以考虑装饰器模式。

需要为某一类型的兄弟类进行改装或加装功能,首选装饰模式。

关于装饰器模式写的不错的文章

3.代理模式

代理模式(Proxy Pattern)又叫委托模式属于是一个使用率非常高结构型设计模式。

其定义如下:为其他对象提供一种代理以控制对这个对象的访问。(Provide a surrogate or placeholder for another object to control access to it.)通俗可以理解成代理就是帮你打理。你只跟代理一个人打交到。而不关心实际操作的人的具体如何做。代理模式目前框架里用的最多, 主要作用是程序本身不关心被代理的身份细节。而只关心它暴露出来的共有行为接口,也可从字面理解就是代理你去执行调用别的类的方法面向被调用的类。

1、代理模式的优点

能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。

客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,  
系统具有较好的灵活性和可扩展性。

2、适用场景

当无法或者不想直接访问某个对象或访问直接某个对象消耗巨大时,可以采取通过一个代理对象来间接访问,
为了保持对客户端透明,代理对象和被代理对象需要实现相同的接口。

4.外观模式

  外观模式:为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

外观模式中,一个子系统的外部与其内部的通信通过一个统一的外观类进行,外观类将客户类与子系统的内部复杂性分隔开,使得客户类只需要与外观角色打交道,而不需要与子系统内部的很多对象打交道。

外观模式是一种使用频率非常高的结构型设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互,
为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,且客户端调用非常方便。

外观模式又称为门面模式,它是一种对象结构型模式。外观模式是迪米特法则的一种具体实现,
通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子系统的耦合度。

外观模式概述

 不知道大家有没有比较过自己泡茶和去茶馆喝茶的区别,如果是自己泡茶需要自行准备茶叶、茶具和开水,如图1(A)所示,
 而去茶馆喝茶,最简单的方式就是跟茶馆服务员说想要一杯什么样的茶,是铁观音、碧螺春还是西湖龙井?正因为茶馆有服务员,
  顾客无须直接和茶叶、茶具、开水等交互,整个泡茶过程由服务员来完成,顾客只需与服务员交互即可,整个过程非常简单省事,

如图1(B)所示。

图1 两种喝茶方式示意图

在软件开发中,有时候为了完成一项较为复杂的功能,一个客户类需要和多个业务类交互,而这些需要交互的业务类经常会作为一个整体出
现,由于涉及到的类比较多,导致使用时代码较为复杂,此时,特别需要一个类似服务员一样的角色,
由它来负责和多个业务类进行交互,而客户类只需与该类交互。
外观模式通过引入一个新的外观类(Facade)来实现该功能,外观类充当了软件系统中的“服务员”,
它为多个业务类的调用提供了一个统一的入口,简化了类与类之间的交互。
在外观模式中,那些需要交互的业务类被称为子系统(Subsystem)。如果没有外观类,
那么每个客户类需要和多个子系统之间进行复杂的交互,系统的耦合度将很大,如图2(A)所示;
而引入外观类之后,客户类只需要直接与外观类交互,客户类与子系统之间原有的复杂引用关系由外观类来实现,从而降低了系统的耦合度
如图2(B)所示。

图2

5.桥接模式

处理多维度变化-桥接模式

在正式介绍桥接模式之前,我先跟大家谈谈两种常见文具的区别,它们是毛笔和蜡笔。
假如我们需要大中小3种型号的画笔,能够绘制12种不同的颜色,
    如果使用蜡笔,需要准备3×12 = 36支,
    但如果使用毛笔的话,只需要提供3种型号的毛笔,外加12个颜料盒即可,涉及到的对象个数仅为 3 + 12 = 15,远小于36,
    却能实现与36支蜡笔同样的功能。
如果增加一种新型号的画笔,并且也需要具有12种颜色,对应的蜡笔需增加12支,而毛笔只需增加一支。为什么会这样呢?

通过分析我们可以得知:在蜡笔中,颜色和型号两个不同的变化维度(即两个不同的变化原因)融合在一起,
无论是对颜色进行扩展还是对型号进行扩展都势必会影响另一个维度;但在毛笔中,颜色和型号实现了分离,
增加新的颜色或者型号对另一方都没有任何影响。如果使用软件工程中的术语,我们可以认为在蜡笔中颜色和型号之间存在较强的耦合性,
而毛笔很好地将二者解耦,使用起来非常灵活,扩展也更为方便。
在软件开发中,我们也提供了一种设计模式来处理与画笔类似的具有多变化维度的情况,即本章将要介绍的桥接模式。

桥接模式是一种很实用的结构型设计模式,如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。

  桥接模式用一种巧妙的方式处理多层继承存在的问题,
  用抽象关联取代了传统的多层继承,将类之间的静态继承关系转换为动态的对象组合关系,
  使得系统更加灵活,并易于扩展,同时有效控制了系统中类的个数。

桥接定义如下:

桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,
又称为柄体(Handle and Body)模式或接口(Interface)模式。

桥接模式的结构与其名称一样,存在一条连接两个继承等级结构的桥。

桥接模式是一个非常有用的模式,在桥接模式中体现了很多面向对象设计原则的思想,包括“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏代换原则”、“依赖倒转原则”等。熟悉桥接模式有助于我们深入理解这些设计原则,也有助于我们形成正确的设计思想和培养良好的设计风格。

在使用桥接模式时,我们首先应该识别出一个类所具有的两个独立变化的维度,
将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。
通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),
而将另一个维度设计为“实现类”层次结构(实现部分)。
例如:对于毛笔而言,由于型号是其固有的维度,因此可以设计一个抽象的毛笔类,
在该类中声明并部分实现毛笔的业务方法,而将各种型号的毛笔作为其子类;
颜色是毛笔的另一个维度,由于它与毛笔之间存在一种“设置”的关系,
因此我们可以提供一个抽象的颜色接口,而将具体的颜色作为实现该接口的子类。
在此,型号可认为是毛笔的抽象部分,而颜色是毛笔的实现部分,
结构示意图如下:
在上图中,如果需要增加一种新型号的毛笔,只需扩展左侧的“抽象部分”,
增加一个新的扩充抽象类;如果需要增加一种新的颜色,只需扩展右侧的“实现部分”,
增加一个新的具体实现类。扩展非常方便,无须修改已有代码,且不会导致类的数目增长过快。

对于客户端而言,可以针对两个维度的抽象层编程,在程序运行时再动态确定两个维度的子类,动态组合对象,将两个独立变化的维度完全解耦,以便能够灵活地扩充任一维度而对另一维度不造成任何影响。

6. 组合模式

树形结构的处理——组合模式

 树形结构在软件中随处可见,例如操作系统中的目录结构、应用软件中的菜单、办公系统中的公司组织结构等等,
 如何运用面向对象的方式来处理这种树形结构是组合模式需要解决的问题,
 组合模式通过一种巧妙的设计方案使得用户可以一致性地处理整个树形结构或者树形结构的一部分,
 也可以一致性地处理树形结构中的叶子节点(不包含子节点的节点)和容器节点(包含子节点的节点)。

1、什么是组合模式

组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。

组合模式是对树型数据结构的一种实现,组合模式主要由两个角色组成

- 枝结点
- 叶结点

所有的组合模式都是由一个根节点扩展出来的,每个枝结点都可以扩展出枝结点和叶结点。叶结点表示这条分支的终点,无法扩展。
iOS 中的 View 就是一个标准的组合模式。
  1. 组合模式的主要优点如下:
(1) 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,
    方便对整个层次结构进行控制。
(2) 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
(3) 在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
(4) 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,
    可以形成复杂的树形结构,但对树形结构的控制却非常简单。

3.在以下情况下可以考虑使用组合模式:

(1) 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
(2) 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
(3) 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。

7. 享元模式-

实现对象的复用- 享元模式

定义:享元模式是 一种 可共享 对象的一种设计模式。在需要使用大量的细粒度 对象的过程中。这种模式 能很大的减少内存和提升性能。

所谓 细粒度,就是对象相近 而且 数量很多。 我们把这些对象 分成 内部状态 和 外部状态。
内部状态 只使用一个对象来共享  。
外部状态 不能共享出来,是对象区分的。

大体实现:

实现享元模式需要两个关键组件,通常是可共享的享元对象和保存他们的池。某种中央对象维护这个池,并从它返回适当的实例。

在iOS开发中,大家肯定都用过UITableViewCell,UICollectionViewCell,
这两个类在使用过程中就使用了享元模式,工作原理基本就是:利用重用池重用思想,
创建页面可显示的cell个数的对象,在页面滚动过程中监听每个cell的状态,从页面消失的cell被放回重用池,
将要显示的cell先去重用池中去取,如果可以取到,则继续使用这个cell,如果没有多余的cell,就重新创建新的,
这样即使你有100条数据,也仅仅只会创建页面可显示个数的cell对象,
这样就大大减少了对象的创建,实现了大量内存占用,导致内存泄露的问题

1.主要优点

 享元模式的主要优点如下:
 (1) 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。
 (2) 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。

2.主要缺点

享元模式的主要缺点如下:
(1) 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
(2) 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。

3.适用场景

 在以下情况下可以考虑使用享元模式:
 (1) 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
 (2) 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
 (3) 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,
    应当在需要多次重复使用享元对象时才值得使用享元模式。

<后续会持续更新>