转载

Java设计模式-装饰者模式

装饰者模式:在不改变原类文件以及不使用继承的情况下,动态地将责任附加到对象上,从而实现动态拓展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

原则

要使用装饰者模式,需要满足以下设计原则:

  1. 多用组合,少用继承
  2. 开放-关闭原则:类应该对拓展开放,对修改关闭

UML类图

我们先来看看装饰者模式的类图,再来详细讲述:

Java设计模式-装饰者模式

由上自下:

  1. Component 是基类。通常是一个抽象类或者一个接口,定义了属性或者方法,方法的实现可以由子类实现或者自己实现。通常不会直接使用该类,而是通过继承该类来实现特定的功能,它约束了整个继承树的行为。比如说,如果 Component 代表人,即使通过装饰也不会使人变成别的动物。
  2. ConcreteComponenComponent 的子类,实现了相应的方法,它充当了“被装饰者”的角色。
  3. Decorator 也是 Component 的子类,它是装饰者共同实现的抽象类(也可以是接口)。比如说, Decorator 代表衣服这一类装饰者,那么它的子类应该是T恤、裙子这样的具体的装饰者。
  4. ConcreteDecoratorDecorator 的子类,是具体的装饰者,由于它同时也是 Component 的子类,因此它能方便地拓展 Component 的状态(比如添加新的方法)。每个装饰者都应该有一个实例变量用以保存某个 Component 的引用,这也是利用了组合的特性。在持有Component的引用后,由于其自身也是 Component 的子类,那么,相当于 ConcreteDecorator 包裹了 Component ,不但有 Component 的特性,同时自身也可以有别的特性,也就是所谓的装饰。

实例演示

为了更加深刻地理解装饰者模式,我们来看一个简单的栗子。首先,我们假设现在有这样一个需求:你有一家服装店,卖各式各样的衣服,现在需要用一个系统来记录客户所要购买的衣服的总价,以便方便地结算。那么在这个例子里面,我们可以用装饰者模式,把客户当做被装饰者,衣服是装饰者,这很直观形象吧,接着我们来一步步实现需求。

创建Component基类

因为总体对象是人,所以我们可以把人抽象为基类,新建Person.java:

public abstract class Person {
    String description = "Unkonwn";
    
    public String getDescription()
    {
        return description;
    }
    
    public abstract double cost(); //子类应该实现的方法
}
复制代码

创建被装饰者——ConcreteComponent

客户分为很多种,有儿童、青少年、成年人等,因此我们可以创建不同的被装饰者,这里我们创建青少年的被装饰者,新建Teenager.java:

public class Teenager extends Person {
    
    public Teenager() {
        this.description = "Shopping List:";
    }
    
    @Override
    public double cost() {
        //什么都没买,不用钱
        return 0;
    }

}
复制代码

创建Decorator

由于不同的部位有不同的衣物,不能混为一谈,比如说,衣服、帽子、鞋子等,那么这里我们创建的Decorator为衣服和帽子,分别新建 ClothingDecorator.javaHatDecorator.java :

public abstract class ClothingDecorator extends Person {
    
    public abstract String getDescription();
}
复制代码
public abstract class HatDecorator extends Person {

    public abstract String getDescription();

}
复制代码

创建ConcreteDecorator

上面既然已经创建了两种 Decorator ,那么我们基于它们进行拓展,创建出不同的装饰者,对于 Clothing ,我们新建 Shirt.java ,对于Hat,我们新建 Casquette ,其实可以根据不同类型的衣物创建更多不同的装饰者,这里只是作为演示而创建了两种。代码如下所示:

public class Shirt extends ClothingDecorator {
    
    //用实例变量保存Person的引用
    Person person;
    
    public Shirt(Person person)
    {
        this.person = person;
    }
    
    @Override
    public String getDescription() {
        return person.getDescription() + "a shirt  ";
    }

    @Override
    public double cost() {
        return 100 + person.cost(); //实现了cost()方法,并调用了person的cost()方法,目的是获得所有累加值
    }

}
复制代码
public class Casquette extends HatDecorator {
    
    Person person;
    
    public Casquette(Person person) {
        this.person = person;
    }
    @Override
    public String getDescription() {
        return person.getDescription() + "a casquette  "; //鸭舌帽
    }

    @Override
    public double cost() {
        return 75 + person.cost();
    }

}
复制代码

最后我们在测试类测试我们的代码:

public class Shopping {

