装饰模式属于结构型模式。它动态地给一个对象添加额外的职责,就增加功能来说,装饰模式比生成子类更加灵活。
装饰模式UML类图.png
Component(抽象组件):接口或者抽象类,被装饰的最原始的对象。 ConcreteComponent(具体组件):实现抽象组件的接口。 Decorator(抽象装饰角色):一般是抽象类,持有一个被装饰者的引用,用来调用被装饰者的方法,同时可以给被装饰者增加新的职责。 ConcreteDecorator(具体装饰类):抽象装饰角色的具体实现。
用法:
• 当不适合采用继承的方式对系统进行新增功能时。
• 需要透明且动态地扩展类的功能时。
按照以上两点,个人理解翻译一下:
• 不适合用继承扩展的情况有哪些呢?假设有Parent、Child、GrandChild三个类,我需要扩展其中的Child类很明显用继承不合适,因为可能会影响到GrandChild;又或者用继承的方法会令到子类的数量暴增并且可能存在大量重复代码,造成代码臃肿。
• 所谓动态,就是给一个对象添加一些额外的职责,同时也可以动态取消,实现功能的动态组合;所谓透明,要给一个对象增加功能,但是不能让这个对象知道,也就是不能去改动这个对象。
例子:
装饰模式应用广泛,例如Java I/O标准库的设计就是Java语言中的最著名的例子。而这次我们参考《大话设计模式》书中,穿衣服的经典案例。
众所周知,人会根据不同的场合穿着不同的衣服。就以男人为例,上班时会着装正式(领带、衬衫、外套、西裤、皮鞋、手表等),睡觉时会穿舒适(睡衣、睡裤等),运动时会穿运动服(T恤衫、运动裤、运动鞋等)。
需求:输出男人在以上三种场合的着装要求
1、原始代码
新手写代码很容易写成把所有的衣服都放在一个类里面,我们先看一下没有用任何涉及模式的例子:
1.1、新建一个男人类
public class Man {
public String name;
public Man(String name) {
this.name = name;
}
public void dress() {
System.out.println("装扮的" + name);
}
public void dressTie() {
System.out.println("系一条领带");
}
public void dressTShirt() {
System.out.println("穿一件TShirt");
}
public void dressSuit() {
System.out.println("穿一件西装");
}
public void dressSportShoes(){
System.out.println("穿运动鞋");
}
//省略一堆代码
}
1.2、调用这个男人类,根据不同场合任意搭配
public static void main(String[] args) {
//场合一:
Man xiaoMing = new Man("小明");
xiaoMing.dressTShirt();
xiaoMing.dressTie();
xiaoMing.dressSuit();
//场合二:省略一堆代码
}
存在的问题
分析一下,实现这个需求看起来没什么问题。根据场合任意搭配可以实现这个需求,但是问题来了:需要为新场合新增衣服时,改动了原来的类,违背了开闭原则。要记住,在实际开发中,需求永远都是在变的。
2、继承的方案
这个方案就不用代码例子,用类图说明:
存在的问题
这种方案,在新增场景的情况又不需要改动原来的类,随心所欲增加任何的功能。看起来没什么问题。但是,三种场景还能接受,但如果需求是三十种场景,上百件不同的衣物呢?恐怕只能根据场合继承一堆“XX场景的男人”类了。用继承的方法会令到子类的数量暴增并且可能存在大量重复代码,造成代码臃肿,最终就是难以维护。
3、利用装饰模式
重点终于来了,装饰模式可以说是继承的一种替代方案,因为通过组合的方式也能扩展功能。在这个例子中,可以理解成“钱财乃身外之物”,所有的衣服都是人的一种装饰。
3.1、目前只考虑男人的情况下,将Component与ConcreteComponent二合一
public class Man {
public String name;
public Man(){
}
public Man(String name) {
this.name = name;
}
public void dress() {
System.out.println("装扮的" + name);
}
}
很简单,就一个dress方法。Man类就是一个需要被装饰的对象,装饰器可以给它增加额外的职责。
3.2、创建一个装饰者类,命名为Finery(服饰)。
public class Finery extends Man{
protected Man man;
public void decorate(Man man){
this.man = man;
}
public void dress(){
if (man != null){
man.dress();
}
}
}
3.3、创建一堆具体装饰者类,它们继承自Finery
public class TShirt extends Finery {
@Override
public void dress() {
System.out.println("T恤衫");
super.dress();
}
}
public class SportShoes extends Finery {
@Override
public void dress() {
System.out.println("运动鞋");
super.dress();
}
}
3.4、实现需求
public static void main(String[] args) {
Man xiaoMing = new Man("小明");
//场合一:运动
Finery sportShoes = new SportShoes();
sportShoes.decorate(xiaoMing); //穿了运动鞋的小明
Finery tShirt = new TShirt();
tShirt.decorate(sportShoes); //穿了运动鞋的小明再穿一件T恤衫
tShirt.dress(); //装扮好了
//场合二:睡觉
//省略一堆代码....
}
运行效果
T恤衫
运动鞋
装扮的小明
用装饰模式修改代码的方案就基本完成了,新增场合通过组合的方式就完事了。但使用此模式的同时会产生很多小对象(各种Finery子类),大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。这正正是这个模式的缺点,不可避免。
4、透明装饰模式与半透明装饰模式
在实际使用过程中,由于新增行为可能需要单独调用,因此这种形式的装饰模式也经常出现,这种装饰模式被称为半透明(Semi-transparent)装饰模式,而标准的装饰模式是透明(Transparent)装饰模式。
4.1、下面我要为具体装饰类新增一个增加颜色的方法:
public class TShirt extends Finery {
@Override
public void dress() {
System.out.println("T恤衫");
super.dress();
}
public void changeColor(String color){
System.out.print(color);
}
}
4.2、代码区别
public static void main(String[] args) {
Man xiaoMing = new Man("小明");
//透明装饰模式
Finery tShirt = new TShirt();
tShirt.decorate(xiaoMing); //穿了一件T恤衫
tShirt.dress(); //装扮好了
System.out.println("===========");
//半透明装饰模式
TShirt tShirt2 = new TShirt();
tShirt2.decorate(xiaoMing); //穿了一件T恤衫
tShirt2.changeColor("红色"); //染上红色
tShirt2.dress(); //装扮好了
}
T恤衫
装扮的小明
===========
红色T恤衫
装扮的小明
很直观,在透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序全部声明为抽象类型。在上述例子中,被修饰的对象(Man类)和装饰的对象(T-Shirt对象用Finery类声明)都要是抽象类型。
而在半透明模式,为了能够调用到新增方法,用具体装饰类型来定义装饰之后的对象,而具体构件类型还是可以使用抽象构件类型来定义,这种装饰模式即为半透明装饰模式。被修饰的对象(Man类)用抽象类型Man,具体装饰用具体装饰声明(T-Shirt对象用T-Shirt类声明)。