面向设计模式的问题-解决方案方法:按需扩展(装饰器模式)






4.38/5 (4投票s)
本文讨论装饰器设计模式。本系列文章采用的方法是识别、分析问题模式并用合适的设计模式来解决。
序言
如果我们放眼四周,会发现我们身边有很多重复出现的问题,如果我们深入观察,就会发现每个问题背后都涉及一套原因模式。为了克服问题带来的困难,我们通常会围绕症状来创造解决方案,即使识别问题模式并围绕模式创造解决方案会更明智,从而避免问题发生。
在编程世界中,重复出现的问题很普遍。其后果很可能是购买昂贵的设备、一遍又一遍地重复编写相同的代码、从头开始重写和修改大量内容,最糟糕的情况下,如果修改成本超过了它能带来的收入,产品线将被关闭。
但好消息是,随着时间的推移,那些懒惰而聪明的程序员们想出了解决这些日常问题的技术和设计模式,通常被称为**设计模式**。
范围
顾名思义,装饰器模式与装饰某物相关——确切地说,是对象。对象可以通过多种方式进行扩展。一种方法是继承。但是,当需要为特定类的单个实例添加额外的属性、职责或功能,但又不影响主类时,这就属于装饰器模式的范畴了。
问题
在面向对象编程中,继承是扩展对象的_基本_方式。概念很简单——创建一个基类,然后在其子类中按需扩展。但这使得扩展与基类紧密绑定,客户端无法动态使用这些扩展。
为了解释这个问题,让我们假设你去了汽车商店,订购了一辆带扰流板的汽车;用编程术语来说,就是CarWithSpoiler
。一旦建成,你希望它是蓝色的。你心想,“这只是在车上喷了层颜色。”但令人惊讶的是,汽车制造商报废了整辆车,重新开始建造一辆带有“Blue
”颜色的ColouredCarWithSpoiler
。当你试图接受这个想法时,另一位顾客来了,要了一辆蓝色的汽车。制造商回答说:“抱歉,帮不了你,我们只有Car
、CarWithSpoiler
和ColouredCarWithSpoiler
,但没有ColouredCar
。”
凭借你超凡的才华和智慧,你设法弄清楚了幕后工作的代码。
有一个名为ICar
的接口,其中包含了汽车的行为。在这个例子中,我们假设只有一个行为GetDescription()
。
public interface ICar
{
string GetDescription();
}
有两个类实现了该接口:Car
和CarWithSpoiler
。你自问:“带扰流板的汽车除了是一辆汽车之外,还有什么别的作用吗?”“没有,它并没有改变汽车的行为。它只是安装在汽车上的扰流板。”你找到了答案。CarWithSpoiler
通过一个名为Spoiler
的属性扩展了Car
。
public class Car : ICar
{
public Car()
{
Console.WriteLine("Creating Car");
}
public string GetDescription()
{
return "Car";
}
}
public class CarWithSpoiler : Car, ICar
{
public string Spoiler { get; set; }
public CarWithSpoiler()
{
Console.WriteLine("Creating CarWithSpoiler");
Spoiler = "Spoiler";
}
public string GetDescription()
{
return base.GetDescription() + " " + Spoiler;
}
}
现在,汽车制造商提到了ColouredCarWithSpoiler
,它也应该具有与带扰流板的汽车相同的行为,但它扩展了另一个属性Colour
。构造函数必须期望传入颜色,以便知道要应用哪种颜色。
public class ColouredCarWithSpoiler : CarWithSpoiler, ICar
{
public string Colour { get; set; }
public ColouredCarWithSpoiler(string colour)
{
Console.WriteLine("Creating ColouredCarWithSpoiler");
Colour = colour;
}
public string GetDescription()
{
return Colour + " " + base.GetDescription();
}
}
当你模拟这个场景时
class Program
{
static void Main(string[] args)
{
Test(); ;
Console.ReadKey();
}
private static void Test()
{
Console.WriteLine("Testing Your Car without Decorator Pattern");
Console.WriteLine("------------------------------------------");
ICar carWithSpoiler = new CarWithSpoiler();
Console.WriteLine("Oh snap!!! You wanted a colour... Scrap it and create a new one...");
ICar colouredCarWithSpoiler = new ColouredCarWithSpoiler("Blue");
Console.WriteLine(colouredCarWithSpoiler.GetDescription());
}
}
所以,问题就出现了。制造商无法为另一位顾客服务,因为系统中没有定义一个扩展了Car
并带有Colour
的类。而且,考虑到可伸缩性,在这种情况下,这种扩展方法会非常昂贵。想象一下,如果有一个新的配件,比如“雾灯”,就需要实现“扰流板”、“颜色”和“雾灯”的所有组合来支持所有客户。这样一来,与我们的Car
类相比,我们就有了七个扩展类,每个新扩展项就需要 4 个新的扩展类。
因此,如果扩展是相互独立的,那么这种继承式的对象扩展方法存在两个问题。
- 客户端在实例化之前必须确定自己需要什么。扩展不能动态地、随时按需添加。
- 如果需要任何新的扩展,就需要对系统进行大量更改,以支持所有可能的组合。
解决方案:装饰器模式
在需要动态扩展对象且每个扩展相互独立的情况下,更灵活的做法是用扩展代码包装核心对象。包装类称为Decorator
。正式地说,装饰器模式允许我们在不影响对象实现的情况下,动态地为对象附加额外的责任。
在这种方法中,扩展类与核心对象类具有“has-a”关系,同时还具有“is-a”关系。换句话说,除了扩展核心对象类之外,扩展类还包含核心对象的一个实例,并用扩展的属性和行为对其进行包装。由于装饰器从主对象类扩展而来,在包装了扩展代码后,它仍然可以作为主对象被替换。
下图可以让我们更好地了解情况
AbstractObject
ConcreteObject
的抽象。
AbstractDecorator
装饰器的抽象,从AbstractObject
扩展而来。它抽象了需要被装饰的AbstractObjects
的行为,并包含一个AbstractObject
的实例。
ConcreteObject
AbstractObject
的实现。
ConcreteDecorator1 and ConcreteDecorator2
从AbstractDecorator
扩展而来,用额外的职责实现了抽象行为。
何时应用
装饰器模式可以应用于以下一种或两种场景:
- 需要动态地为单个对象添加可以随时撤销的额外责任。
- 并且需要在不影响其他对象的情况下进行。
- 大量的独立扩展是相互独立的,并且需要指数级增长的子类来支持所有可能的扩展组合。
实现
让我们用装饰器模式来实现我们当前问题的解决方案。
正如我们已经拥有的,有一个名为ICar
的抽象,它已经被Car
实现了。
public interface ICar
{
string GetDescription();
}
public class Car : ICar
{
public Car()
{
Console.WriteLine("Creating Car");
}
public string GetDescription()
{
return "Car";
}
}
现在,根据装饰器模式,我们需要为装饰器创建一个抽象,它实现ICar
并包含一个ICar
。我们称之为CarDecoratorBase
。
public abstract class CarDecoratorBase : ICar
{
protected ICar car;
public abstract string GetDescription();
}
一旦我们有了装饰器的抽象,就让我们继续处理具体的装饰器并实现抽象行为。我们不修改通过构造函数传递进来的汽车的行为,而是在其之后添加一些额外的东西。
public class Spoiler : CarDecoratorBase
{
public Spoiler(ICar car)
{
Console.WriteLine("Creating Spoiler");
this.car = car;
}
public override string GetDescription()
{
return this.car.GetDescription() + " + Spoiler";
}
}
这里发生的事情是一样的,只是我们先添加装饰,然后执行汽车的行为。
public class Colour : CarDecoratorBase
{
string _colour;
public Colour(ICar car, string colour)
{
Console.WriteLine("Applying Colour");
this.car = car;
_colour = colour;
}
public override string GetDescription()
{
return _colour + " " + this.car.GetDescription();
}
}
现在,进行测试。
class Program
{
static void Main(string[] args)
{
Test();
Console.ReadKey();
}
private static void Test()
{
Console.WriteLine("Testing Your Car with Decorator Pattern");
Console.WriteLine("---------------------------------------");
ICar car = new Car();
car = new Spoiler(car);
Console.WriteLine("Oh snap!!! You wanted a colour... No worries :)... Decorate it with colour");
car = new Colour(car, "Blue");
Console.WriteLine(car.GetDescription());
Console.WriteLine("\n----------------------------------");
Console.WriteLine("\nNew Customer wants a Green Car.\n");
Decorator.Solution.ICar car2 = new Decorator.Solution.Car();
car2 = new Colour(car2, "Green");
Console.WriteLine(car2.GetDescription());
}
}
嗯,似乎我们能够利用现有资源满足任何可能的请求。
优点和缺点
万事皆有优缺点。如果实现得当,它会带来两个主要好处:
- 与继承方法相比,它为扩展对象并添加责任提供了更大的灵活性。
- 每个装饰器都是自描述的。因此,我们不需要带有所有责任组合的类。
至于缺点,从现实世界的角度来看,它会造成一些小小的歧义,因为装饰器是抽象对象的扩展,但它们通常并非如此。在我们的例子中,扰流板实际上不是汽车,尽管它们实现了ICar
。当涉及相当多的装饰器时,它还使得代码难以阅读和理解,因为跟踪装饰链变得困难。
然而,由于其轻量级和易于实现,装饰器在需要时非常有用且方便。
实际示例
假设你需要创建一个按钮,该按钮要适应所有尺寸的设备。对于小设备,它会显示一个图标;对于中等设备,它会显示一个文本;对于大设备,它会同时显示图标和文本。装饰器模式非常适合这种情况,你可以用文本和图标来装饰按钮。
结论
尽管存在其带来的弊端,装饰器模式仍然非常容易理解和实现。装饰器通常是出于必然而产生的。此外,装饰器通常很小,易于维护,而且更重要的是,它们是单元测试的。在考虑装饰器模式时,还应考虑诸如_桥模式_、_适配器模式_和_组合模式_等其他设计模式,用于替换或组合使用。
源代码也可在GitHub上获取。