文章目录
- 一、引言
- 二、外观模式
- 三、总结
一、引言
外观模式是一种结构型设计模式, 能为程序库、 框架或其他复杂类提供一个简单的接口。也就是说,该模式的目的用于隔离接口,换句话说,就是扮演中间层的角色,把本来结合紧密的两部分模块(系统)隔离开来,让这两部分内容通过中间层来打交道(类似于依赖倒置原则:高层和低层都依赖于抽象层),从而极大地降低了两部分模块之间的耦合性并通过中间层对许多操作进行简化。
二、外观模式
一款游戏可玩的内容越多,或者说功能越多,它的配置项也越多。以《英雄联盟》为例,其配置项就分好多类,例如有图形、声音、语音聊天等,而每个配置项都会有很多可供配置的细节内容,例如:
- 图形一一是否全屏显示、是否开启特效、窗口分辨率、是否开启抗锯齿等。
- 声音一一是否开启背景声音、是否开启环境音效、是否开启表情声音、音量大小设
置等。 - 聊天设置一一麦克风音量、麦克风灵敏度、聊天音量等。
现在,策划希望在闯关打斗类游戏项目中也引人这些配置项。程序经过思考,准备创建3个类来分别实现图形、声音、语音聊天相关功能,因为这些类在整个项目中只保持一个对象就可以,因此准备将这3个类设置为单例,代码如下:
//图形相关
class graphic
{
public:
static graphic& getInstance()
{
static graphic instance;
return instance;
}
void display(bool enable) {//是否全屏显示(true:是)
cout << "图形->是否全屏显示->" << enable << endl;
}
void effect(bool enable) {//是否开启特效(true:是)
cout << "图形->是否开启特效->" << enable << endl;
}
void resolution(int index) {//设置窗口分辨率
cout << "图形->分辨率设置选项->" << index << endl;
}
void antialiasing(bool enable) {//是否开启抗锯齿(true:是)
cout << "图形->是否开启抗锯齿->" << enable << endl;
}
private:
graphic() {}
graphic(const graphic&) = delete;
graphic& operator=(const graphic&) = delete;
~graphic() {}
};
//声音相关
class sound
{
public:
static sound& getInstance()
{
static sound instance;
return instance;
}
void bgsound(bool enable) {
cout << "声音->是否开启背景声音->" << enable << endl;
}
void envirsound(bool enable) {
cout << "声音->是否开启环境声音->" << enable << endl;
}
void expsound(bool enable) {
cout << "声音->是否表情环境声音-" << enable << endl;
}
void setvolume(int level) {
cout << "声音->音量大小为->" << level << endl;
}
private:
sound() {}
sound(const sound&) = delete;
sound& operator=(const sound&) = delete;
~sound() {}
};
// 语音聊天相关类
class chatvoice {
private:
chatvoice() {};
chatvoice(const chatvoice&) = delete;
chatvoice& operator=(const chatvoice&) = delete;
~chatvoice() {};
public:
static chatvoice& getInstance() {
static chatvoice instance;
return instance;
}
public:
void micvolume(int level)//麦克风音量大小设置(0~100)
{
cout << "语音聊天->麦克风音量大小为->" << level << endl;
}
void micsens(int level)//麦克灵敏度设置(0~100)
{
cout << "语音聊天->麦克风灵敏度为->" << level << endl;
}
void chatvolume(int level)//聊天音量设置(0~100)
{
cout << "语音聊天->聊天音量为->" << level << endl;
}
};
从上面的代码中可以看到一个问题,一个项目中可能有多个单例类,从而造成了一些代码冗余,例如这些单例类的构造函数、拷贝构造函数、拷贝赋值运算符以及析构函数只有名字不同,每个单例类也都需要有getInstance
成员函数。所以实际上可以实现一个单例类模,通过单例类模板可以避免多个单例类的定义中造成的代码冗余。
我们使用如下代码来实现对游戏中的图形、声音、语音的处理::
graphic& g_gp = graphic::getInstance();
g_gp.display(false);
g_gp.effect(true);
g_gp.resolution(2);
g_gp.antialiasing(false);
cout << "**********" << endl;
sound& g_snd = sound::getInstance();
g_snd.setvolume(80);
g_snd.envirsound(true);
g_snd.bgsound(false);
cout << "**********" << endl;
chatvoice & g_cv = chatvoice::getInstance();
g_cv.chatvolume(70);
g_cv.micsens(65);
/*
图形->是否全屏显示->0
图形->是否开启特效->1
图形->分辨率设置选项->2
图形->是否开启抗锯齿->0
**********
声音->音量大小为->80
声音->是否开启环境声音->1
声音->是否开启背景声音->0
**********
语音聊天->聊天音量为->70
语音聊天->麦克风灵敏度为->65
*/
我们可以把上述三各类称为业务类,实际使用这些类接口的代码就是客户端代码。上述代码比较简单,实际上是业务类彼此之间还可能有相互调用关系。而且客户端代码可能出现在任何地方,因此,客户端代码和业务类直接交互是比较麻烦的。
如果业务类是第三方机构编写的,我们可以建议第三方机构设计一个新的类,隔在客户端代码和业务类之间,扮演中间层的角色。客户端不再与业务类交互,而是直接与新的类打交道。这个新的类扮演的就是外观模式的角色。
对客户端提供一些简单的调用接口(这些接口往往可以实现一系列动作),通过接口与业务类打交道来大大简化客户端代码直接与业务类打交道时的复杂性。
下面我们设计一个conffacade
类,提供两个接口,一个用于较高配置的电脑,一个用于较低配置的。
//扮演外观模式角色的类
class conffacade {
public:
conffacade(const conffacade&) = delete;
conffacade& operator=(const conffacade&) = delete;
~conffacade() {}
static conffacade& getInstance() {
static conffacade instance;
return instance;
}
void LowConfComputer()//对于低配置计算机,只开启一些低配置选项
{
graphic& g_gp = graphic::getInstance();
g_gp.display(true); //全屏耗费资源更低
g_gp.effect(false);
g_gp.resolution(2);
g_gp.antialiasing(false);
sound& g_snd = sound::getInstance();
g_snd.bgsound(false);
g_snd.envirsound(false);
g_snd.expsound(false);
g_snd.setvolume(15);
chatvoice& g_cv = chatvoice::getInstance();
g_cv.micvolume(20);
g_cv.micsens(50);
g_cv.chatvolume(60);
}
void HighConfComputer() {//对于高配置计算机,能达到最好效果的项全部开
graphic& g_gp = graphic::getInstance();
g_gp.display(false);
g_gp.effect(true);
g_gp.resolution(0);
g_gp.antialiasing(true);
sound& g_snd = sound::getInstance();
g_snd.bgsound(true);
g_snd.envirsound(true);
g_snd.expsound(true);
g_snd.setvolume(50);
chatvoice& g_cv = chatvoice::getInstance();
g_cv.micvolume(100);
g_cv.micsens(100);
g_cv.chatvolume(100);
}
private:
conffacade() {}
};
这样的话我们就可以使用如下代码来达到目的:
conffacade& g_cffde = conffacade::getInstance();
cout << "低配置计算机,调用LowConfComputer接口" << endl;
g_cffde.LowConfComputer();
cout << "********" << endl;
cout << "高配置计算机,调用HighConfComputer接口" << endl;
g_cffde.HighConfComputer();
如果客户端代码和业务类之间直接进行交互,那么两者的耦合度就会比较高,当对业务类代码进行修改时,可能也需要同时修改客户端的代码。通过引入扮演外观模式角色的类conffacade
,将客户端与具体的复杂业务类接口分隔开,客户端只需要与外观角色打交道,而不需要与业务类内部的很多接口打交道,这就降低了客户端和业务类之间的代码耦合度。
外观模式结构
一般来说,外观模式包含两种角色:
- 外观角色(Facade):
conffacade
类扮演着这个角色,客户端可以调用这个角色的方法(成员函数),该方法将客户端的请求传递到业务类中去处理。 - 子系统角色(SubSystem):子系统就是前面提到的业务类,项目中可以有多个子系统,每个子系统也可以是一个单独的类,也可以是彼此之间具有耦合关系的类,这些角色可以被外观角色调用,也可以被客户端直接调用。
也可以创建附加外观 (Additional Facade) 类可以避免多种不相关的功能污染单一外观, 使其变成又一个复杂结构。 客户端和其他外观都可使用附加外观。
引入外观模式的定义(设计意图):提供了一个统一的接口来访问子系统中的一群接口。外观模式定义了一个高层接口,让子系统更容易调用。
三、总结
外观模式定义一组高层接口或者说提供子系统的一个到多个简化接口让子系统更容易使用以及让操作变得更直接,让客户端和子系统实现解耦。当子系统发生改变时,一般只需要修改外观类而基本不需要修改客户端代码(虽然这种修改有点违背开闭原则)。
外观模式提供的接口给客户端调用带来了很大的方便,使客户端无须关心子系统的工作细节,但这些接口也许缺乏灵活性,当然,如果客户端认为有必要对子系统进行最灵活的控制,那么依然可以绕过外观模式类而直接使用子系统。
一个项目中可以有多个外观类,每个外观类都负责与一些特定的子系统进行交互,即便是同一个子系统,也可以有多个外观类。外观模式为客户端和子系统之间提供了一种简化的交互渠道,但并没有为子系统增加新的行为,如果希望增加新的行为,则应该通过修改子系统角色来实现。
外观可以让自己的代码独立于复杂子系统。但是也可能成为与程序中所有类都耦合的上帝对象。
再举一个更易理解的例子,普通照相机与傻瓜相机,傻瓜照相机就扮演着外观模式的角色,傻瓜照相机不需要拍照者调整光圈等直接按下快门即可拍照。
外观模式为现有对象定义了一个新接口; 适配器模式则会试图运用已有的接口。 适配器通常只封装一个对象;外观通常会作用于整个对象子系统上。
当只需对客户端代码隐藏子系统创建对象的方式时,可以使用抽象工厂模式来代替外观。
享元模式展示了如何生成大量的小型对象, 外观则展示了如何用一个对象来代表整个子系统。
外观和中介者模式的职责类似: 它们都尝试在大量紧密耦合的类中组织起合作。外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。中介者将系统中组件的沟通行为中心化。 各组件只知道中介者对象, 无法直接相互交流。
外观类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。外观与代理模式的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。 代理与其服务对象遵循同一接口, 使得自己和服务对象可以互换, 在这一点上它与外观不同。