何时使用装饰器模式?

85

我正在审查我的设计模式,而我尚未在我的编码中认真使用的一种模式是装饰者模式。

我理解这个模式,但我想知道一些现实世界中使用装饰者模式的好的具体例子,即需要装饰者模式作为最佳/最优/最优雅解决方案的特定情况。

谢谢。


可能是何时需要使用装饰器模式?的重复问题。 - brainless
10个回答

99

装饰器模式用于为现有的对象(即在运行时已实例化的类)添加附加功能,而不是针对对象的和/或子类。通过继承一个对象的类,可以很容易地向整个类别的对象添加功能,但无法以这种方式扩展单个对象。通过使用装饰器模式,您可以为单个对象添加功能,并使其它相似的对象保持不变。

在Java中,装饰器模式的经典示例是Java I/O Streams的实现。

FileReader       frdr = new FileReader(filename);
LineNumberReader lrdr = new LineNumberReader(frdr);

上面的代码创建了一个读取器 -- lrdr,它从文件中读取并跟踪行号。第1行创建了一个文件读取器 (frdr),第2行增加了行号跟踪。

实际上,我强烈建议您查看Java I/O类的Java源代码。


45

装饰器模式在流技术中被广泛应用:您可以使用一个流来包装另一个流,以获得增加的功能。就我所知,这在 .Net 框架中常见,其他地方也可能存在。我最喜欢的是将 GZipStream 用于 FileStream,以实现额外的压缩。


23
同样地,Java 流只是一组巨大的装饰器集合,这些装饰器装饰了一个装饰另一个装饰某个东西的装饰器。我的脸都要融化了。 - Hooray Im Helping
11
我认为在我桌子上的《Head First: 设计模式》书中,流应该是一个比咖啡例子更好的例子。 - Charlie Salts
2
有人能解释一下为什么装饰器模式是这种情况的最佳解决方案吗? - Andrew Johnson

39

最近我在一个使用CommandProcessor接口的Web服务中使用了装饰器模式:

public Command receive(Request request);
public Response execute(Command command);
public void respond(Response response);
基本上,CommandProcessor接收一个请求,并创建适当的命令,执行该命令并创建适当的响应,并发送响应。 当我想要添加计时和记录日志时,我创建了一个TimerDecorator,它使用现有的CommandProcessor作为其组件。 TimerDecorator实现了CommandProcessor接口,但只是添加了计时,然后调用它的目标——真正的CommandProcessor。 就像这样:
public class TimerDecorator implements CommandProcessor {
   private CommandProcessor target;
   private Timer timer;

   public TimerDecorator(CommandProcessor processor) {
      this.target = processor;
      this.timer = new Timer();
   }

   public Command receive(Request request) {
      this.timer.start();
      return this.target.receive(request);
   }

   public Response execute(Command command) {
      return this.target.execute(command);
   }

   public void respond(Response response) {
      this.target.response(response);
      this.timer.stop();
      // log timer
   }

}

因此真正的CommandProcessor被封装在TimerDecorator内部,我可以像处理目标CommandProcessor一样处理TimerDecorator,但现在计时逻辑已经添加了。


19

装饰器模式可以在运行时动态地改变对象的功能。

组合和继承已经被有效地使用来实现这个目标。

一个真实的例子:计算饮料的价格,其中可能包含多种口味。

