【设计模式】结构型模式其四: 装饰器模式

147 阅读7分钟

装饰器模式

首先需要了解什么是装饰器(磨刀不误砍柴功): 装饰器是一种结构型设计模式,它允许动态地将新的行为或责任添加到对象上。装饰器模式通过将对象包装在装饰器对象中,以实现对对象的透明包装,从而在不改变对象自身的情况下,为对象增加新的功能或责任。

举一个例子:

有一个人,人可以吃饭,人也可以跑步,但每个人跑步速度,方式不一样。我就先写着,等人来继承。

Person类 (抽象构件)

    // 抽象类 人,拥有基本吃饭能力
    abstract class Person{
        public void eat(){
            System.out.println("我可以吃饭的");
        }
        public abstract void run();// 我可以跑,但我还不清楚怎么跑
    }

BoEr类 (具体构件)

现在是为人这个类编写继承类, 拥有基本吃饭功能与各自独特的跑步速度。

// 博尔特
class BoEr extends Person{
    @Override
    public void run() {
        System.out.println("跑的快");
    }
}
// 普通人
class normal extends Person{
    @Override
    public void run(){
        System.out.println("跑的慢");
    }
}

到目前还没有编写装饰器,只是为其作准备。

现在开始了:

衣服装饰类 (抽象装饰类)

abstract class ClothesDecorator extends Person{
    // 这里是将 抽象构件类 Person作为一个属性, 毕竟我想为Person这个类或它的子类进行装饰
    private Person person;
    // 为Person进行初始化
    ClothesDecorator(Person person){
        this.person = person;
    }
    // 继承了跑步方法,这里应该重写,但是我这里将ClothesDecorator作为抽象类,不用添加特别的方法(当然可以直接在这里添加独特的方法,写成抽象类是为了拓展性)。
    @Override
    public void run() {
        //这一步挺关键的, 当new出装饰器后,调用的是子类的run方法。如果是为Person添加修饰,Person原本的跑步还是应该执行。
        person.run();
    }
}

之后是 体现 刚才 抽象装饰器类 拓展性的代码(具体装饰类):

// 衣物装饰器
class CoolClothesDecorator extends ClothesDecorator{
    CoolClothesDecorator(Person person){
        // 初始化父类对象
        super(person);
    }
    // 重写run方法
    @Override
    public void run(){
        coolClothes();
        // 防止父类run方法丢失
        super.run();
    }
    // 该类特别的方法(修饰),是为run使用的,所以在run方法里调用
    public void coolClothes(){
        System.out.println("穿上很酷的衣服");
    }
}
// 裤子装饰器
class CoolPantsDecorator extends ClothesDecorator{
    CoolPantsDecorator(Person person){
        super(person);
    }
    @Override
    public void run(){
        // 添加的独特的业务方法 
        coolPants();
        // 原有业务方法
        super.run();
    }
    public void coolPants(){
        System.out.println("穿上很酷的裤子");
    }
}
// 鞋子装饰器
class CoolSheetsDecorator extends ClothesDecorator{
    CoolSheetsDecorator(Person person){
        super(person);
    }
    public void coolSheets(){
        System.out.println("穿上很酷的鞋子");
    }
    @Override
    public void run(){
        coolSheets();
        super.run();
    }
}

测试代码:

public class Main {
    public static void main(String[] args) {
        // 为博尔特添加衣服(也可以把new BoEr() 换为 new NorMal())
        Person br = new BoEr();
        System.out.println("修饰之前的方法:");
        br.eat();
        br.run();
        System.out.println("下面是修饰后的:");
        // 抽象类(或接口)指向装饰器,它们的返回类型Perso可以是(它们都是Person的子类)
        Person ccd = new CoolClothesDecorator(br);
        // 添加裤子
        Person cpd = new CoolPantsDecorator(ccd);
        // 添加鞋子
        Person csd = new CoolSheetsDecorator(cpd);
        // 添加之后还是可以吃饭,原属性不丢失
        csd.eat();
        // 对修饰之后的跑步方法进行测试
        csd.run();
    }
}
    // 可以这样简写
  Person csd = new CoolSheetsDecorator( new CoolPantsDecorator(new CoolClothesDecorator(br)));

输出:

修饰之前的方法:
我能吃
跑的快
下面是修饰后的:
我能吃
穿上很酷的鞋子
穿上很酷的裤子
穿上很酷的衣服
跑的快

分析:不断new的过程就是把当前类作为新类的属性,然后进行调用。‘

我们每次new对象的过程中其实是把构造方法的属性作为父类。

如果父类的方法先执行 ,那么越深层的对象方法越先调用,我这里是先调用子类方法,所以顺序不一样。

JVM执行顺序:

  1. 首先 new CoolClothesDecorator(br) ,创建一个 CoolClothesDecorator 的实例,初始化其父类br
  2. 然后再 new CoolPantsDecorator() ,创建 CoolPantsDecorator 的实例,将CoolClothesDecorator 作为其父类,
  3. 然后再 CoolSheetsDecorator(), 创建 CoolSheetsDecorator 的实例, 将 CoolPantsDecorator作为其父类。
  4. 这样形成一个"装饰器链"
  5. 一层层的创建为其添加父类。
  6. 当调用结果对象的方法时,会按照装饰器链的顺序,依次调用每个组件的方法。