    public static void main(String[] args) {
        Person person = new Teenager();
        
        person = new Shirt(person);
        person = new Casquette(person);
        
        System.out.println(person.getDescription() + " ¥ " +person.cost());
    }

}
复制代码

先创建一个 Teenager 对象,接着用 Shirt 装饰它,就变成了穿着 ShirtTeenager ,再用 Casquette 装饰,就变成了戴着 Casquette 的穿着 ShirtTeenager 。运行结果如下所示:

Java设计模式-装饰者模式

我们梳理一下以上的逻辑,画出如下所示的韦恩图:

Java设计模式-装饰者模式
Teenager、Shirt、Casquette 都是继承自 Person 基类,但是具体实现不同, TeenagerPerson 的直接子类,表示了被装饰者; Teenager、Shirt 是装饰者,保存了 Person 的引用,实现了 cost() 方法,并且在 cost() 方法内部,不但实现了自己的逻辑,同时也调用了 Person 引用的 cost()

方法,即获取了被装饰者的信息,这是装饰者的一个特点,保存引用的目的就是为了获取被装饰者的状态信息,以便将自身的特性加以组合。

总结

以上就是装饰者模式的一个小栗子,讲述了装饰者的基本用法。总结一下装饰者模式的特点。

  1. 装饰者和被装饰者有相同的接口(或有相同的父类)。
  2. 装饰者保存了一个被装饰者的引用。
  3. 装饰者接受所有客户端的请求,并且这些请求最终都会返回给被装饰者(参见韦恩图)。
  4. 在运行时动态地为对象添加属性,不必改变对象的结构。

使用装饰者模式的最大好处就是其拓展性十分良好,通过使用不同的装饰类来使得对象具有多种多样的属性,灵活性比直接继承好。然而它也有缺点,那就是会出现很多小类,即装饰类,使程序变得复杂。

扩展阅读

学习了装饰者模式用法、特点以及优缺点后,我们再来看看装饰者模式在实际开发过程的应用。装饰者模式在Java中经常出现的地方就是 JavaIO 。提到 JavaIO ,脑海中就冒出了大量的类: InputStream、FileInputStream、BufferedInputStream ……等,真是头都大了,其实,这里面大部分都是装饰类,只要弄清楚这一点就容易理解了。我们来看看JavaIO是怎样使用装饰者模式的。

从字符流来分析,我们知道,有两个基类,分别是 InputStreamOutputStream ,它们也就是我们上面所述的 Component 基类。接着,它有如下子类: FileInputStream、StringBufferInputStream 等,它们就代表了上面所述的 ConcreteComponent ,即装饰对象。此外, InputStream 还有 FilterInputStream 这个子类,它就是一个抽象装饰者,即 Decorator ,那么它的子类: BufferedInputStream、DataInputStream 等就是具体的装饰者了。那么,从装饰者模式的角度来看 JavaIO ,是不是更加容易理解了呢?

下面,我们来自己实现自己的JavaIO的装饰者。要实现的功能是:把一段话里面的每个单词的首字母大写。

我们先新建一个类:UpperFirstWordInputStream.java

public class UpperFirstWordInputStream extends FilterInputStream {
    
    private int cBefore = 32;
    
    protected UpperFirstWordInputStream(InputStream in) {
        //由于FilterInputStream已经保存了装饰对象的引用,这里直接调用super即可
        super(in);
    }
    
    public int read() throws IOException{
        //根据前一个字符是否是空格来判断是否要大写
        int c = super.read();
        if(cBefore == 32)
        {
            cBefore = c;
            return (c == -1 ? c: Character.toUpperCase((char) c));
        }else{
            cBefore = c;
            return c;
        }
            
    }
    
}
复制代码

接着编写一个测试类:InputTest.java

public class InputTest {

    public static void main(String[] args) throws IOException {
        int c;
        StringBuffer sb = new StringBuffer();
        try {
            //这里用了两个装饰者,分别是BufferedInputStream和我们的UpperFirstWordInputStream
            InputStream in = new UpperFirstWordInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
            while((c = in.read()) >= 0)
            {
                sb.append((char) c);
            }
            System.out.println(sb);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}
复制代码

(注意:上面的 test.txt 文件需要你自行创建,放到同一个文件夹内即可,内容可随意填写。) 最后,我们看下运行结果:

Java设计模式-装饰者模式
Java设计模式-装饰者模式
原文  https://juejin.im/post/5d6493f3f265da03e3698fb0
正文到此结束
Loading...