《HF 设计模式》 C3 装饰者模式

1 模拟咖啡厅菜单

现在我们去尝试模拟咖啡厅的菜单,比如顾客需要一杯拿铁,那我们就给他一杯拿铁,并计算出价格

但是如果顾客需要加入一些调料,如摩卡,奶泡,蒸奶等,问题就变得复杂起来,调料有很多种,每种调料都有其价格,顾客可以选择1种或多种,这让我们获得一个加了调料的拿铁对象变得复杂起来,而且如果需要双倍的奶泡呢?如果还需要引入大中小杯呢?
这会使得计算最终的价格变得格外繁琐

那么该如何设计一个咖啡厅菜单系统,保证能满足顾客的需求,且能计算正确的费用?

1.1 粗糙地使用继承

设计一个材料父类Beverage,所有子类产品继承Beverage,各自实现cost()抽象方法,获得实际的价格,getDescription用来取得产品的名称
在这里插入图片描述

Beverage:

/**
 * @author 雫
 * @date 2021/3/2 - 9:32
 * @function 材料
 */
public abstract class Beverage {
    
    
    private String description;

    public Beverage(String description) {
    
    
        this.description = description;
    }

    public String getDescription() {
    
    
        return description;
    }

    public abstract double cost();
}

几种产品

/**
 * @author 雫
 * @date 2021/3/2 - 9:34
 * @function 果汁
 */
public class Juice extends Beverage {
    
    

    public Juice() {
    
    
        super("juice");
    }

    @Override
    public double cost() {
    
    
        return 8;
    }

}


/**
 * @author 雫
 * @date 2021/3/2 - 9:37
 * @function 水果果汁
 */
public class JuiceWithFruit extends Beverage {
    
    

    public JuiceWithFruit() {
    
    
        super("juiceWithFruit");
    }

    @Override
    public double cost() {
    
    
        return 18;
    }
}

/**
 * @author 雫
 * @date 2021/3/2 - 9:35
 * @function 茶
 */
public class Tea extends Beverage {
    
    

    public Tea() {
    
    
        super("tea");
    }

    @Override
    public double cost() {
    
    
        return 10;
    }

}

测试:

在这里插入图片描述
现在通过继承,我们能得到几种简单的产品,现在增加各种产品,以及加了各种不同调料的产品,每个都来继承Beverage

在这里插入图片描述
使用继承,为了增加各种新的产品,就需要不断扩展新的类,以拿铁为例,一个什么都不加,一个加了奶泡,一个大杯,一个小杯,按照这样的设计方式,就是无上限的类,更糟糕的是,如果某个产品或者某个材料的价格发生了变化,那么与之相关的所有产品都需要进行修改

并且这样的设计违背了我们的设计原则
1,少用继承,多用组合
2,针对接口编程,而不是针对实现编程

1.2 细致一些地使用继承

上述使用继承地方式过于简单,可以换一种方式来实现继承,我们在Beverage中实现cost方法,即把所有调料放到父类中设置为boolean类型,并为其加上set/get方法,用户点单时,根据需要设置调料为true/false,再由父类计算出调料的价格加上子类商品本身的价格来实现

Beverage:

/**
 * @author 雫
 * @date 2021/3/2 - 10:11
 * @function 材料
 */
public class Beverage {
    
    
    private String description;

    private boolean milk;
    private boolean soy;
    private boolean mocha;
    private boolean whip;

    public Beverage(String description) {
    
    
        this.description = description;
    }

    public String getDescription() {
    
    
        return description;
    }

    public boolean isMilk() {
    
    
        return milk;
    }

    public void setMilk(boolean milk) {
    
    
        this.milk = milk;
    }

    public boolean isSoy() {
    
    
        return soy;
    }

    public void setSoy(boolean soy) {
    
    
        this.soy = soy;
    }

    public boolean isMocha() {
    
    
        return mocha;
    }

    public void setMocha(boolean mocha) {
    
    
        this.mocha = mocha;
    }

    public boolean isWhip() {
    
    
        return whip;
    }

    public void setWhip(boolean whip) {
    
    
        this.whip = whip;
    }

    public double cost() {
    
    
        double price = 0;
        if(isMilk()) {
    
    
            price += 2;
        }
        if(isSoy()) {
    
    
            price += 1;
        }
        if(isMocha()) {
    
    
            price += 4;
        }
        if(isWhip()) {
    
    
            price += 3;
        }
        return price;
    }

}

产品:

public class Tea extends Beverage {
    
    

    public Tea() {
    
    
        super("Tea");
    }

    @Override
    public double cost() {
    
    
        return 10 + super.cost();
    }
}

测试:
在这里插入图片描述
通过这种方式的继承,我们避免了“类爆炸”,能较为灵活的生成用户想要的产品,但是这样的代码不具有弹性,对象和实现已经捆绑在一起了,很难轻松的加入新功能以及进行细微的调整

考虑如下一些因素:

