Head First第三章

# 装饰者模式

作用:“给爱用继承的人一个全新的设计眼界”,即解决继承滥用,能够在不修改任何底层代码的情况下,给你的(或别人的)对象赋予新的职责。

解决问题:类数量爆炸(指继承)、设计死板、基类加入新功能并不适用于所有子类。

# 符合设计原则:开放 - 关闭原则

定义:类应该对扩展开放,对修改关闭。即在不修改现有代码的情况下,允许类的扩展。

说明:过度使用开放 - 关闭原则(选择需要被扩展的代码部分时)要小心,每个地方都使用该原则是一种浪费,因为该原则通常会引入新的抽象层次,导致代码变得复杂而难以理解。

# 解释

(1)装饰者和被装饰者对象有相同的超类型,通常装饰者模式采用的是抽象类;

(2)你可以用一个或多个装饰者包装一个对象;

(3)既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)场合,可以用装饰过的对象代替它;

(4)装饰者可以在所委托被装饰者的行为之前 / 或之后,加上自己的行为,以达到特定的目的;

(5)对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象;

# 装饰者模式定义及结构图

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

# 例子

(1)将调料作为装饰者,将饮料作为被装饰对象;

(2)一种饮料可以填加多种调料,即可以被多种调料装饰;

(3)他们拥有共同的超类,即他们必须是一样的类型,这样好利用继承达到 “类型匹配”,注意这里不是利用继承获得 “行为”;

代码:

定义抽象组件 - 饮料类:

package com.example.test;
 
/**
 * <p>
 * <code>Beverage</code>
 * </p>
 * Description: 饮料基类
 * (1) Beverage 是一个抽象类,有两个方法:getDescription () 及 cost ();
 * (2) getDescription () 已经在此实现了,但是 cost () 必须在子类中实现;
 *
 * @author Mcchu
 * @date 2018/1/16 9:24
 */
public abstract class Beverage {
    String description = "Unknown Beverage";
 
    public String getDescription() {
        return description;
    }
    
    public abstract double cost();
}

定义调料抽象类,也就是装饰者类:

package com.example.test;
 
/**
 * <p>
 * <code>Condiment</code>
 * </p>
 * Description: 调料装饰者类
 * (1) 首先,必须让 Condiment Decorator 能够取代 Beverage,所以将 Condiment Decorator 扩展自 Beverage 类;
 * (2) 所有的调料装饰者都必须重新实现 getDescription () 方法;
 *
 * @author Mcchu
 * @date 2018/1/16 9:29
 */
public abstract class CondimentDecorator extends Beverage{
 
    public abstract String getDescription();
}

定义具体组件 - 饮料类:

浓缩咖啡

package com.example.test;
 
/**
 * <p>
 * <code>Espresso</code>
 * </p>
 * Description: 具体饮料 - 浓缩咖啡
 * (1) Espresso 类扩展自 Beverage 类,因为 Espresso 是饮料的一种
 * (2) 构造器是为了设置饮料的描述,description 实例变量继承自 Beverage
 * (3) cost () 方法不需要管调料的价钱,直接把 Espresso 的价格返回
 * 
 * @author Mcchu
 * @date 2018/1/16 9:36
 */
public class Espresso extends Beverage {
    
    public Espresso(){
        description = "Espresso";
    }
    
    @Override
    public double cost() {
        return 1.99;
    }
}

混合咖啡

package com.example.test;
 
/**
 * <p>
 * <code>HouseBlend</code>
 * </p>
 * Description: 具体饮料 - 混合咖啡
 * (1) 这里做法和浓缩咖啡类相似
 *
 * @author Mcchu
 * @date 2018/1/16 9:41
 */
public class HouseBlend extends Beverage {
 
    public HouseBlend(){
        description = "House Blend Coffee";
    }
 
    @Override
    public double cost() {
        return .89;
    }
}

下面我们定义具体的装饰者类:

package com.example.test;
 
/**
 * <p>
 * <code>Mocha</code>
 * </p>
 * Description:
 * (1) 具体装饰者类 - 摩卡调料
 * (2) 摩卡调料是一个装饰者类,扩展自 CondimentDecorator,而 CondimentDecorator 扩展自 Beverage 类
 *
 * @author Mcchu
 * @date 2018/1/16 9:46
 */
