设计模式学习笔记(十二):装饰模式
1 概述
1.1 引言
装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为。比如,一张照片,不改变照片本身,增加一个相框。
装饰模式是一种用于替代继承的技术,无须定义子类即可给对象动态增加职责,使用对象之间的关联关系来代替继承关系,在装饰模式中引入了装饰类,在装饰类中既可以调用待装饰的原有类方法,还可以增加新的方法,以扩充原有的类功能。
1.2 定义
装饰模式:动态地给对象增加一些额外的职责。
就增加对象功能来说,装饰模式比生成子类实现更为灵活,装饰模式是一种对象结构型模式。
1.3 结构图
1.4 角色
- Component(抽象构件类):是具体构件以及抽象装饰类的父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰之后的对象,以实现客户端的透明操作
- ConcreteComponent(具体构件类):是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责
- Decorator(抽象装饰类):用于给具体构件类增加职责,但是具体职责在子类实现。抽象装饰类维护一个指向抽象构件的引用,通过该引用可以调用装饰之前构件对象的方法,并通过子类扩展该方法以达到装饰的目的
- ConcreteDecorator(具体装饰类):负责向构件中添加新的职责,每一个具体装饰类都定义了一些新的行为,可以调用抽象装饰类中定义的方法,并可以增加新的职责用以扩充对象的行为
2 典型实现
2.1 步骤
- 定义抽象构件类:可以是抽象类或者接口,声明业务方法
- 定义具体构件类:继承或实现抽象构件,实现具体业务方法
- 定义抽象装饰类:继承或实现抽象构件,增加一个抽象构件私有成员,通过该成员可以调用装饰之前具体构件的方法
- 定义具体装饰类:继承抽象装饰类,并且增加装饰行为,在装饰之前调用具体构件方法,接着调用装饰方法
2.2 抽象构件类
简化只有一个业务方法:
abstract class Component
{
abstract void operation();
}
2.3 具体构件类
继承抽象构件:
class ConcreteComponent extends Component
{
public void operation()
{
System.out.println("具体构件方法");
}
}
2.4 抽象装饰类
class Decorator extends Component
{
private Component component;
public Decorator(Component component)
{
this.component = component;
}
public void operation()
{
component.operation();
}
}
抽象装饰类需要包含一个抽象构件的私有成员,以便可以通过setter或构造方法注入不同的具体构件,同时在业务方法中方便调用具体构件未装饰之前的方法。
2.5 具体装饰类
class ConcreteDecorator extends Decorator
{
public ConcreteDecorator(Component component)
{
super(component);
}
public void operation()
{
super.operation();
newBehavior();
}
public void newBehavior()
{
System.out.println("装饰方法");
}
}
继承抽象装饰类,在业务方法中首先调用父类(抽象装饰类)的方法再调用新的装饰方法。
2.6 客户端
public static void main(String[] args)
{
Component component = new ConcreteComponent();
Component decorator = new ConcreteDecorator(component);
decorator.operation();
}
客户端针对抽象构件编程即可,无需指定具体装饰类或者具体构件类的类型,使用装饰器时,通过构造方法注入具体构件,直接调用业务方法即可。
3 实例
设计一个图形界面构件库,具体构件有窗体,文本框以及列表框,装饰方法包括添加滚动条与添加黑边框,使用装饰模式对系统进行设计。
设计如下:
- 抽象构件类:
Component
- 具体构件类:
Window
+TextBox
+ListBox
- 抽象装饰类:
Decorator
- 具体装饰类:
ScrollBarDecorator
+BlackBorderDecorator
代码如下:
public class Test
{
public static void main(String[] args) {
Component component = new Window();
Component decorator = new ScrollBarDecorator(component);
decorator.display();
}
}
abstract class Component
{
abstract void display();
}
class Window extends Component
{
public void display()
{
System.out.println("显示窗口");
}
}
class TextBox extends Component
{
public void display()
{
System.out.println("显示文本框");
}
}
class ListBox extends Component
{
public void display()
{
System.out.println("显示列表框");
}
}
class Decorator extends Component
{
private Component component;
public Decorator(Component component)
{
this.component = component;
}
public void display()
{
component.display();
}
}
class ScrollBarDecorator extends Decorator
{
public ScrollBarDecorator(Component component)
{
super(component);
}
public void display()
{
addScrollBar();
super.display();
}
public void addScrollBar()
{
System.out.println("添加滚动条");
}
}
class BlackBorderDecorator extends Decorator
{
public BlackBorderDecorator(Component component)
{
super(component);
}
public void display()
{
addBlackBorder();
super.display();
}
public void addBlackBorder()
{
System.out.println("添加黑边框");
}
}
输出如下:
核心部分就是客户端的代码:
Component component = new Window();
Component decorator = new ScrollBarDecorator(component);
decorator.display();
创建具体构件后,再创建具体装饰器,把具体构件传入具体装饰器的构造方法中,这样具体装饰器就能在装饰之后(在添加滚动条之后)调用具体构件的方法(调用显示窗口)。
另外,如果向增加新的装饰方法,比如增加了滚动条后,再增加黑边框,只需要将”滚动条装饰器“本身再装饰一次:
Component component = new Window();
Component decorator = new ScrollBarDecorator(component);
decorator = new BlackBorderDecorator(decorator);
decorator.display();
也就是把已经对具体构件进行装饰之后的具体装饰器,注入到另一个具体装饰器的构造方法再一次装饰。
4 透明装饰与半透明装饰
4.1 透明装饰模式
标准的装饰模式就是透明装饰,比如上述例子。在透明装饰模式中,要求客户端完全针对抽象构件编程,也就是将对象全部声明为抽象构件类型,而不是具体构件类型或具体装饰器类型。
透明装饰模式的优点如下:
- 客户端透明地使用装饰前以及装饰后的对象,无须关心两者区别
- 能对已装饰过的对象进行多次装饰
在实现透明装饰模式时,要求具体装饰类的业务方法覆盖抽象装饰类的业务方法,需要调用原有具体构件对象的业务方法以及新增装饰方法。
4.2 半透明装饰模式
对于有时用户需要单独调用装饰方法,这时候需要使用具体装饰类型定义装饰后的对象,而具体构件对象还是可以使用抽象构件定义,这种装饰模式就叫半透明装饰模式。对于客户端来说:
- 具体构件类型无需关心,是透明的
- 具体装饰类型必须指定,是不透明的
例子如下,修改上面的滚动条具体装饰类:
class ScrollBarDecorator extends Decorator
{
public ScrollBarDecorator(Component component)
{
super(component);
}
public void display()
{
super.display();
}
public void addScrollBar()
{
System.out.println("添加滚动条");
}
}
其中addScrollBar
由客户端单独调用:
Component component = new Window();
ScrollBarDecorator decorator = new ScrollBarDecorator(component);
decorator.display();
decorator.addScrollBar();
半透明装饰可以带来更大的灵活性,使用起来更加方便,客户端可以单独调用装饰方法来进行装饰,但是缺点就是不能对同一个对象进行多次装饰。
5 注意事项
- 保持接口相同:尽量保持装饰类的接口与被装饰类的接口相同,这样对客户端而言装饰前/后的对象可以一致对待,也就是尽量使用透明装饰模式
- 减少具体构件行为:过多的行为不需要放在具体构件类中,通过具体装饰类进行扩展
- 去除抽象构件:如果只有一个具体构件类,那么抽象装饰类可以作为该具体构件类的直接子类,也就是说将原来的抽象构件用具体构件代替
6 主要优点
- 动态扩展灵活:对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加。通过选择不同的具体装饰类,可以动态扩展对象的行为
- 多次装饰:可以对一个对象进行多次装饰,使用不同的具体装饰类以及这些装饰类的排列组合,可以创造很多不同行为的组合
- 构件与装饰类独立变化:具体构件类以及具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类或者具体装饰类,无须修改原有代码,符合开闭原则
7 主要缺点
- 对象较多:使用装饰模式会产生很多小对象,这些对象的区别在于相互连接方式的不同,小对象过多会一定程度上影响性能
- 排查繁琐:尽管装饰模式比继承更加灵活,但也意味着比继承更加容易出错,排错也很困难,对于多次装饰后的对象可能需要逐级排查
8 适用场景
- 在不影响其他对象的情况下,以动态和透明的方式给单个对象增加职责
- 在不能采用继承扩展系统或者采用继承不利于对系统扩展和维护时可以使用装饰模式