生活案例
咖啡厅 咖啡定制案例
在咖啡厅中,有多种不同类型的咖啡,客户在预定了咖啡之后,还可以选择添加不同的调料来调整咖啡的口味,当客户点了咖啡添加了不同的调料,咖啡的价格需要做出相应的改变。
要求
:程序实现具有良好的拓展性、改动方便、维护方便
【方案一】
写一个抽象类Drink,然后将所有咖啡和调料组合形成多个类来继承抽象类,缺点
:当增加一个单品咖啡,或者调味,类的数量就会大增,产生类爆炸问题
【方案二】
分析:
- 可以控制类的数量,不至于造成很多的类
- 增加或者删除调料种类时,代码的维护量很大
- 如果同样一种调料可以点多份时,可以将 hasMilk 返回一个对应int类型的数据来表示调料的份数
装饰者模式介绍
介绍
- 动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更好,装饰者模式也体现了开闭原则(ocp)
- 假如有一块蛋糕,如果加上奶油,就变成了奶油蛋糕;再加上草莓,就变成了草莓奶油蛋糕。整个过程就是在不断装饰蛋糕的过程。根据装饰者模式编写的程序的对象与蛋糕十分相似。首先有一个相当于蛋糕的对象,然后像不断地装饰蛋糕一样地不断地对其增加功能,它就变成了使用目的更加明确的对象。
出场角色
- Component(主体,被装饰对象):增加功能时的核心角色,定义了接口(API)
- ConcreteComponent(具体主体):实现了Component角色所定义的接口
- Decorator(装饰者):该角色具有与Component角色相同的接口(API),在它内部保存了Component角色
- ConcreteDecorator( 具体的装饰者)
案例实现
案例一(咖啡厅问题)
类图
代码实现
【被装饰主体】
package com.atguigu.decorator; public abstract class Drink { /** * 描述 */ public String des; /** * 价格 */ private float price = 0.0f; public String getDes() { return des; } public void setDes(String des) { this.des = des; } public float getPrice() { return price; } public void setPrice(float price) { this.price = price; } /** * 计算费用的抽象方法,需要子类来实现 * @return */ public abstract float cost(); }
【缓冲类:整合所有咖啡的共同点,这个类不一定要写,要结合实际情况】
package com.atguigu.decorator; public class Coffee extends Drink { @Override public float cost() { return super.getPrice(); } }
【单品咖啡:意大利咖啡】
package com.atguigu.decorator; public class Espresso extends Coffee { public Espresso() { setDes(" 意大利咖啡 "); // 初始化意大利咖啡的价格 setPrice(6.0f); } }
【单品咖啡:美式咖啡】
package com.atguigu.decorator; public class LongBlack extends Coffee { public LongBlack() { setDes(" longblack "); setPrice(5.0f); } }
【单品咖啡:浓咖啡】
package com.atguigu.decorator; public class ShortBlack extends Coffee{ public ShortBlack() { setDes(" shortblack "); setPrice(4.0f); } }
【装饰者】
package com.atguigu.decorator; /** * 装饰物,继承了Drink,还聚合了Drink */ public class Decorator extends Drink { private Drink obj; /** * 聚合Drink * @param obj */ public Decorator(Drink obj) { this.obj = obj; } @Override public float cost() { // getPrice 自己价格 + 咖啡的价格 return super.getPrice() + obj.cost(); } /** * 输出信息 * @return */ @Override public String getDes() { // obj.getDes() 输出被装饰者的信息 return des + " " + getPrice() + " && " + obj.getDes(); } }
【具体装饰者:巧克力】
package com.atguigu.decorator; /** * 具体的Decorator, 这里就是调味品 */ public class Chocolate extends Decorator { public Chocolate(Drink obj) { super(obj); setDes(" 巧克力 "); // 调味品 的价格 setPrice(3.0f); } }
【具体装饰者:牛奶】
package com.atguigu.decorator; public class Milk extends Decorator { public Milk(Drink obj) { super(obj); setDes(" 牛奶 "); setPrice(2.0f); } }
【具体装饰者:豆浆】
package com.atguigu.decorator; public class Soy extends Decorator{ public Soy(Drink obj) { super(obj); setDes(" 豆浆 "); setPrice(1.5f); } }
【主类】
package com.atguigu.decorator; public class CoffeeBar { public static void main(String[] args) { System.out.println("============== 订单1 ============="); // 装饰者模式下的订单:2份巧克力+一份牛奶的LongBlack // 1. 点一份 LongBlack Drink order = new LongBlack(); System.out.println("费用=" + order.cost()); System.out.println("描述=" + order.getDes()); System.out.println(); // 2.加入一份牛奶 order = new Milk(order); System.out.println("order 加入一份牛奶 费用 =" + order.cost()); System.out.println("order 加入一份牛奶 描述 = " + order.getDes()); System.out.println(); // 3.加入一份巧克力 order = new Chocolate(order); System.out.println("order 加入一份牛奶 加入一份巧克力 费用 =" + order.cost()); System.out.println("order 加入一份牛奶 加入一份巧克力 描述 = " + order.getDes()); System.out.println(); // 4.加入一份巧克力 order = new Chocolate(order); System.out.println("order 加入一份牛奶 加入2份巧克力 费用 =" + order.cost()); System.out.println("order 加入一份牛奶 加入2份巧克力 描述 = " + order.getDes()); System.out.println(); } }
【运行】
============== 订单1 ============= 费用=5.0 描述= longblack order 加入一份牛奶 费用 =7.0 order 加入一份牛奶 描述 = 牛奶 2.0 && longblack order 加入一份牛奶 加入一份巧克力 费用 =10.0 order 加入一份牛奶 加入一份巧克力 描述 = 巧克力 3.0 && 牛奶 2.0 && longblack order 加入一份牛奶 加入2份巧克力 费用 =13.0 order 加入一份牛奶 加入2份巧克力 描述 = 巧克力 3.0 && 巧克力 3.0 && 牛奶 2.0 && longblack
咖啡样式拓展代码实现
只需要新增一个单品咖啡类,就可以购买了,拓展性非常强大
【新增单品咖啡:无因咖啡】
package com.atguigu.decorator; public class DeCaf extends Coffee { public DeCaf() { setDes(" 无因咖啡 "); setPrice(1.0f); } }
【主类】
package com.atguigu.decorator; public class CoffeeBar { public static void main(String[] args) { System.out.println("============== 订单2 ============="); Drink order2 = new DeCaf(); System.out.println("order2 无因咖啡 费用 =" + order2.cost()); System.out.println("order2 无因咖啡 描述 = " + order2.getDes()); System.out.println(); order2 = new Milk(order2); System.out.println("order2 无因咖啡 加入一份牛奶 费用 =" + order2.cost()); System.out.println("order2 无因咖啡 加入一份牛奶 描述 = " + order2.getDes()); System.out.println(); } }
【运行】
============== 订单2 ============= order2 无因咖啡 费用 =1.0 order2 无因咖啡 描述 = 无因咖啡 order2 无因咖啡 加入一份牛奶 费用 =3.0 order2 无因咖啡 加入一份牛奶 描述 = 牛奶 2.0 && 无因咖啡 Process finished with exit code 0
案例二
类图
代码实现
【抽象主体】
package com.atguigu.decorator.Sample; public abstract class Display { /** * 获取横向字符数(抽象方法,需要子类去实现) * @return */ public abstract int getColumns(); /** * 获取纵向行数(抽象方法,需要子类去实现) * @return */ public abstract int getRows(); /** * 获取第row行的字符串(抽象方法,需要子类去实现) * @param row * @return */ public abstract String getRowText(int row); /** * 显示所有行的字符串 */ public void show() { // 遍历行数 for (int i = 0; i < getRows(); i++) { // 获取改行的字符串来打印出来 System.out.println(getRowText(i)); } } }
【具体主体】
package com.atguigu.decorator.Sample; /** * 该类用来显示单行字符串 */ public class StringDisplay extends Display { /** * 要显示的字符串 */ private String string; /** * 构造方法 * * @param string 要显示的字符串 */ public StringDisplay(String string) { this.string = string; } @Override public int getColumns() { // 字符数 return string.getBytes().length; } @Override public int getRows() { // 行数是1 return 1; } /** * 只有第0行可以获取到字符串,其他都是空 * @param row * @return */ @Override public String getRowText(int row) { // 仅当row为0时返回值 if (row == 0) { return string; } else { return null; } } }
【抽象装饰者】
package com.atguigu.decorator.Sample; /** * 装饰者抽象类,注意要继承抽象主体,并聚合抽象主体 */ public abstract class Border extends Display { /** * 表示被装饰物 */ protected Display display; protected Border(Display display) { // 在生成实例时通过参数指定被装饰物 this.display = display; } }
【具体修饰者1】
package com.atguigu.decorator.Sample; /** * 在字符串的左右两侧添加边框 */ public class SideBorder extends Border { /** * 表示装饰边框的字符 */ private char borderChar; /** * 通过构造函数指定Display和装饰边框字符 * @param display * @param ch */ public SideBorder(Display display, char ch) { super(display); this.borderChar = ch; } /** * 字符数为字符串字符数加上两侧边框字符数 * @return */ public int getColumns() { return 1 + display.getColumns() + 1; } /** * 行数即被装饰物的行数 * @return */ public int getRows() { // 在字符串的两侧添加字符并不会增加行数,所以直接返回主体的行数即可 return display.getRows(); } /** * 指定的那一行的字符串为被装饰物的字符串加上两侧的边框的字符 * @param row * @return */ public String getRowText(int row) { return borderChar + display.getRowText(row) + borderChar; } }
【具体装饰者2】
package com.atguigu.decorator.Sample; /** * 在字符串的上下左右都加上装饰框 */ public class FullBorder extends Border { public FullBorder(Display display) { super(display); } public int getColumns() { // 字符数为被装饰物的字符数加上两侧边框字符数 return 1 + display.getColumns() + 1; } public int getRows() { // 行数为被装饰物的行数加上上下边框的行数 return 1 + display.getRows() + 1; } /** * 指定的那一行的字符串 * * @param row * @return */ public String getRowText(int row) { if (row == 0) { // 上边框 return "+" + makeLine('-', display.getColumns()) + "+"; } else if (row == display.getRows() + 1) { // 下边框 return "+" + makeLine('-', display.getColumns()) + "+"; } else { // 其他边框 return "|" + display.getRowText(row - 1) + "|"; } } /** * 生成一个重复count次字符ch的字符串 * * @param ch * @param count * @return */ private String makeLine(char ch, int count) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < count; i++) { buf.append(ch); } return buf.toString(); } }
【主类】
package com.atguigu.decorator.Sample; public class Main { public static void main(String[] args) { Display b1 = new StringDisplay("Hello, world."); Display b2 = new SideBorder(b1, '#'); Display b3 = new FullBorder(b2); b1.show(); b2.show(); b3.show(); Display b4 = new SideBorder( new FullBorder( new FullBorder( new SideBorder( new FullBorder( new StringDisplay("你好,世界。") ), '*' ) ) ), '/' ); b4.show(); } }
【运行】
Hello, world. #Hello, world.# +---------------+ |#Hello, world.#| +---------------+ /+------------------------+/ /|+----------------------+|/ /||*+------------------+*||/ /||*|你好,世界。|*||/ /||*+------------------+*||/ /|+----------------------+|/ /+------------------------+/ Process finished with exit code 0
装饰着模式在IO流源码的应用
- InputStream 是抽象类, 类似我们前面讲的 Drink
- FileInputStream 是 InputStream 子类,类似我们前面的 DeCaf, LongBlack
- FilterInputStream 是 InputStream 子类:类似我们前面 的 Decorator 修饰者
DataInputStream 是 FilterInputStream 子类,具体的修饰者,类似前面的 Milk, Soy 等
FilterInputStream 类 有 protected volatile InputStream in; 即聚合了被装饰者
总结
- 在装饰者模式中,装饰者与被装饰者具有一致性。装饰者类是表示被装饰者的类的子类,这就体现了它们之间的一致性,它们具有相同的接口,这样,就算被装饰者被装饰了,接口还是向外暴露的(接口的透明性)
- 可以在不改变被装饰者的前提下增加功能,如案例中在显示字符串之前对字符串进行修饰
- 只需要一些装饰物即可添加许多功能:通过自由组合调料,可以让咖啡拥有各种不同的味道
- 装饰者模式也有缺点:会导致程序中增加许多功能类似的很小的类
什么是父类和子类的一致性
可以将子类的实例保存到父类的变量中,也可以直接调用从父类中继承的方法
如何让自己和被委托对象有一致性
使用委托让接口具有透明性时,自己和被委托对象具有一致性
Rose和Violet都有相同的method方法。Rose将method方法的处理委托给了 Violet。这两个类虽然都有 method 方法,但是没有明确在代码中体现出“共通性”。
如果要明确地表示method方法是共通的,只需要像下面这样编写一个抽象类Flower,然后让Rose和Violet都继承并实现方法即可。
或者让Flower作为接口
文章说明
- 本文章为本人学习尚硅谷的学习笔记,文章中大部分内容来源于尚硅谷视频(点击学习尚硅谷相关课程),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对尚硅谷的优质课程表示感谢。
- 本人还同步阅读《图解设计模式》书籍(图解设计模式/(日)结城浩著;杨文轩译–北京:人民邮电出版社,2017.1),进而综合两者的内容,让知识点更加全面