关于Decorator装饰器模式嵌套包装的个人的改进设想
本文系笔者在学习软件构造课程期间所写,不保证通用性和正确性,仅供参考。
基于课程要求,本文所涉及语言为Java。
目录
一、Decorator介绍
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,作为现有的类的一个包装,装饰器模式通过将对象包装在装饰器类中,动态地修改其行为。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
可以认为主要是用到了委托的思想,委托另外一个类完成接口要求的功能,而自己做的是在另外一个类工作的基础上进行行为修改。
下文中我们将以一个普通的列表为例进行改进说明。这个列表非常简单,只有添加一个int的功能。举例中没有具体实现,都用打印信息来代替。
首先是基本的接口:
public interface List {
void add(int i);
}
然后,有一个实现这个接口的最基本的类:
public class BaseList implements List{
@Override
public void add(int i){
System.out.println("BL: add " + i);
}
}
接下来定义一个ListDecorator抽象类,它通过委托另外一个List来实现List接口。
public abstract class ListDecorator implements List{
protected List list;
public ListDecorator(List list){
this.list = list;
}
@Override
public void add(int i) {
list.add(i);
}
}
二、内部调用法
现在我们实现两个Decorator,一个是UndoList,它可以记录添加历史,并可以调用undo()方法撤销上一次add()操作。
class UndoList extends ListDecorator{
public UndoList(List list) {
super(list);
}
@Override
public void add(int i) {
list.add(i);
System.out.println("UL: record this add");
}
public void undo() {
System.out.println("UL: last add has been removed.");
}
}
还有一个是ShoutList,它并不能干什么,只会大叫,你可以调用SHOUT()方法来让它大叫一下。
class ShoutList extends ListDecorator{
public ShoutList(List list) {
super(list);
}
@Override
public void add(int i) {
list.add(i);
System.out.println("SL: HEY GUYS I JUST ADD " + i + " TO MY LIST!!!");
}
public void SHOUT(){
System.out.println("SL: SHOOOOOOOUT!!!!!");
}
}
现在如果我们希望构建一个既可以撤销又可以大叫的列表,那么可以这么写:
UndoList myList = new UndoList(new ShoutList(new BaseList()));
但是这样有个问题:因为ShoutList被嵌套在了里面,我就没法让这个列表大叫了,这不好。
当然,可以先在其他地方把ShoutList构造出来,再嵌套到myList里面,想大叫的时候调用那个ShoutList即可,但这样终究不太优雅,怎样直接调用myList就可以大叫呢?
注意到ListDecorator中,委托的list是protected的,所以在内部我们可以沿着list一直深入,直到底层。这样,当向内部找到ShoutList的时候,让它大叫不就可以了吗?用这个朴素的思想,我们为ListDecorator添加两个方法:
private List inner(){
return list;
}
public void call(MethodType method){
List toCall = this;
switch(method){
case Undo:
while(toCall instanceof ListDecorator){
if(!(toCall instanceof UndoList)){
toCall = ((ListDecorator) toCall).inner();
}
else{
((UndoList) toCall).undo();
break;
}
}
break;
case SHOUT:
while(toCall instanceof ListDecorator){
if(!(toCall instanceof ShoutList)){
toCall = ((ListDecorator) toCall).inner();
}
else{
((ShoutList) toCall).SHOUT();
break;
}
}
break;
}
}
解释一下:
inner()返回这个decorator委托的内部list,由于我们不希望用户直接调用这个方法,因此将其设为private。
call()调用一个方法,以MethodType方法类型作为参数。这是一个枚举类型,可以定义在接口里或者其他什么地方,当然用字符串也可以,只是作为方法的标识而已。然后从本身开始,逐步往内检查有没有想要的类型,如果有,就调用它对应的方法。如果没有想要的类型,可以抛异常可以返回false,这里就不列了。当有新方法时,只需要增加一个枚举和一个switch内的case即可。
三、列表构造法
现在我们再来看一下构造这个列表的语句:
UndoList myList = new UndoList(new ShoutList(new BaseList()));
感觉也有些不太美观。如果这是一个给用户的API,要是用户可以直接在构造的时候把想要的类型作为参数传给构造方法,那看起来就直观多了。我们还是可以用类似上面的思想来实现。定义一个新类ConcreteList:
public class ConcreteList implements List{
private List list;
public ConcreteList(){
list = new BaseList();
}
public ConcreteList(DecoratorType... decoratorTypes){
list = new BaseList();
for(DecoratorType type : decoratorTypes){
switch (type){
case ShoutList:
list = new ShoutList(list);
break;
case UndoList:
list = new UndoList(list);
break;
}
}
}
@Override
public void add(int i) {
list.add(i);
}
public void call(MethodType method){
if(list instanceof ListDecorator){
((ListDecorator) list).call(method);
}
}
}
这里用了"..."这一语法糖,构造时可以传入任意数量的DecoratorType。这也是一个枚举类型,标识各个decorator。然后遍历所有参数,每次都包装上一个decorator即可。
现在再来构造一个又能撤销又能大叫的列表,看起来漂亮很多:
ConcreteList myList = new ConcreteList(DecoratorType.ShoutList, DecoratorType.UndoList);
myList.call(MethodType.SHOUT);
myList.call(MethodType.Undo);
四、与Strategy并行使用
Decorator模式还有一个问题是新包裹的类只能在内部委托类完成其工作的前后进行额外的操作,而不能在其工作中间插入操作。从思想上来说这其实并不是一个问题,因为decorator的思想就是基于内部已经封装好的前提下进行装饰,并不考虑内部实现。但如果真的想在内部实现的中间插入一段操作,则可以利用strategy模式进行处理。
我们希望实现一个SmellyList,它让列表中实际存入的数据是输入数据再加上114514。就这个例子而言它是可以单纯用decorator实现的,但是我们试用strategy来解决。
首先定义strategy类的接口AddStrategy:
public interface AddStrategy {
int value(int i);
}
还有一个最基本的strategy,它将传入的数据原封不动地返回:
public class BaseAdd implements AddStrategy{
@Override
public int value(int i) {
return i;
}
}
将BaseList的add过程添加上strategy的介入:
public class BaseList implements List{
private AddStrategy addStrategy;
public BaseList(){
addStrategy = new BaseAdd();
}
public void setStrategy(AddStrategy addStrategy){
this.addStrategy = addStrategy;
}
@Override
public void add(int i){
i = addStrategy.value(i);
System.out.println("BL: add " + i);
}
}
我们很容易定义一个SmellyAdd:
public class SmellyAdd implements AddStrategy{
@Override
public int value(int i) {
return i + 114514;
}
}
这样,构造一个BaseList,再setStrategy(new SmellyAdd()),就可以达到我们想要的效果。综合第二节和第三节的内容,可以比较容易地把strategy也封装到ConcreteList中,对用户而言这两者就不存在什么区别了。另外,strategy本身也可以嵌套使用decorator模式,使单一的一个strategy可以装饰上丰富的功能。这里strategy没有放在方法的参数里,而是存在List中,主要是为了实现接口,要放在参数中也是完全可行的,只需要进行一点设计上的修改即可。
但是这样也有问题:例如在本例中,虽然存入的数据改变了,但是UndoList和ShoutList获得的数据并没有改变,这就容易导致程序出现预料外的bug,不便于规范地进行代码实现。因此,这一节的内容实际上可能并没有太大的实用意义,只是作为一个头脑风暴吧。
五、结语
某种意义上这种形式的封装不太符合OCP原则,但是想要调用某个类特有的功能,那就只能判断该类,似乎并无他法。
而这就是我在第一次接触decorator后,自己瞎想得出来的方案。这种做法是早就有了并且很通用,还是有更好的解决方法,还是非常简单完全不值一提,亦或并不优雅不建议使用,我一概不知。所以也不敢说多有用吧,看个乐就好~