结构型模式之装饰器模式

模式定义

  装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。

模式动机

一般有两种方式可以实现给一个类或对象增加行为:

  • 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。
  • 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)

应用实例

  1. 拿英雄联盟举例吧。初始时一个英雄有自己基础的属性,例如攻击力啊,血量,蓝量,防御等。随着时间的推移,英雄们购买一些装备去增加这些属性例如狂徒,无尽,反甲等,而这些增加装备是由客户后续不定时添加的,是动态的,所以不能选择继承的方式(英雄继承狂徒的属性),而是让英雄添加一个狂徒的属性。

模式结构

装饰模式包含如下角色:

  • Component: 抽象构件
  • ConcreteComponent: 具体构件
  • Decorator: 抽象装饰类
  • ConcreteDecorator: 具体装饰类

image.png
  如上图所示,需要被装饰的对象在最上方,它自身可以有自己的实例,一般通过抽象类来实现
  右侧中间是一个装饰器类或者接口,其实内容与原对象基本一致,不过我们自定义的装饰器一般会继承这个装饰器基类。
  最下层就是具体的装饰器了,可以看到,具体装饰器类中需要包含被装饰对象成员(也就是说,装饰器需要和被装饰对象有同样的子类),然后增加一些额外的操作。

代码分析

下面代码就是我上述应用实例中举例的实现,英雄穿装备

//抽象英雄
class AbstractHero {
public:
	virtual void ShowStatus() = 0;
public:
	int mHp;
	int mMp;
	int mAt;
	int mAr;
};

//具体的英雄
class Hero :public AbstractHero {
public:
	Hero() {
		mHp = 0;
		mMp = 0;
		mAt = 0;
		mAr = 0;
	}
	virtual void ShowStatus() {
		cout << "血量:" << mHp << endl;
		cout << "蓝量:" << mMp << endl;
		cout << "攻击力:" << mAt << endl;
		cout << "护甲:" << mAr << endl;
	}
};
//抽象的装饰类,英雄穿上了某件装备,还是一个英雄
class AbstractEquipment :public AbstractHero {
public:
	AbstractEquipment(AbstractHero* hero) { this->pHero = hero; }
	virtual void ShowStatus() {}
protected:
	AbstractHero* pHero;
};

//增加一个狂徒装饰器
class KuangtuEquipment :public AbstractEquipment {
public:
	KuangtuEquipment(AbstractHero* hero) :AbstractEquipment(hero) {}
	void AddKuangtu() {  //在原来的基础上增加一些东西
		this->mHp = this->pHero->mHp + 1000;
		this->mMp = this->pHero->mMp;
		this->mAr = this->pHero->mAr;
		this->mAt = this->pHero->mAt;
	}
	virtual void ShowStatus() {
		AddKuangtu();
		cout << "血量:" << this->mHp << endl;
		cout << "蓝量:" << this->mMp << endl;
		cout << "攻击力:" << this->mAt << endl;
		cout << "护甲:" << this->mAr << endl;

		delete this->pHero;  //把原来的对象删除
	}
};

class WujinEquipment :public AbstractEquipment {
public:
	WujinEquipment(AbstractHero* hero) :AbstractEquipment(hero) {}
	void AddWujin() {
		this->mHp = pHero->mHp;
		this->mMp = pHero->mMp;
		this->mAr = pHero->mAr;
		this->mAt = pHero->mAt + 80;
	}

	virtual void ShowStatus() {
		AddWujin();
		cout << "血量:" << this->mHp << endl;
		cout << "蓝量:" << this->mMp << endl;
		cout << "攻击力:" << this->mAt << endl;
		cout << "护甲:" << this->mAr << endl;
		delete this->pHero;
	}
};

void test1() {
	AbstractHero* hero = new Hero;
	cout << "初始英雄,没有装备" << endl;
	hero->ShowStatus();

	cout << "英雄穿上狂徒之后" << endl;
	hero = new KuangtuEquipment(hero);
	hero->ShowStatus();

	cout << "英雄穿上无尽之后" << endl;
	hero = new WujinEquipment(hero);
	hero->ShowStatus();

	delete hero;
}

上述代码输出如下

初始英雄,没有装备
血量:0
蓝量:0
攻击力:0
护甲:0
英雄穿上狂徒之后
血量:1000
蓝量:0
攻击力:0
护甲:0
英雄穿上无尽之后
血量:1000
蓝量:0
攻击力:80
护甲:0

模式分析

  • 与继承关系相比,关联关系的主要优势在于不会破坏类的封装性,而且继承是一种耦合度较大的静态关系,无法在程序运行时动态扩展。在软件开发阶段,关联关系虽然不会比继承关系减少编码量,但是到了软件维护阶段,由于关联关系使系统具有较好的松耦合性,因此使得系统更加容易维护。当然,关联关系的缺点是比继承关系要创建更多的对象。
  • 使用装饰模式来实现扩展比继承更加灵活,它以对客户透明的方式动态地给一个对象附加更多的责任。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。

装饰器模式的优缺点

优点

  • 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
  • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
  • 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
  • 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”

缺点

  • 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
  • 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。

适用环境

  1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  2. 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
  3. 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类).

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://neo00.top/archives/结构型模式之装饰器模式