该文档进行设计模式的装饰模式的介绍
装饰模式
简介
装饰模式,重要就在于装饰俩字。装饰模式存在的意义是在于不改变一个对象本身的基础上去给对象添加添加额外的新行为。这种模式可以用一种现实生活中的一个行为进行类比。最贴近的就是一个裸人不断的穿上衣服,这个不断穿衣服的过程就可以视为装饰模式的目的。体现在具体类的设计中就是对于一个类的属性的不断扩充。
我们再来从设计模式三原则的角度来分析一下。主要看到开放封闭原则。我们在设计中如果想要去添加一个类的属性,由于开放封闭原则,我们是不希望去直接修改类的设计的。那么我们就需要另外一种方法来进行这个属性的扩充,这种设计的方法或者说模式就被定义为装饰模式。
总的来说,这种模式的真正意义就在于解决类的扩展性问题
举例
要了解设计模式还是得回到具体的例子中去,我们举个网络传输的例子

图中其实已经显示的很明确了,在网络传输的模型中,每层模型其实就相当于一个装饰器。每经过一层模型,我们的数据就会多一些属性,而这些属性就是装饰器给它进行添加的。在进入下一层后,我们会保留在这一层中获取到的新属性。或者这么理解,即使你当前数据已经经历过了一个装饰器的装饰,但是当你到达下一个装饰器的时候,你可以将这个数据对象看做是一个没有经历过装饰的数据。
分类
对于装饰模式提到的装饰方法,其实我们一般能够想到俩种装饰技巧
第一种就是最直观的继承,这种很好理解,你继承了一个类就继承了这个类的属性
你再在这个新类中去添加一些属性就可以实现这种封装了
第二种方法是装饰模式中常用的方法,使用关联机制
这个你去看关联关系的UML类图也能够一眼看出来(你都看到这了不至于连这个都不知道吧)
区别(优劣)
既然这俩都能够实现装饰,那我们更常用关联机制下的装饰模式的原因以及这俩的优劣呢?
继承机制
使用继承的直接优点就是简单,我们需要一个新的属性,就直接在这个子类中去添加属性就可以了,但是这种设计也带来 了一些问题。回想一下我们C++中继承类的创建。这种类的创建其实是静态的。
或者这么说吧,你的子类对象创建其实是需要创建一套属于子类自己的数据的,也就是说,包括了父类的对象也是在构建子类对 象时进行初始化的,这其实会导致一个什么问题呢,就是我们其实很难去控制这个子类包含的基类数据。因为这是一个属于自己的副 本,那么当我们想将这个子类再装饰上另一个子类时,这里的设计其实会比较混乱,即使你会说可以使用父类指针,但事实上这种设 计会导致在后续中我们的冗余数据会增多。因为在装饰时我们并不是使用现有的进行装饰,而是创建了一个副本进行装饰的,这个就 是整个继承机制的最大弊端。
而且在传统的继承模型中,我们是通过类层次结构来扩展功能的,但这会导致子类的功能是预先定义好的。如果想要动态地扩展 或修改对象的功能,传统的继承设计就显得不够灵活。例如:
如果我们想给一个已有的类添加新的行为,通常需要通过继承来创建新的子类,这种方式是静态的,需要在编译时确定。如果某 个子类已经被创建,我们无法在运行时再灵活地对其进行修改或扩展,不能随时给对象增加新的功能。这就是静态继承的局限性。
关联机制
关联机制,顾名思义就是使用关联关系来实现的机制,这是一种更加灵活的方法,通过将一个类的对象嵌入到另一个新对 象中,有另一个对象来进行决定是否调用嵌入对象的api方法以及决定是否对于这个对象的行为进行扩展。我们将这么一个新对 象称之为装饰器。
为了使得装饰器以及被装饰器装饰的对象相对于客户端来首透明,我们规定这俩者必须实现相同的接口。通过这样的设计,客户 端使用时不需要去关注这个类是否已经被装饰过。
我们可以在被装饰的类中去调用在装饰器类中定义的方法,来为这个类实现更多的功能。而且由于我们前面规定的接口统一而衍 生出来的透明性,这里就实现了我们前面的递归嵌套,即对于已经装饰过的对象可以继续作为新的被装饰的对象进行装饰。这种架构 下我们可以去递归嵌套多个装饰,从而添加任意多的功能。
也就是说,我们以关联机制来实现的装饰模式其实是以对客户来说透明的方式来动态的给一个对象加上更多的属性,换句话说, 客户端并不会觉的对象在装饰前后有什么不同,这句话说的有点歧义,不过你需要自己理解一下。装饰模式可以在不需要创建额外更 多的子类的情况下去对对象的功能进行扩展,这个就是装饰模式的模式动机。
我再对这里进行一下解析,可能对于前面的动态添加有点误会。简单来看吧,我们只需要在程序编译时去设计出对应的装饰器类 并进行实例化,然后我们就可以在代码中进行设计,在必要时将这些个装饰类绑定上我们需要的类对象,需要注意的是,我们这些装 饰类中,一般都存在一个父类指针用于指向一个相对唯一的对象,这个将会由一定区域内的代码进行使用。接着,当我们想要使用这 个装饰器时,我们只需要去调用对应的类方法即可,此时装饰器就能够实现我们的功能。就比如对应装饰api的多次调用应该实现对 于包含的类对象的多次装饰行为。
UML类图
在任何一个设计模式中,它的UML类图都是重中之重。
继承问题
这里你第一眼可能会注意到其中的继承关系。在这个类图中,你可以看到对应的所有子类的父类都是Component类。为什么呢,不是说我们不应该使用继承关系来实现吗?
这里我们就需要来进行一些实际的分析了。首先我们应该明白,事实上在实际设计中,能够完全符合设计模式原则的项目是少之又少的,各个模式的具体应用落到项目中后,不免会发生一些变形,就比如这里。我们所有的类都是由Compontent类出来的,为什么呢?
这里其实就考虑了一个我们接口设计规范方面的问题了。在我们最理想的设计下,我们是期望我们的抽象装饰类是不依赖于这个Compontent类的。我们期望我们的封装类只包含对应的抽象构建类指针和对应的接口。我们实际上也完全可以这么做,但是我们这里为什么还是将这个装饰类从抽象构建类继承下来的呢。这里就是我们前面提到的接口规范。
接口规范
这我们一个项目的接口规范设计中,这个一般是需要在项目初期就进行确定的。但是吧,如果是手动去规定一个个类中的命名格式,其实是很不切实际的,毕竟这么做的工作量很大,而且一不小心就会出错。所以这里我们就考虑通过继承这种强耦合来实现我们整个装饰模式的接口规范。
我们前文已经提到,我们在装饰模式中,需要尽可能的实现装饰前类和装饰后类都尽可能的对用户透明。这种透明其实可以简单的理解为:通过学习一个类的api方法,我们基本可以通过同名api调用对应的所有类似的类,或者说继承下来的类以及那些通过设计模式实现的具有层次关系的类。这个就是我们的接口规范所能能做到的事。而这里巧妙的运用了继承这种前耦合的关系来进行设计,大大简化了设计的难度。
妥协
对于这个抽象构件类,既然我们在这个装饰模式的设计中,我们需要其作为我们的接口规范,那么我们就还需要对其进行一些属性的限定。在这个抽象构件类中,我们一般需要为其添加任何成员变量,对于不必要的api,我们都不予以实现,一般的api都使用纯虚函数进行限定。通过一系列的规则设置,我们能够了解到这个接口类的规范以及系列子类的接口,大大简化了设计难度以及了解难度。
分析
我们回来对这个结构进行分析,其实我们可以看到,这种继承的妥协是十分优雅的。你看,从抽象构建类中继承下来的具体构建类以及抽象装饰者类中都将具有相同的接口,那么,在我们的抽象装饰者类中,我们就已经可以对于接口进行一层最简单的装饰了,就是在这个装饰者类中的同名函数去调用我们的成员指针所拥有的成员函数。这种对于默认行为的简单规定,将给我们这个抽象装饰类的具体装饰子类的设计省去很多麻烦。
多优雅吧,你看。
代码实例
所有的代码和文档我都会放在另外一个仓库里,自行访问我的gitHub账号获取
抽象构件类
1 | //战士的基类 |
这个是我们上面那个类图中的抽象构件类层次。在我这个类中,我添加了一个成员属性去进行标识,其实这个属性你完全可以丢到等下的子类中,不过丢到这里也无伤大雅了。需要注意的是,如果你一定要添加一些属性在这个抽象基类中,那么这些个属性应该是一些相对简单的,可以通过简单的方法进行操作的,所有比较复杂的操作都不应该被放到基类中。
可以看到,我们这里保留了几个虚方法,在这个层次下定义的虚方法将是所有子类中所需要实现的属性,也就是说,通过在抽象基(母)类中去定义抽象方法,我们可以去规定所有的子类所必须实现的一些属性,无论对应的实现类中是否还添加了自己的方法。
抽象装饰类
1 | //恶魔果实基类 |
这个其实就是我们类图中的抽象装饰类层次。可以看到,在抽象装饰类中,我们只是添加了一个父类的指针。其实我们还可以对这种架构进行进一步的分析。
我们其实可以把装饰类和构件类先组合到一起,如果这样的话其实就是继承下的功能扩展,而且前面的缺陷也已经提到过了就不再赘述。这里之所以将抽象装饰类给它分离出来其实就是为了贴合我们三原则中的单一职责原则,我们等下再结合下面再进行分析。
其实在这个抽象装饰基类中,我们还可以在这个层次中去添加一些方法,如果添加了这些个方法,那么这些个方法将会像抽象装饰类和一层的具体构件类继承抽象构件基类方法一般去继承抽象装饰类的方法。不过由于层次间的隔离,所以这些由抽象装饰类派生出来的子类的方法的顶层只是抽象装饰类。因此,为了保证接口规范,如果我们一定需要在抽象装饰类中去添加方法的话,我们需要保证这些方法的接口应该跟我们抽象构件类的接口相似。而且由于单一职责原则,所有的这些类的功能应该是比较单一的,所以不应该出现接口功能会相差很大导致需要我们去在抽象装饰类中去添加属性的情况。
在抽象装饰类中,我们需要做的其实就是明确我们需要添加的方法。如果必要时,为了可读性,我们应该在抽象装饰类中去添加当前层次的上层需要我们实现的函数,以避免我们需要逐层往上去寻找所有需要实现的方法。
具体构件类
1 | //黑胡子 |
这个类对应了我们UML类图中抽象构件类下的第一层的具体构建类,这个类规定了我们的一个基础具体构建的属性,在这个类中,我们除了必须要去实现抽象类方法以外,我们还可以对属性进行添加,因为这里的性质就是对应着我们生活中的产品。当然,在这种架构下,在这个类中添加的属性是不会被装饰类中所发现的(如果你不去专门的通知的话)。
因此,在这一层次的具体构件类中去添加属性得慎之又慎
即使我们添加了属性,这个属性由于单一职责原则也不应该脱离我们整个类的设计初衷,所以这些属性应该与我们的抽象基类方法之间存在着一些互动。
具体装饰类
1 | //暗暗果实 |
这个类的层次就是我们前面的具体装饰类,这个类中,我们需要去实现所有的由抽象构件基类与上层抽象装饰类需要我们去实现的方法。不过,如果我们设计的架构良好,我们将不必去关注我们的上层到底存在了多少层套娃,只需要去关心最近一层的方法接口,这里的设计其实存在一点注意的细节,就是由于我们的抽象装饰类是抽象类,所以其中可能还一些来自抽象构件类的可以实现的方法,我们需要在子类中去实现这些可能存在的方法。当然,在一般的项目中,这些应该有一些清晰的指引的。
在这里我们就没有对抽象装饰类进行方法的再一次扩充,所以我们可以看到这里我们只需要实现我们抽象构件类要求的方法即可。除此之外,我们可以看到我们的具体装饰类中其实实现了自己的方法,其应用在了要求实现的接口中。这里其实装饰器类的核心,正是这里的嵌套调用起到了装饰的作用。
对于这里,我们举出的其实就只是一些输出的例子。那么如果存在数据处理方面的要求时我们需要怎么做呢?
在设计中,我们其实不想要我们期望的数据本身被修改,在这里就是我们的抽象构件基类中元素name,但是我们还是需要对这些数据进行处理的话,我们势必就需要一些副本来进行数据的处理的,在一些代码中,你可以看到是在抽象装饰基类中去进行一些成员的拷贝再在具体装饰类中去进行对应的操作的。这其实是取决于我们的具体装饰类的行为,如果我们具体装饰类行为普遍需要数据操作,那么我们就会考虑在基类中去添加对应的方法。但如果只是特殊的几个元素,自然也就没必要在基类中去声明了,只在特定的类中去修改即可。
由于我还没去理解网络传输,我不太敢确定具体的传输过程,不过我可以应用一下这个场景。就是说你这个数据说是通过协议来进行了传输,但是你本地还是保留了你要传输的数据的。也就是说,你在这里传输数据的过程中,你至少是会对一些数据进行保护的,这里最浅显来看就是我们本地的要传输的数据始终是在本地有一份的。也就是说,这种多层的装饰,一般始终会对上一层的数据进行保护,不会直接去修改原本的数据,即使要使用,要修改,一般也只会涉及到副本。
测试
1 | void text() |
输出
通过上面那些,我们其实可以了解到装饰模式下的一个本质
一个对象,只有经历过装饰了,那么它才能够具有这个装饰类的属性。如果我们将这个装饰类给从这个装饰完的类中剥离出来,那么这个类将会失去这些被赋予的属性。
上面那个测试程序中就体现了这一点,我们使用了抽象构件进行了一个具体对象的构建,这个对象的输出也只是这个实现类对应的自己的方法,然后我们通过这个构件去为其调用具体装饰类的构建,通过调用对应的同名接口,我们可以看到其产生了不同的行为。接下来,我们将这个装饰类给他删除了并重新调用了具体构件类的方法,可以看到这个类输出的还是进入装饰类之前的方法。由此也证明了上面的那句话。
1 | 黑胡子 with great power. |