public class Mocha extends CondimentDecorator {
 
    /**
     * 用一个实例变量记录饮料,也就是被装饰者
     */
    Beverage beverage;
 
    /**
     * 想办法让被装饰者(饮料)被记录到实例变量中
     * 这里把饮料当作构造器的参数,再由构造器将此饮料记录在实例变量中
     * 
     * @param beverage 被装饰者
     */
    public Mocha( Beverage beverage ){
        this.beverage = beverage;
    }
 
    /**
     * 我们希望叙述不只是描述饮料,而是完整的连调料也描述出来
     * 这里首先利用委托的做法,得到一个饮料的叙述,然后在其后加上附加的叙述
     * 
     * @return 饮料描述 + 附加的调料描述
     */
    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Mocha";
    }
 
    /**
     * 计算带摩卡调料的饮料价钱
     * 这里先把调用委托给被装饰对象,以计算价钱,然后加上摩卡的价钱,得到最后结果
     * 
     * @return 摩卡调料价钱 + 被装饰对象的价钱
     */
    @Override
    public double cost() {
        return .20 + beverage.cost();
    }
}

同理,定义奶泡调料装饰者类:

package com.example.test;
 
/**
 * <p>
 * <code>Whip</code>
 * </p>
 * Description: 装饰者类 - 奶油调料
 *
 * @author Mcchu
 * @date 2018/1/16 9:46
 */
public class Whip extends CondimentDecorator {
 
    /**
     * 用一个实例变量记录饮料,也就是被装饰者
     */
    Beverage beverage;
 
    /**
     * 想办法让被装饰者(饮料)被记录到实例变量中
     * 这里把饮料当作构造器的参数,再由构造器将此饮料记录在实例变量中
     *
     * @param beverage 被装饰者
     */
    public Whip(Beverage beverage ){
        this.beverage = beverage;
    }
 
    /**
     * 我们希望叙述不只是描述饮料,而是完整的连调料也描述出来
     * 这里首先利用委托的做法,得到一个饮料的叙述,然后在其后加上附加的叙述
     *
     * @return 饮料描述 + 附加的调料描述
     */
    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Whip";
    }
 
    /**
     * 计算带奶泡调料的饮料价钱
     * 这里先把调用委托给被装饰对象,以计算价钱,然后加上奶泡的价钱,得到最后结果
     *
     * @return 奶泡调料价钱 + 被装饰对象的价钱
     */
    @Override
    public double cost() {
        return .30 + beverage.cost();
    }
}

再定义豆浆装饰者类:

package com.example.test;
 
/**
 * <p>
 * <code>Soy</code>
 * </p>
 * Description: 装饰者类 - 豆浆
 *
 * @author Mcchu
 * @date 2018/1/16 9:46
 */
public class Soy extends CondimentDecorator {
 
    /**
     * 用一个实例变量记录饮料,也就是被装饰者
     */
    Beverage beverage;
 
    /**
     * 想办法让被装饰者(饮料)被记录到实例变量中
     * 这里把饮料当作构造器的参数,再由构造器将此饮料记录在实例变量中
     *
     * @param beverage 被装饰者
     */
    public Soy(Beverage beverage ){
        this.beverage = beverage;
    }
 
    /**
     * 我们希望叙述不只是描述饮料,而是完整的连调料也描述出来
     * 这里首先利用委托的做法,得到一个饮料的叙述,然后在其后加上附加的叙述
     *
     * @return 饮料描述 + 附加的调料描述
     */
    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Soy";
    }
 
    /**
     * 计算带豆浆调料的饮料价钱
     * 这里先把调用委托给被装饰对象,以计算价钱,然后加上豆浆的价钱,得到最后结果
     *
     * @return 豆浆调料价钱 + 被装饰对象的价钱
     */
    @Override
    public double cost() {
        return .40 + beverage.cost();
    }
}

至此,饮料基类,装饰者基类,具体饮料类(被装饰),具体调料类(装饰者)已定义完毕,如果引入了一种新的饮料或调料,只需增加具体的类即可,下面是测试:

package com.example.test;
 