abstract class Beverage {
    protected String name;
    protected int price;
    public Beverage(){
        
    }
    public  Beverage(String name){
        this.name = name;
    }
    public void setName(String name){
        this.name = name;
    }
    public String getName(){
        return name;
    }
    protected void setPrice(int price){
        this.price = price;
    }
    protected int getPrice(){
        return price;
    }
    protected abstract void decorateBeverage();
    
}
class Tea extends Beverage{
    public Tea(String name){
        super(name);
        setPrice(10);
    }
    public void decorateBeverage(){
        System.out.println("Cost of:"+ name +":"+ price);
        // You can add some more functionality
    }
}
class Coffee extends Beverage{
    public Coffee(String name){
        super(name);
        setPrice(15);
    }
    public void decorateBeverage(){
        System.out.println("Cost of:"+ name +":"+ price);
        // You can add some more functionality
    }   
}
abstract class BeverageDecorator extends Beverage {
    protected Beverage beverage;
    public BeverageDecorator(Beverage beverage){    
        this.beverage = beverage;   
        setName(beverage.getName()+"+"+getDecoratedName());
        setPrice(beverage.getPrice()+getIncrementPrice());
    }
    public void decorateBeverage(){
        beverage.decorateBeverage();
        System.out.println("Cost of:"+getName()+":"+getPrice());
    }   
    public abstract int getIncrementPrice();
    public abstract String getDecoratedName();
}
class SugarDecorator extends BeverageDecorator{
    public SugarDecorator(Beverage beverage){
        super(beverage);
    }
    public void decorateBeverage(){
        super.decorateBeverage();
        decorateSugar();        
    }
    public void decorateSugar(){
        System.out.println("Added Sugar to:"+beverage.getName());
    }
    public int getIncrementPrice(){
        return 5;
    }
    public String getDecoratedName(){
        return "Sugar";
    }
}
class LemonDecorator extends BeverageDecorator{
    public LemonDecorator(Beverage beverage){
        super(beverage);
    }
    public void decorateBeverage(){
        super.decorateBeverage();
        decorateLemon();    
    }
    public void decorateLemon(){
        System.out.println("Added Lemon to:"+beverage.getName());       
    }
    public int getIncrementPrice(){
        return 3;
    }
    public String getDecoratedName(){
        return "Lemon";
    }
}

public class VendingMachineDecorator {  
    public static void main(String args[]){
        Beverage beverage = new SugarDecorator(new LemonDecorator(new Tea("Assam Tea")));
        beverage.decorateBeverage();
        beverage = new SugarDecorator(new LemonDecorator(new Coffee("Cappuccino")));
        beverage.decorateBeverage();
    }
}

输出:

Cost of:Assam Tea:10
Cost of:Assam Tea+Lemon:13
Added Lemon to:Assam Tea
Cost of:Assam Tea+Lemon+Sugar:18
Added Sugar to:Assam Tea+Lemon
Cost of:Cappuccino:15
Cost of:Cappuccino+Lemon:18
Added Lemon to:Cappuccino
Cost of:Cappuccino+Lemon+Sugar:23
Added Sugar to:Cappuccino+Lemon

这个例子是在自动售货机中添加多种口味后计算饮料成本。

在上面的例子中:

茶的成本=10,柠檬=3,糖=5。如果你加糖+柠檬+茶,它的成本是18。

咖啡的成本=15,柠檬=3,糖=5。如果你加糖+柠檬+咖啡,它的成本是23。

通过为两种饮料(茶和咖啡)使用相同的装饰器,减少了子类的数量。如果没有装饰器模式,您应该为不同的组合创建不同的子类。

组合将如下所示:

SugarLemonTea
SugarTea
LemonTea

SugarLemonCapaccuino
SugarCapaccuino
LemonCapaccuino

等等。

通过为两种饮料使用相同的装饰器,子类的数量已经减少。

相关SE问题:

使用GoF装饰器模式进行IO的用例和示例