1,顾客想要双倍的调料
2,顾客想要大杯的产品
3,产品或调料价格改变时,需要更改源码
4,一旦出现新的调料,需要在Beverage中新增set/get方法,并修改其cost方法
5,对某些产品来说,是不应该继承Beverage中的某些调料的
   如茶没有必要继承奶泡,摩卡这些调料,这是冗余不合理的设计

1.3 再谈继承和组合

虽然继承威力强大,能够将“好东西”不断复用,但是继承也有者若干缺陷

利用继承设计子类的行为,是在静态编译时决定的,且所有的子类都会继承相同的行为,这一点虽然给了子类更多功能,但也限制了子类,它占用了子类宝贵的“继承名额”,并且可能让子类继承到一些冗余不需要的东西,并且继承往往让你实现的功能和类绑定在了一起,这让扩展和修改变得麻烦

利用组合能动态扩展对象的行为,可以在“运行时”动态地进行扩展,可以将多种新功能通过组合来扩展对象的行为,就可以在运行时动态地进行扩展

通过动态地组合对象,可以为代码增加新的功能,而无需修改现有代码,不修改现有代码,那么引进bug或产生副作用的机会将大大减小

回到设计原则:

多用组合,少用继承

所谓组合,就是把几个类放在一块使用,把某个类(接口)当作别的类的成员,这样可以为类扩展更丰富的功能

程序应该免于变化,而善于扩展

1.4 开放-关闭原则

设计原则:

类应该对扩展开放,对修改关闭

该设计原则要求:
允许类容易扩展,在不修改现有代码的情况下,就可搭配新的行为
即尽量对程序在不更改源码的情况下进行扩展

如果能满足这样的设计原则,那么设计出来的系统就具有弹性可以应对变化,可以接受新功能来应对改变的需求

虽然这有些矛盾,但的确有些技术能够允许不直接修改源码的情况下对其进行扩展,在选择需要被扩展的代码部分需要十分小心,每个地方都采用开放-关闭原则是一种浪费,也没必要,还会导致代码变得复杂

1.5 认识装饰者模式

回到模拟咖啡厅菜单的问题,先前使用继承会出现的问题:

1,类数量爆炸
2,设计死板
3,子类可能继承冗余的功能

现在我们换一种思路,以产品为主体,以调料为装饰
现在生成一杯加了摩卡和奶泡的拿铁的过程:

1,生成一个拿铁对象
2,以摩卡对象装饰它
3,以奶泡对象装饰它
4,调用cost方法得到产品价格
   并委托依赖将调料的价格加上去

在这里插入图片描述

在这里插入图片描述

装饰者模式动态地将责任附加到了对象上,若要扩展功能,装饰者提供了比继承更有弹性地替代方案

装饰者模式的几个特点:

1,装饰者和被装饰者有相同的超类
2,可以用一个或多个装饰者包装一个对象
3,装饰者可以在所委托被装饰者地行为前/后
   加上自己的行为,以达到特定的目的
4,对象可以在任何时候被装饰

在这里插入图片描述

1.6 重新设计咖啡菜单

设计结构:
在这里插入图片描述
虽然一直在强调,少用继承,但这里装饰者和被装饰者都继承一个超类Beverage因为装饰者和被装饰者要求是同一类型,我们利用继承完成了类型匹配,而不是用继承获得功能,在Java中也可以让装饰者和被装饰者实现同一接口以达到类型匹配,但这里用继承更合理

装饰者与被装饰者的超类:

/**
 * @author 雫
 * @date 2021/3/2 - 12:19
 * @function 装饰者与被装饰者的超类
 */
public abstract class Beverage {
    
    
    protected String description;

    public Beverage(String description) {
    
    
        this.description = description;
    }

    public String getDescription() {
    
    
        return description;
    }

    public abstract double cost();
}

装饰者的超类:

/**
 * @author 雫
 * @date 2021/3/2 - 12:20
 * @function 装饰者的超类
 */
public abstract class Decorator extends Beverage {
    
    

    public Decorator(String description) {
    
    
        super(description);
    }

    @Override
    public abstract double cost();

    @Override
    public abstract String getDescription();
}

被装饰者:

/**
 * @author 雫
 * @date 2021/3/2 - 12:25
 * @function
 */
public class Tea extends Beverage {
    
    

    public Tea() {
    
    
        super("Tea");
    }

    @Override
    public double cost() {
    
    
        return 10;
    }
}

装饰者:

/**
 * @author 雫
 * @date 2021/3/2 - 12:22
 * @function
 */
public class Mocha extends Decorator {
    
    
    private Beverage beverage;

    public Mocha(Beverage beverage) {
    
    
        super("摩卡");
        this.beverage = beverage;
    }

    @Override
    public double cost() {
    
    
        return beverage.cost() + 5;
    }

    @Override
    public String getDescription() {
    
    
        return beverage.getDescription() + " + Mocha";
    }
}

/**
 * @author 雫
 * @date 2021/3/2 - 12:24
 * @function
 */
public class Milk extends Decorator {
    
    
    private Beverage beverage;

    public Milk(Beverage beverage) {
    
    
        super("牛奶");
        this.beverage = beverage;
    }

    @Override
    public double cost() {
    
    
        return 3 + beverage.cost();
    }