/**
 * <p>
 * <code>StarbuzzCoffee</code>
 * </p>
 * Description: 测试类
 *
 * @author Mcchu
 * @date 2018/1/16 10:06
 */
public class StarbuzzCoffee {
 
    public static void main(String[] args) {
        // 一杯 Espresso,不加调料,打印描述与价钱
        Beverage beverage = new Espresso();
        System.out.println( "描述:"+beverage.getDescription() );
        System.out.println( "价钱:"+ "$" + beverage.cost() );
 
        // 一杯 HouseBlend,加两份摩卡,加一份奶泡,再加一份豆浆,打印描述与价钱
        Beverage beverage2 = new HouseBlend();
        beverage2 = new Mocha(beverage2);   // 用 Mocha 装饰 HouseBlend
        beverage2 = new Mocha(beverage2);  // 再用 Mocha 装饰 HouseBlend
        beverage2 = new Whip(beverage2);  // 用奶泡 Whip 装饰 HouseBlend
        beverage2 = new Soy(beverage2);  // 用豆浆 Soy 装饰 HouseBlend
        System.out.println( "描述:"+beverage2.getDescription() );
        System.out.println( "价钱:"+ "$" + beverage2.cost() );
    }
}

# JDK 中的装饰者: JAVA I/O

我们看一个类图就明白了,很像上面我们的例子

上面是 JDK 封装的输入流(input...)设计,其实输出流(output...)的设计也是类似,还有 Writer 流(作为基于字符数据的输入输出)等。

# 装饰者模式引出的缺点

利用装饰者模式,常常造成设计中有大量的小类,数量实在太多,可能会造成使用此 API 的程序员的困扰。

# 模拟 JDK 中 JAVA I/O 的设计,自己编写一个装饰者,把输入的所有大写字符转小写

package com.example.test;
 
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
 
/**
 * <p>
 * <code>LowerCaseInputStream</code>
 * </p>
 * Description: 自定义 java i/o 装饰者
 * 扩展 FilterInputStream 类,这是所有 InputStream 的抽象装饰者
 *
 * @author Mcchu
 * @date 2018/1/16 10:37
 */
public class LowerCaseInputStream extends FilterInputStream {
 
    /**
     * Creates a <code>FilterInputStream</code>
     * by assigning the  argument <code>in</code>
     * to the field <code>this.in</code> so as
     * to remember it for later use.
     *
     * @param in the underlying input stream, or <code>null</code> if
     *           this instance is to be created without an underlying stream.
     */
    protected LowerCaseInputStream(InputStream in) {
        super(in);
    }
 
    /**
     * 针对字节:将大写转成小写
     *
     * @return 小写字节
     * @throws IOException IO 异常
     */
    public int read() throws IOException {
        int c = super.read();
        return ( c == -1 ? c : Character.toLowerCase( (char) c ) );
    }
 
    /**
     * 针对字节数组:将大写转成小写
     *
     * @param b 传入字节数组(the buffer into which the data is read)
     * @param offset 起始位移(the start offset in the destination array)
     * @param len 长度(the maximum number of bytes read)
     * @return 小写字节数组(the total number of bytes read into the buffer)
     * @throws IOException IO 异常
     */
    public int read(byte[] b, int offset, int len) throws IOException{
        int result = super.read( b, offset, len );
        for ( int i=offset; i<offset+result; i++ ){
            b[i] = (byte)Character.toLowerCase( (char)b[i] );
        }
        return result;
    }
}

测试

package com.example.test;
 
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
 
/**
 * <p>
 * <code>TestLowerCaseInputStream</code>
 * </p>
 * Description: 测试我们自定义的 LowerCaseInputStream
 *
 * @author Mcchu
 * @date 2018/1/16 10:47
 */
public class TestLowerCaseInputStream {
 
    public static void main(String[] args) throws IOException{
        int c;
        
        try {
            // 设置 FileInputStream
            FileInputStream fis = new FileInputStream("text.txt");
            
            // 先用 BufferedInputStream 装饰她,再用我们新定义的 LowerCaseInputStream 装饰她
            InputStream in = new LowerCaseInputStream( new BufferedInputStream( fis ));
            
            while( ( c = in.read() ) >= 0 ){
                System.out.println( (char)c );
            }
            
            in.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}