8
装饰器简单而又极其强大。它是实现关注点分离的关键,也是开闭原则的必要工具。以下单商品为常见例子:
IOrderGateway
{
    void PlaceOrder(Order order);
{

主要实现: AmazonAffiliateOrderGateway

可能的装饰器包括:

  • IncrementPerformanceCounterOrderGateway (增加性能计数器订单网关)
  • QueueOrderForLaterOnTimeoutOrderGateway (将订单排队以便在超时后处理订单网关)
  • EmailOnExceptionOrderGateway (在异常情况下发送电子邮件订单网关)
  • InterceptTestOrderAndLogOrderGateway (拦截测试订单并记录订单网关)

此处更详细的例子说明了一个装饰器,用于在完成订单时保存使用礼品卡创建的订单的联系人:

class OrderWithGiftCardGateway extends OrderGatewayDecorator
{
    ...

    public function createOrder(CreateOrderRequest $order)
    {
        if ($this->containsGiftCard($order))
        {
            $this->addContactToFolder($order);
        }

        return parent::createOrder($order);
    }
}

4
  1. 动态、透明地为个体对象添加职责。
  2. 对于可以撤销的职责。
  3. 当通过子类扩展不可行时。有时可能会出现大量独立的扩展,支持每种组合会导致子类的爆炸。

1

GOF定义:

动态地将附加职责添加到对象。装饰器为扩展功能提供了一种灵活的替代子类化的方式。

该模式表明类必须对修改关闭,但对扩展开放,可以添加新功能而不会干扰现有功能。当我们想要为特定对象添加特殊功能而不是整个类时,这个概念非常有用。在此模式中,我们使用对象组合的概念而不是继承。

通用示例:

public abstract class Decorator<T> {
    private T t;

    public void setTheKernel(Supplier<? extends T> supplier) {
        this.t = supplier.get();
    }

    public T decorate() {
        return Objects.isNull(t) ? null : this.t;
    }

}

实现方案
public interface Repository {
    void save();
}

public class RepositoryImpl implements Repository{
    @Override
    public void save() {
        System.out.println("saved successfully");
    }
}

public class EnhancedRepository<T> extends Decorator<T> {
    public void enhancedSave() {
        System.out.println("enhanced save activated");
    }
}

public class Main {
    public static void main(String[] args) {
        EnhancedRepository<Repository> enhanced = new EnhancedRepository<>();
        enhanced.setTheKernel(RepositoryImpl::new);
        enhanced.enhancedSave();
        enhanced.decorate().save();
    }
}

0

装饰器模式被 C# 语言本身使用。它用于装饰 C# 的 Stream I/O 类。 装饰版本包括 BufferedStream、FileStrem、MemoryStrem、NetworkStream 和 CryptoStream 等类。

这些子类继承自 Stream 类,并且还包含一个 Stream 类的实例。

这里阅读更多


0

当您想要为类/对象添加多个功能,并且希望随时具有添加它们的灵活性时,装饰者模式非常方便。您可以扩展基类并添加所需的更改,但是这种方式会产生许多可能会让您头脑爆炸的子类。但是,通过使用装饰者模式,您可以拥有所需的更改,同时仍然具有简单、易懂的流程。 您的设计很容易进行扩展,但对于修改则非常简单。 最好的例子可能是 Java 和 C# 中实现的 Stream 对象。 例如,您有一个 File Stream,在一个用例中,您想要对其进行加密,然后压缩,然后记录日志,最后做一些花哨的操作。然后在另一个类中,您决定执行其他操作:您想要将其转换,然后加密,然后获取时间等。如果使用继承,则必须创建至少 3 个子类,如果需要任何其他要求,则必须添加更多子类,在这种情况下(Stream),您将获得数十个子类以进行小的更改。

class EncryptZipLogStream{}
class ConvertEncryptTimeStream{}
class MoreStreamProcess{}
class OtherMoreStreamProcess{}
...

在每个使用案例中,您必须记住需要哪个类并尝试使用它。但是想象一下,如果您使用组合而不是继承,并且为每个流程都有装饰器类,您可以轻松地组合所需的包装器,并以最少的努力和最大的简单性进行任何所需的处理。
class WhereIWannaUseUseCaseOne {
    EncryptStream(ZipStream(LogStream(FileStream("file name)))));
    // rest of the code to use the combines streams.
}

然后你想出了另一个使用案例:

class WhereIWannaUseUseCaseSecond {
    ConvertStream(TimeStream(LogStream(FileStream("file name)))));
    // rest of the code to use the combines streams.
}

等等,您可以在运行时使用简单的流程和易于理解的逻辑灵活地进行任何操作。


-1

一个对我来说非常真实的例子:

我不得不更新在项目中广泛使用的类,但是那个类在一个库中,源代码丢失在虚无世界中。

我可以将整个库反编译以创建另一个版本,或者使用装饰器设计模式,我选择了后者。这使我能够添加缺少的功能并简单地更新配置。

当负责任地使用时,这是一种非常有用的模式。

这个特殊案例启发了我制作 this video,在视频中我解释了这个模式。


我的回答可能不够清晰,让我重新表述一下。 如果你的一个名为A的类依赖于另一个名为B的类,而你无法自行更新B或请求更新版本,则装饰器模式可以提供帮助。 装饰器模式确保你向A提供了B的实例,同时修改了B的原始行为,以确保兼容性。 被装饰的类C作为B的包装器。在调用C的方法时,你可以在委托给B之前操作输入,并在返回之前操作B的输出,就像一个方面一样。 - Mariano LEANCE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接