在软件设计中,某个对象会组合很多不同的功能,如果把所有功能都写在这个对象所在的类里,该类会包含很多复杂的代码逻辑,导致代码不美观且难以维护。于是就有了再定义一些新类。这些类负责各自的功能模块,就会实例化一些各司其职的对象。而这些对象再跟原始对象进行组合,以共同完成一个复杂的完整功能。这些对象就称为装饰对象,主要为原对象进行附加功能。有个问题就是,如何把装饰对象跟原对象进行组合的同时,又保证不修改原对象的情况下,进行扩展不同的附加功能。这时候装饰模式就派上用场了。
比如有个推广产品的功能需求:用户看视频后就奖励一个红包,需要做成一个红包生成器。红包具有不同的功能,皮肤换装、显示特效等。根据装饰模式来分析,红包是原对象,作为红包最原始的模样。而皮肤和特效是装饰对象,是为红包装饰的,附加了不同的功能(如具有春节风格功能的皮肤,具有抖动功能的特效)。
针对红包生成器,装饰模式的主要运用方面:
1、用户自定义可选功能: 用户可勾选使用特效功能,弹出时会有抖动的效果。
(这里的用户可分为使用红包的用户,和调用红包生成器底层库接口的程序员)
2、迭代版本可选功能: 在迭代版本中, 我们会对某一模块进行动态地移除或添加功能。如在上个版本中,分析到红包精美的皮肤并不是用户的重要需求,并且传送新皮肤会增加网络资源的成本,反而简洁的界面更能吸引到用户。于是在此版本中可移除掉这个功能,从而不会修改原对象里的代码逻辑。
即装饰模式的主要作用是,用户可以灵活地动态地扩展附加功能,并且不会影响原对象的代码逻辑。
以下是有关红包生成器错误设计的例子:
//方式一:原对象继承其他多个装饰对象的接口
//public interface IRedPacket...//红包,原对象接口
//public interface IEffect...//特效
//public interface ISkin...//皮肤
public class RedPacket: IRedPacket,IEffect, ISkin
{
public RedPacket() { }
public void setPlace()//实现 IRedPacket.setPlace 方法
{
setSound();//实现 IEffect.setSound 方法
setShake();//实现 IEffect.setShake 方法
setBgImg();//实现 ISkin.setBgImg 方法
//...
//如果有移除或扩展某些功能的新需求时,
//需要继承或者移除某个基类,就会修改某些实现的方法。
}
public void setSound() { }
public void setShake() { }
public void setBgImg() { }
//...
}
//方式二:在原对象所在的类里进行对象组合:
// public class Effect:IEffect...
//public class Skin:ISkin...
public class RedPacket : IRedPacket
{
private Effect effect;
private Skin skin;
public RedPacket() { }
public void setPlace()
{
effect.setSound();
effect.setShake();
skin.setBgImg();
//...
//如果有移除或扩展某些功能的新需求时,
//需要继承或者移除某个装饰对象,就会修改某些代码逻辑。
}
//...
}
//不管是在原对象里进行对象组合,还是原对象继承其他对象的基类:
//当有新需求时,都会对原对象进行修改代码逻辑,从而导致难以扩展和维护的问题。
由以上代码可知,在这两种方式中,如果遇到新需求需要修改时,第一种方式往往会比第二种方式更加的困难,因为多继承关系需要对某些基类的每个方法进行实现。第二种方式也好不了哪里去,因为在原对象里有修改过的痕迹。
特点:
结构:
(以下结构中的抽象组件为抽象类或接口)
适合应用场景特点:
需求:
在手机上根据不同的应用场景,会出现具有不同功能和风格的虚拟键盘。在转账界面时,弹出数字模式键盘。在搜索框里,一旦输入一两个字母时就会有以输入字母开头的预知单词浮现。在聊天框里,不仅有预知单词,还有表情包的工具栏。
设计分析:
1、定义原对象抽象接口和装饰对象抽象接口:
//Component:原对象抽象类
public interface Ikeyboard
{
void show();//生成并弹出键盘的公共接口
}
//Decorator:装饰对象抽象类
//方式一:在某些书中,有提到:可以跳过定义此 Decorator 抽象类的步骤,
//就直接定义public class NumberMode:Ikeyboard,然后类里包装一个 Ikeyboard 派生实例。
//方式二(利用装饰接口,遵循装饰模式的结构):
//public interface INumberMode...//定义某个指定的 Decorator 抽象类
//public class NumberMode:INumberMode...//类里包装一个 Ikeyboard 派生实例。
//先忽略以上方式一和方式二,等理解透了本例子再回头来理解就知道怎么用了。
//但为了遵循装饰模式的结构,在这里我还是定义了一个更规范的 Decorator 抽象类。(被用于单继承的基类)
public abstract class Decorator : Ikeyboard
{
protected Ikeyboard ikeyboard;
public Decorator(Ikeyboard ikeyboard)
{
this.ikeyboard = ikeyboard;
}
public virtual void show() { }
//添加其他抽象方法或实现的方法...
}
2、定义原对象具体类和装饰对象具体类:
//ConcreteComponent:原对象具体类(键盘)
public class Keyboard : Ikeyboard
{
public Keyboard() { }
public void show() { }
}
//ConcreteDecorator:装饰对象具体类(数字模式键盘)
public class NumberMode : Decorator
{
public NumberMode(Ikeyboard ikeyboard):base(ikeyboard)
{
base.ikeyboard = ikeyboard;
}
public override void show() { ikeyboard.show(); Console.WriteLine("切换为数字模式键盘."); }
}
//ConcreteDecorator:装饰对象具体类(预知单词)
public class PresetWords : Decorator
{
public PresetWords(Ikeyboard ikeyboard) : base(ikeyboard)
{
base.ikeyboard = ikeyboard;
}
public override void show() { ikeyboard.show(); Console.WriteLine("增加了预知单词功能."); }
}
//ConcreteDecorator:装饰对象具体类(表情包工具)
public class EmojiPack : Decorator
{
public EmojiPack(Ikeyboard ikeyboard) : base(ikeyboard)
{
base.ikeyboard = ikeyboard;
}
public override void show() { ikeyboard.show(); Console.WriteLine("增加了表情包工具."); }
}
//还可扩展其他功能,如
//public class TouchKeypad : Decorator...//手写键盘装饰类
//public class QuickPhrases: Decorator...//快捷短语装饰类...
3、主程序:
class Program
{
static void Main(string[] args)
{
Ikeyboard keyboard, numberMode, presetWords, preWords, chatKeyboard;
//原对象
keyboard = new Keyboard();
//1.在转账时,弹出附带数字键盘模式的键盘
numberMode = new NumberMode(keyboard);
Console.WriteLine("\n在转账时:");
numberMode.show();
//2.在搜索框里,弹出附带预知单词的键盘
presetWords = new PresetWords(keyboard);
Console.WriteLine("\n在搜索框里:");
presetWords.show();
//3.在聊天框里,弹出既有预知单词,又有表情包功能的键盘
preWords = new PresetWords(keyboard);
chatKeyboard = new EmojiPack(preWords);//preWords 里有 PresetWords 功能
Console.WriteLine("\n在聊天框里:");
chatKeyboard.show();//chatKeyboard 里有 EmojiPack 和 PresetWords 的功能。
Console.ReadLine();
}
}
以上关于虚拟键盘的例子,是属于透明装饰模式。在软件编程的领域中,所谓透明的意思就是用户只需要调用附加功能的公共方法,而无须知道某个装饰对象里的一些具体公开行为。而半透明的意思就是用户只可以知道指定装饰对象的一些具体公开行为。
通俗地来说,就像是某家店的玻璃门。假定玻璃门的门把手是透明的,在门外急性子的顾客因为没法找到门把手,冒然冲进去必定撞得头破血流。而这个门把手如果是半透明的,有一层灰色罩着,门外的顾客会选择先用门把手拉门再进去。(这里的开门行为属于作为玻璃门的门把手这个装饰对象的,顾客可以直接调用这装饰对象的开门功能)
与透明相比,除了附加功能之外,用户还可以对指定的装饰对象动态地进行处理比较详细的逻辑。
半透明装饰模式的例子
需求:
大家协同开发一个客户管理系统,分工负责各自的任务。程序员A负责对某些编辑框进行封装。其中有一个显示客户信息的界面,需要封装一种编辑框用来输入客户的联系电话。然后把编辑框模块提供给另一个程序员B使用。程序员B可以对该编辑框的输入设置限制为11位数字的手机号格式。可复制该内容,但不可把其他内容粘贴在此处。
设计分析:
//Component:原对象抽象类(编辑框接口)
public interface ITextEdit
{
void create();
}
ConcreteComponent:原对象具体类(编辑框)
public class TextEdit : ITextEdit
{
public TextEdit() { }
public void create() { }
}
//Decorator:装饰对象抽象类
public abstract class AbsTelephoneDecorator : ITextEdit
{
protected ITextEdit iTextEdit;
public AbsTelephoneDecorator(ITextEdit iTextEdit)
{
this.iTextEdit = iTextEdit;
}
public virtual void create() { }
public abstract void setNumberLength(int len);
public abstract void setCanCopy(bool isCanCopy);
public abstract void setCanPaste(bool isCanPaste);
}
//ConcreteDecorator:装饰对象具体类(联系电话)
public class TelephoneDecorator : AbsTelephoneDecorator
{
public TelephoneDecorator(ITextEdit iTextEdit) :base(iTextEdit)
{
base.iTextEdit = iTextEdit;
}
public override void create() { iTextEdit.create(); Console.WriteLine("手机号的编辑框创建完成."); }
public override void setNumberLength(int len)
{
Console.WriteLine($"set NumberLength:{len}");
}
public override void setCanCopy(bool isCanCopy)
{
Console.WriteLine($"IsCanCopy:{isCanCopy}");
}
public override void setCanPaste(bool isCanPaste)
{
Console.WriteLine($"IsCanPaste:{isCanPaste}");
}
}
//主程序
class Program
{
static void Main(string[] args)
{
//半透明装饰模式,用户可以对指定装饰对象里的一些行为进行处理。
//原对象
ITextEdit textEdit = new TextEdit();
//装饰对象
AbsTelephoneDecorator telephone = new TelephoneDecorator(textEdit);
//用户设置装饰对象的一些行为
telephone.setNumberLength(11);
telephone.setCanCopy(true);
telephone.setCanPaste(false);
telephone.create();
//透明装饰模式
ITextEdit telephone2 = new TelephoneDecorator(textEdit);
//telephone2 无法提供装饰对象的 setNumberLength setCanCopy setCanPaste。
Console.ReadLine();
}
}