关于装饰器模式的非适用情况


前言

装饰器模式是java程序设计中较为重要也较为难理解的一种模式。本文就装饰器模式提出一点小的思考:可能在下面提到这种情况下,虽然看起来在这种情况下应该使用装饰器模式,但是如果我们直接使用装饰器模式又会引发一些问题。

总结起来,使用装饰器模式需要满足的设计标准是,装饰器类对外只提供接口中定义的public方法,装饰器的特性需要通过私有方法实现,然后在公有方法中调用。如果希望装饰器实现除了在接口中定义的共有方法之外的方法,最好不使用装饰器模式。强行使用装饰器模式会让装饰器失效。


一、问题描述

我们现在设计了一个Stack类型的接口,用于实现栈的数据结构的功能.

public interface Stack<E>{
	public boolean push(E item);
	public E pop();
}

然后我们给出了Stack的一个具体实现ArrayStack

public Class ArrayStack<E> implements Stack<E>{
	...
	@Override
	public boolean push(E item) {
		System.out.println("In ArrayStack push()");
		return true;
	}

	@Override
	public E pop() {
		System.out.println("In ArrayStack pop()");
		return null;
	}
}

现在的问题是,我们想要拥有特殊功能的Stack类型,比如 UndoStack(撤回功能)、LogStack(日志功能)等等。

我们很自然地想到使用装饰器模式。因为公有的Stack接口实现了最基础的功能,而类似于撤回,日志这样的功能就像一些挂件一样,很容易装饰到Stack上面。而且使用装饰器模式后,想要构造具有日志功能和撤回功能的Stack类型也看起来会变得简单。

但是我们接下来会解释,在这种情况下,直接使用装饰器似乎不是一个很好的选择。

二、基于装饰器的设计

2.1 简单地展示实现的结构

首先我们需要实现一个装饰器的抽象类,它继承了Stack接口

public abstract class StackDecorator<E> implements Stack<E> {
	private Stack<E> stack;
	
	public StackDecorator(Stack<E> s) {
		this.stack = s;
	}
	
	public Boolean push(E item) {
		return stack.push(item);
	}
	
	public E pop() {
		return stack.pop();
	}
}

到这里为止都是我们熟悉的装饰器的设计结构。但是在具体实现UndoStack和LogStack的时候我们发现,实现它们功能的时候需要除了push和pop以外的其他公有方法。

在实现LogStack的时候,我们让它继承StackDecrator,但是我们注意到,我们必须提供一个公有方法getLog()让使用者获取日志的内容。

public class LogStack<E> extends StackDecorator<E> {

	private String log;
	
	public LogStack(Stack<E> s) {
		super(s);
		this.log = "";
	}
	
	public Boolean push(E item) {
		log += "Push an item.\n";
		return super.push(item);
	}
	
	public E pop() {
		log += "Pop an item.\n";
		return super.pop();
	}
	
	public String getLog() {
		return log;
	}
}

同理,实现UndoStack的时候,我们必须提供一个公有undo(),让使用者撤回上一次操作。

public class UndoStack<E> extends StackDecorator<E> {
	
	E lastItem = null;
	String lastOp = "";

	public UndoStack(Stack<E> s) {
		super(s);
	}
	
	public Boolean push(E item) {
		lastOp = "push";
		return super.push(item);
	}
	
	public E pop() {
		lastOp = "pop";
		lastItem = super.pop();
		return lastItem;
	}
	
	public Boolean undo() {
		switch (lastOp) {
		case "pop":
			super.push(lastItem);
			return true;
		case "push":
			super.pop();
			return true;
		default:
			return false;
		}
	}

}

2.2 隐含的问题

注意到,这两个方法都不在Stack接口中提供,但是它们又是实现相应功能所必须的public方法,这样的实现是有很大问题的。当我们真正使用上面的装饰器构造一个Undo Log Stack的时候,我们会发现效果不尽人意。

public class StackClient {
	public static void main(String[] argvs) {
		
		Stack<String> stack = new LogStack<String>(new UndoStack<>(new ArrayStack<>()));
		stack.push("First");
		System.out.print(((LogStack<String>) stack).getLog());		// this Op is OK
		System.out.println(((UndoStack<String>) stack).undo());		// this Op raises exception
		
	}
}

如上所示,我们构造了一个看似同时具有log功能和undo功能的Stack。但是实际上,它只具备log的功能,虽然这个装饰类内部的逻辑实现会记录上一次操作的类型和操作数,但是我们无法通过调用undo方法撤回上一步操作。在层层包装的过程中,我们实际上已经隐藏了不在接口Stack中定义的那些public方法。

正如这个例子提到的,我们使用接口类型声明变量stack,在静态检查中,它的public方法只有push和pop(除了Object中定义的那些方法外),没有undo和getLog,如果我们想使用getLog方法,就必须通过类型强转,正如上面展示的那样。但是如果我们想使用undo方法,那么很不幸,在目前这种结构下并不行,如果你想通过类型强转实现的话,会在运行时产生异常。

因为我们最终看到的stack实际上是一个LogStack类型,它没有实现undo方法,虽然它的内部变量stack(定义在抽象类StackDecrator中)是一个UndoStack类型,但是这个变量并不对外部展示。

这就回到我在前言里提到的那句话,或许我们在设计装饰器时必须考虑到不应该在装饰器中添加接口中声明的public方法之外的共有方法,除非这个公有方法不影响装饰器的功能。不然,如果某一天,这个装饰器被其他装饰器包装的时候,这个额外定义的public方法就失效了。

2.3 不要解决它,使用其他模式

请不要试图解决这个问题,因为你的解决方案很有可能是拆东墙补西墙。这里列举几种危险的“解决方案”:

  • 在使用特殊定义了有关功能实现的public方法后,规定该装饰器不能被其他装饰器包装。这种行为是有悖于装饰器模式的初衷的,也是违反程序员直觉的,请你不要做这样容易引人犯错的约定。
  • 在Stack接口中添加不必要的方法,比如为Stack添加方法undo和getLog。记住,最顶层的接口中只能包含所有实现公有的功能,不应该拥有多余部分。你可能想的是,对于实现undo和getLog的Stack实现,在这两个方法中不做任何事情。但是这样会给使用你创造的类的使用者带来巨大的疑惑——为什么你提供了这两个方法,但是它们什么都做不了???
  • 在UndoStack中添加getLog方法调用其包裹stack的getLog方法;在LogStack中添加undo方法,调用其包裹的stack类的undo方法。这种设计和上一种没有本质区别,而且这两种设计的共同坏处在于,一旦我们想要设计一个新的Stack类型,而且这个stack类型也实现了一些特殊功能(比如ReverseStack实现reverse功能),那么,你必须在所有之前实现的代码中添加有关reverse的描述。

总结

总结起来,使用装饰器模式需要满足的设计标准是,装饰器类对外只提供接口中定义的public方法,装饰器的特性需要通过私有方法实现,然后在公有方法中调用。如果希望装饰器实现除了在接口中定义的共有方法之外的方法,最好不使用装饰器模式。强行使用装饰器模式会让装饰器失效。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值