    @Override
    public String getDescription() {
    
    
        return beverage.getDescription() + " + Milk";
    }
}

测试:
在这里插入图片描述
新的设计采用了装饰者模式,采用了多用组合的设计原则,其中把被装饰者作为装饰者的成员,进而调用被装饰者的方法,还采用了开闭原则,我们的代可以在不更改原有代码的情况下,增加更多的装饰者以扩展功能

1.7 装饰者模式小结

在这里插入图片描述
每一次“装饰”的过程,是生成一个新的装饰者对象的过程,这个装饰者对象不仅是简单的装饰,而是被装饰者“装饰”完后的成品
即被装饰者是拿铁,装饰者是牛奶,装饰完成后,装饰者是加了牛奶的拿铁,把对象作为成员,通过组合扩展了功能

在这里插入图片描述
在装饰者内,通过调用被装饰者的一些方法,就可以做更多的事,可以“添油加醋”的扩展出更多功能,而不需要更改源码

比如现在我们需要大中小杯的计价功能,有了装饰者模式,我们只需要新建一种装饰者即可,包装一下指定的饮料和调料即可

/**
 * @author 雫
 * @date 2021/3/2 - 14:38
 * @function
 */
public class Cup extends Decorator {
    
    
    private int size;
    private Beverage beverage;

    public Cup(Beverage beverage, int size) {
    
    
        super("容量");
        this.beverage = beverage;
        this.size = size;
    }

    @Override
    public double cost() {
    
    
        if(this.size == 1) {
    
    
            return 3 + beverage.cost();
        } else if(this.size == 2) {
    
    
            return 5 + beverage.cost();
        } else if(this.size == 3) {
    
    
            return 8 + beverage.cost();
        } else {
    
    
            return 5 + beverage.cost();
        }
    }

    @Override
    public String getDescription() {
    
    
        return beverage.getDescription() + " : " + this.size;
    }
}

在这里插入图片描述
通过装饰者模式,不用再回到Beverage增加新的size成员,在不更改源码的情况下,扩展了程序的功能,这就是系统弹性,越多的更改会造成越多的混乱

装饰者模式就是增加行为到被包装的对象上,但是通过上面对装饰者模式的使用,我们看到创建了很多对象,意味着我们要管理更多对象,但是装饰者通常是用类似工厂模式或生成其模式创建的,它们封装的很好

1.8 Java I/O中的装饰者模式

在java.io中充满了装饰者模式,再来看一遍装饰者模式的结构:
在这里插入图片描述
先来看java.io中的几个类:

FileInputStream
BufferedInputStream
LineNumberInputStream

这几个类之间的关系:
在这里插入图片描述
一目了然的装饰者模式,FileInputStream是被装饰者,BufferedInputStream和LineNumberInputStream是装饰者,它们是同一类型,共同的父类是InputStream

再来看一下调用关系:
在这里插入图片描述
在这里插入图片描述
装饰者模式也有一个“缺点”,利用装饰者模式,常常造成设计中产生大量的类,数量实在太多,可能会在使用API时出现困扰

我们可以编写一个类继承FilterInputStream来自己做一个I/O装饰者

/**
 * @author 雫
 * @date 2021/3/2 - 15:25
 * @function 一个简单的装饰者
 * 用于将输入的字符流全转换成小写
 */
public class LowerCaseInputStream extends FilterInputStream {
    
    
    
    protected LowerCaseInputStream(InputStream in) {
    
    
        super(in);
    }

    @Override
    public int read() throws IOException {
    
    
        int c = super.read();
        return (c == -1 ? c : Character.toLowerCase((char) c) );
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
    
    
        int result = super.read(b, off, len);
        for(int i = off; i < off + result; i++) {
    
    
            b[i] = (byte)Character.toLowerCase((char) b[i]);
        }
        return result;
    }
    
}

测试:

/**
 * @author 雫
 * @date 2021/3/2 - 15:17
 * @function
 */
public class Test1 {
    
    

    public static void main(String[] args) throws IOException {
    
    

        int c;

        InputStream in =
                new LowerCaseInputStream(
                        new BufferedInputStream(
                                new FileInputStream("test.txt")));

        while ((c = in.read()) >= 0 ) {
    
    
            System.out.println((char) c);
        }

        in.close();
    }
}

FileInputStream对象先后被BufferedInputStream和LowerCaseInputStream装饰,虽然这里没什么用,但是把中间过程拆开,我们能分别得到被BufferedInputStream和LowerCaseInputStream装饰过的对象,调用被装饰过的对象的方法来扩充功能

1.9 关于装饰者模式的要点

1,继承是扩展的手段之一,但不一定是达到弹性设计的最佳方案

2,组合和委托可用于在运动时动态加上新的行为

3,装饰者模式意味着一群装饰者类,这些类用来包装具体类

4,装饰者可以在被装饰者的行为前或行为后添加自己的行为,甚至替换掉被装饰者的行为,从而达到特定的目的

5,装饰者会导致设计中出现很多类,如果过度使用会让程序复杂

猜你喜欢

转载自blog.csdn.net/weixin_43541094/article/details/114276603