完整代码:(可以复制去测试,加深理解)

public class Main {
    public static void main(String[] args) {
        Person br = new BoEr();
        System.out.println("修饰之前的方法:");
        br.eat();
        br.run();
        System.out.println("下面是修饰后的:");
        Person ccd = new CoolClothesDecorator(br);
        Person cpd = new CoolPantsDecorator(ccd);
        Person csd = new CoolSheetsDecorator(cpd);
        csd.eat();
        csd.run();
    }
}
// 抽象人,拥有基本吃饭能力
abstract class Person{
    public void eat(){
        System.out.println("我能吃");
    }
    public abstract void run();
}
// 博尔特
class BoEr extends Person{
    @Override
    public void run() {
        System.out.println("跑的快");
    }
}
// 普通人
class normal extends Person{
    @Override
    public void run(){
        System.out.println("跑的慢");
    }
}
// 衣服装饰器
abstract class ClothesDecorator extends Person{
    private Person person;

    ClothesDecorator(Person person){
        this.person = person;
    }
    @Override
    public void run() {
        person.run();
    }
}
class CoolClothesDecorator extends ClothesDecorator{
    CoolClothesDecorator(Person person){
        super(person);
    }
    @Override
    public void run(){
        coolClothes();
        super.run();
    }
    public void coolClothes(){
        System.out.println("穿上很酷的衣服");
    }
}

class CoolPantsDecorator extends ClothesDecorator{
    CoolPantsDecorator(Person person){
        super(person);
    }
    @Override
    public void run(){
        coolPants();
        super.run();
    }
    public void coolPants(){
        System.out.println("穿上很酷的裤子");
    }
}

class CoolSheetsDecorator extends ClothesDecorator{
    CoolSheetsDecorator(Person person){
        super(person);
    }
    public void coolSheets(){
        System.out.println("穿上很酷的鞋子");
    }
    @Override
    public void run(){
        coolSheets();
        super.run();
    }
}

透明装饰器模式:

上面的装饰器设计模式: 我们实现的其实叫做透明装饰器模式

我们设计的装饰器类可以对同一个对象不断装饰:

  • 透明(Transparent)装饰模式:要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该将对象声明为具体构件类型或具体装饰类型,而应该全部声明为抽象构件类型(JAVA的装饰器FilterInputStream不是抽象类,但是它形如抽象类,重写了InputStream的所有方法,而且是可继承的)

  • 对于客户端而言,具体构件对象和具体装饰对象没有任何区别。 可以让客户端透明地使用装饰之前的对象和装饰之后的对象,无须关心它们的区别

缺点: 经过多次装饰后的对象功能更强,但是不能单独调用某一具体装饰器的具体方法

解决办法:上面我的代码是一直使用抽象的被装饰对象指向新建出来的装饰器对象,为此,当我们想要使用某一个具体类的具体方法,可以使用具体装饰类去指向:

Person ccd = new CoolClothesDecorator(br);
Person cpd = new CoolPantsDecorator(ccd);
CoolClothesDecorator csd = new CoolClothesDecorator(cpd);
csd.eat();
csd.run();
csd.coolClothes();

这样确实可以调用独特方法,但是客户端会区别对待: 具体装饰类必须指定,客户端必须选择,加大操作难度。

模式优缺点

模式优点:

  • 对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加
  • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为
  • 可以对一个对象进行多次装饰
  • 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,且原有类库代码无须改变,符合开闭原则

模式缺点

  • 使用装饰模式进行系统设计时将产生很多小对象,大量小对象的产生势必会占用更多的系统资源,在一定程度上影响程序的性能

  • 比继承更加易于出错,排错也更困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐


写这篇文章的原因:

今天看廖雪峰老师的Java教程,恰好看到Java的InputStream及其子类的设计,它就是典型的装饰器设计模式。

image.png

InputStream被左边三个类实现(具体的实现类) 文件输入流输入,字节数组输入流,Http输入流,按数据源表示。当然还没展示完所有实现类

FilterInputStream是Java中InputStream的装饰者类,它提供了一些额外的功能,可以在读取数据时对数据进行过滤或处理。FilterInputStream本身并不实现任何具体的数据读取操作,而是将数据读取委托给另一个InputStream对象,同时在读取数据时进行过滤或处理。与我举的例子不同的是它并不是一个抽象类 它拥有子类BufferedInputStream等类, 这些类拥有特别的方法,比如BufferedInputStream,它重写了read方法,因此,读取时会将流读到缓存。

// 创建一个FileInputStream对象
InputStream inputStream = new FileInputStream("test.txt");

// 使用BufferedInputStream装饰FileInputStream对象,实现缓存功能
inputStream = new BufferedInputStream(inputStream);

// 使用DataInputStream装饰BufferedInputStream对象,实现读取基本类型数据的功能
inputStream = new DataInputStream(inputStream);

// 读取数据
int data = inputStream.read();

Output也类似 image.png