设计模式之装饰模式(二)

目录

 

一,背景

二,探讨

2.1 装饰模式的使用

2.2 jdk中的装饰模式

三, 总结


 

一,背景

喝咖啡是现代年轻人很喜欢的一种休闲方式。按种类,咖啡可以分为摩卡(Mocha),拿铁(Latte)等,而每一种咖啡又可以添加一种或多种佐料就成了新的口味,比如说有的人喜欢加糖,而有的人喜欢加糖的同时又加牛奶。

隔壁街的咖啡馆马上就要开张了,老板听说你是一位优秀的“设计师”,所以想把他们家点咖啡的系统交给你来做,好处就是以后你来点咖啡都可以享受七折的优惠。于是,为了拿下这喝咖啡的好处,你开始思考如何完成这个系统,首先咖啡肯定包含有其描述信息,表明这到底是哪种咖啡?同时还应该有一个方法返回其价格,于是很容易你完成了这样一个类:

public abstract class Coffee {
    /**
     * 获取coffee的描述信息
     * @return
     */
    public abstract String getDescription();
    /**
     * 获取coffee的价格
     * @return
     */
    public abstract Float getCost();

}

这样做的好处就是:有什么咖啡只需要继承上面的咖啡基类,然后重写相应的描述和价格方法就行了,比如下面:

但是你突然发现一个问题,那就是咖啡的种类真是太多了,也怪人的口味太不一样。要是真的每一种咖啡都生成一个对应的类,如加牛奶的摩卡咖啡,加冰的摩卡咖啡,加巧克力的摩卡咖啡,加牛奶的拿铁咖啡等等,这样产生的类真是太多了,真的就是类爆炸!!!

但是这肯定难不倒聪明的你,从上面加不加巧克力,加冰与否,你产生了一个点子,可不可以给coffee类添加相应的“口味属性”,当为true表示有,当为false则代表不含有该调味品,而咖啡基类在设计时只需要充分考虑到所有的口味就行了,如下所示:

package com.mytest.other;

public abstract class Coffee {
    //是否加冰
    protected Boolean hasIce;
    //是否加牛奶
    protected Boolean hasMilk;
    //是否加巧克力
    protected Boolean hasChocolate;
    //...还有其他一些属性

    /**
     * 获取coffee的描述信息
     * @return
     */
    public abstract String getDescription();
    /**
     * 获取coffee的价格
     * @return
     */
    public abstract Float getCost();

    public Boolean getHasIce() {
        return hasIce;
    }

    public void setHasIce(Boolean hasIce) {
        this.hasIce = hasIce;
    }

    public Boolean getHasMilk() {
        return hasMilk;
    }

    public void setHasMilk(Boolean hasMilk) {
        this.hasMilk = hasMilk;
    }

    public Boolean getHasChocolate() {
        return hasChocolate;
    }

    public void setHasChocolate(Boolean hasChocolate) {
        this.hasChocolate = hasChocolate;
    }
}

 但是这样做也出现了个问题,那就是继承了该基类的咖啡派生类,自带有咖啡的基础属性,虽然达到了复用的好处,但是却带来了一些麻烦,比如热咖啡,里面出现是否加冰的属性是否适合?但是当然我们可以通过设置其为false来解决;但是更为严重的是,当咖啡馆新推出了一种新的口味,这就意味着你需要修改基类添加上相应的配料属性,同时带来了问题,父类发生变化,子类也需要修改相应的描述信息,以及价格的计算方式,这样一来违反了开闭原则,导致程序员大部分的时间都浪费在了类的维护上,那么有没有好的方法能解决呢?

装饰模式:在不改变原有类的基础上,给类添加新的职责。

二,探讨

2.1 装饰模式的使用

对于上述的问题,很适合使用装饰模式来完成。因为不管咖啡之中添加了什么奇奇怪怪的东西,但是并不影响它是咖啡的本质,所以我们可以设计一个装饰类,它专门用于处理这些复杂的配料问题。如下:声明基本的咖啡基类

public abstract class Coffee {
    /**
     * 获取coffee的描述信息
     * @return
     */
    public abstract String getDescription();
    /**
     * 获取coffee的价格
     * @return
     */
    public abstract Float getCost();

}

这与上面并没有什么区别。我们只需要声明获取咖啡描述及其价格的方法即可,然后具体的行动交给咖啡的派生类来处理。

声明具体的咖啡产品,例如最常见的摩卡咖啡和拿铁咖啡。

摩卡咖啡我们假设其价格为5元,如下:

public class MochaCoffee extends Coffee {

    private static final String description = "摩卡咖啡";
    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public Float getCost() {
        return 5.0f;
    }
}

拿铁咖啡假设其价格为6.5元,如下:

public class LatteCoffee extends Coffee {

    private final String description = "拿铁咖啡";
    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public Float getCost() {
        return 6.5f;
    }
}

如果还有其他的咖啡,那么只需要照着上面的例子从Coffee类派生即可,所有的咖啡只负责咖啡最基本的口味,而配料则由相应的装饰类来完成。

声明一个咖啡的装饰类叫做CoffeeDecorate,它需要继承自咖啡类,以保证它和coffee具有相同的动作,如下:

public abstract class CoffeeDecorate extends Coffee {
    @Override
      public String getDescription() {
        return "咖啡装饰器";
    }
}

然后从咖啡装饰类派生出具体的口味,不同的是它需要持有一个Coffee对象,因为咖啡装饰类,面向的对象就是咖啡,如下,我们声明一个加冰,和加牛奶的咖啡装饰类,

加冰:

/**
 * 加冰的咖啡
 */
public class IceCoffeeDecorate extends  CoffeeDecorate {
    //对具体的coffee进行装饰
    private Coffee coffee;
    public IceCoffeeDecorate(Coffee coffee){
        this.coffee = coffee;
    }

    /**
     * 加冰额外收5毛
     * @return
     */
    @Override
    public Float getCost() {
        return coffee.getCost() + 0.5f;
    }

    /**
     * 获取的coffee信息带上加冰
     * @return
     */
    @Override
    public String getDescription() {
        return coffee.getDescription() + "--加冰";
    }
}

加牛奶:

/**
 * 牛奶装饰类
 */
public class MilkCoffeeDecorate extends CoffeeDecorate {
    private Coffee coffee;

    public MilkCoffeeDecorate(Coffee coffee){
        this.coffee = coffee;
    }

    /**
     * 加牛奶,多收1元
     * @return
     */
    @Override
    public Float getCost() {
        return coffee.getCost() + 1;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + "--加牛奶";
    }
}

最后到了我们测试的时候,让我们“趁热”生产一个加冰,又加牛奶的摩卡咖啡吧:

public class CoffeeTest {
    @Test
    public void test(){
        //先生产摩卡咖啡
        Coffee coffee = new MochaCoffee();
        //然后对摩卡咖啡进行装饰
        //先加牛奶
        coffee = new MilkCoffeeDecorate(coffee);
        //再加冰
        coffee = new IceCoffeeDecorate(coffee);
        //获取咖啡的名称
        System.out.println(coffee.getDescription());
        //获取咖啡的加个 
        System.out.println(coffee.getCost());
    }
}

然后,看看具体的效果:

摩卡咖啡--加牛奶--加冰
6.5

相应的类图如下:

这样做的好处是什么呢?你不需要修改具体Coffee类的前提下,可以使用装饰类扩充其功能,而且你可以在一个装饰器下再嵌套另一个装饰器,俗称套娃,使Coffee更为强大。

2.2 jdk中的装饰模式

提到java中装饰模式的使用,就不得不提到java的io了,如下是我们最常见的用法:

    @Test
    public void testIo() throws Exception{
        InputStream in = new FileInputStream("a.txt");
        BufferedInputStream bis = new BufferedInputStream(in);
        bis.close();
    }

首先获取一个输入流对象,然后对其进行装饰,得到一个带缓冲的输入流,BufferedInputStream具体的类图如下:

从上面的设计可以得出InputStream就是被装饰的对象,而FilterInputStream就是具体的装饰类。我们可以参考,实现一个自己的字节流包装类,如下:

public class UpperCaseInputStream extends FilterInputStream {
    protected UpperCaseInputStream(InputStream in) {
        super(in);
    }

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

    @Override
    public int read(byte[] b) throws IOException {
        return super.read(b);
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int num = super.read(b, off, len);
        if(-1 == num)
            return num;
        for(int i = 0; i < num; i++){
            b[i] = (byte)Character.toUpperCase((char)b[i]);
        }
        return num;
    }
}

这个字节流包装类的作用便是将读到的字符全部转为大写,在使用时我们可以这样来做:

    @Test
    public void test1() throws Exception{
        InputStream in = new FileInputStream("D://a.txt");
        in = new UpperCaseInputStream(in);
        in = new BufferedInputStream(in);
        byte[] bytes = new byte[1024];
        int result;
        while((result = in.read(bytes)) != -1){
            for(int i = 0; i < result; i++)
                System.out.print((char)bytes[i]);
        }
        in.close();
    }

在这里有两个问题需要注意:

(1)read方法为什么返回的是int呢?而不应该是byte的嘛

刚开始我也不理解,最后发现是因为byte是带符号的,范围在-128~127,我们知道在判断是否读完的时候是用 -1 来进行判断的,但这就有个问题,万一某一个byte二进制表示就是-1 呢?此处没办法进行判断,所以采用了int。当用int时,如果读出来是-1,在其高位进行补零,结果就是正的,不影响使用-1来做判断的标志。

(2)java中默认采用Unicode编码,即2个字节表示一个字符,char类型的表示范围是0~65535,是没有负数的,这是Unicode编码的规定,Unicode规定了怎样来表示不同字符的编码,但是没有说明如何进行存储,而utf-8编码是一种可变长的编码方式,它是Unicode编码的具体实现。

三, 总结

装饰模式可以动态地给对象添加上新的职责,而不去修改原有的代码,它比继承更为灵活,同时具有弹性;缺点就是这种“套娃”模式在使用的时候不方便,因为装饰类的膨胀,需要开发者知道都有哪些装饰类,而且得知道它们不同的功能,增加了开发的难度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值