装饰器模式
装饰器(Decorator)模式:
你
就是你
,是不一样的烟火,对你
进行任何的装饰
之后依旧是那个你
,归来依旧是那个少年!
装饰模式是继承关系的一个替代方案。装饰类Decorator,不管装饰多少层,返回的对象还是原类型(记住加粗的这句话,默念三遍,很重要)。
不管装饰多少次,类型不要发生变化才是装饰器模式。
装饰者的出现也再一次的证明了面向对象的设计原则:多用组合,少用继承!对扩展开放,对修改关闭!
定义
英文原话
Attach additional responsibilities to an object dynamically keeping the same interface. Decorators provide a flexible alternative to subclassing for extending functionality.
直译
动态地向对象附加额外的职责,同时保持相同的接口。装饰器为扩展功能提供了一种比子类化更灵活的替代方案。
如何理解呢?
装饰器模式是继承关系的替代:
装饰模式可以替代继承,解决类膨胀的问题。
下面会进行详细的阐述。
4个角色
类图
1. 抽象构件(Component)角色
该角色用于规范需要装饰的对象(原始对象)。
2. 具体构件(Concrete Component)角色
该角色实现抽象构件接口,定义一个需要装饰的原始类。
3. 装饰(Decorator)角色
该角色持有一个构件对象的实例,并定义一个与抽象构件接口一致的接口。
4. 具体装饰(Concrete Decorator)角色
该角色负责对构件对象进行装饰。
说明:
装饰类和被装饰类可以独立发展,而不会相互耦合。即Component 类无须知道Decorator类,Decorator类是从外部来扩展Component类的功能,而Decorator也不用知道具体的构件。
装饰模式是继承关系的一个替代方案。装饰类Decorator,不管装饰多少层,返回的对象还是Component(记住加粗的这句话,默念三遍,很重要)。
装饰模式可以动态地扩展一个实现类的功能
装饰模式的缺点:多层的装饰是比较复杂的。
代码示例
package com.polaris.designpattern.list2.structural.pattern2.decorator.proto;
// 抽象组件
interface Component {
void operate();
}
// 具体组件
class ConcreteComponent implements Component {
@Override
public void operate() {
System.out.println("执行具体组件的操作");
}
}
// 抽象装饰器
class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operate() {
if (component != null) {
component.operate();
}
}
}
// 具体装饰器
class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component component) {
super(component);
}
@Override
public void operate() {
super.operate(); // 调用抽象装饰器的operation方法
// 添加额外的功能或行为
addedBehavior();
}
public void addedBehavior() {
System.out.println("执行具体装饰器的额外操作");
}
}
// 客户端代码
public class ClassicalDecoratorDemo {
public static void main(String[] args) {
Component component = new ConcreteComponent();
// 递归地组合装饰器对象
Component decoratedComponent = new ConcreteDecorator(new ConcreteDecorator(component));
// 执行操作
decoratedComponent.operate();
}
}
/* Output:
执行具体组件的操作
执行具体装饰器的额外操作
执行具体装饰器的额外操作
*///~
典型场景
- ■ 需要扩展一个类的功能,或给一个类增加附加功能。
- ■ 需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
- ■ 需要为一批类进行改装或加装功能。
NOTES:
- 装饰模式是对继承的有力补充。
- 单纯使用继承时,在一些情况下就会增加很多子类,而且灵活性差,维护也不容易。
- 装饰模式可以替代继承,解决类膨胀的问题,如Java基础类库中的io类库(输入输出流相关的类)大量借鉴了装饰模式(参考进一步分析)。
示例解析:以去咖啡馆买咖啡为例
类图
抽象构件
对咖啡馆的咖啡进行了抽象
package com.polaris.designpattern.list2.structural.pattern2.decorator;
// 咖啡接口
interface Coffee {
String getDescription();
double getCost();
}
具体构件
咖啡馆的最普通的咖啡,对抽象构件的一种实现。
package com.polaris.designpattern.list2.structural.pattern2.decorator;
// 基础咖啡实现
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "美式咖啡";
}
@Override
public double getCost() {
return 2.5;
}
}
装饰器类
抽象出装饰器角色,一个抽象类,实现了抽象构件
Coffee
接口,它的实现在这里有两个,一个是加糖装饰器
SugarDecorator
,加奶装饰器MilkDecorator
,还可以根据需要继续扩展…
package com.polaris.designpattern.list2.structural.pattern2.decorator;
// 咖啡装饰器接口
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double getCost() {
return coffee.getCost();
}
}
// 加糖装饰器
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", 加糖";
}
@Override
public double getCost() {
return coffee.getCost() + 0.5;
}
}
// 加奶装饰器
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", 加奶";
}
@Override
public double getCost() {
return coffee.getCost() + 1.0;
}
}
// ... 可以添加更多装饰器,如肉桂粉装饰器等
测试类:去咖啡馆买咖啡
package com.polaris.designpattern.list2.structural.pattern2.decorator;
public class CoffeeShop {
public static void main(String[] args) {
// 创建基础咖啡
Coffee coffee = new SimpleCoffee();
System.out.println("你点了:" + coffee.getDescription() + ",价格是:" + coffee.getCost());
// 添加加糖装饰器
Coffee sweetCoffee = new SugarDecorator(coffee);
System.out.println("你加了糖,现在是:" + sweetCoffee.getDescription() + ",价格是:" + sweetCoffee.getCost());
// 递归添加加奶装饰器(加在已经加糖的咖啡上)
Coffee latte = new MilkDecorator(sweetCoffee);
System.out.println("你又加了奶,现在是:" + latte.getDescription() + ",价格是:" + latte.getCost());
// 你可以继续递归添加更多装饰器...
}
}
/* Output:
你点了:美式咖啡,价格是:2.5
你加了糖,现在是:美式咖啡, 加糖,价格是:3.0
你又加了奶,现在是:美式咖啡, 加糖, 加奶,价格是:4.0
*///~
注意
从上面示例你也会发现,无论装饰几层,类型不变,都是Coffee
类型
Coffee coffee = new SimpleCoffee();
Coffee sweetCoffee = new SugarDecorator(coffee);
Coffee latte = new MilkDecorator(sweetCoffee);
Java I/O类库借鉴了装饰器模式
UML类图
准备待读取的文件
ExampleFileForRead.txt
相对于当前工程的目录路径是:
src/main/java/com/polaris/designpattern/list2/structural/pattern2/decorator/inputstream/ExampleFileForRead.txt
文件内容如下:
设计模式类型:
1. ■ 创建型(creational)👈️
2. ■ 结构型(structural)👈️
3. ■ 行为型(behavioral)👈️
代码示例1:读取文件,并打印输出
控制台打印输出的中文、特殊字符乱码了
package com.polaris.designpattern.list2.structural.pattern2.decorator.inputstream;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* 存在打印中文、特殊字符乱码问题
*/
public class DecoratorPatternDemo {
public static void main(String[] args) {
String exampleFile = System.getProperty("user.dir") +
File.separator +
"src/main/java/com/polaris/designpattern/list2/structural/pattern2/decorator/inputstreamapi" +
"/ExampleFileForRead.txt";
try (
// 创建一个 FileInputStream 对象,用于从文件中读取数据
FileInputStream fis = new FileInputStream(exampleFile);
// 使用 BufferedInputStream 包装 FileInputStream,添加缓冲功能
BufferedInputStream bis = new BufferedInputStream(fis);
/*行不通:FilterInputStream构造函数受保护
FileInputStream fis = new FileInputStream(exampleFile);
FilterInputStream filteris= new FilterInputStream(fis);
BufferedInputStream bis = new BufferedInputStream(filteris);*/
) {
// 读取并处理数据,这里只是简单地打印到控制台
int data;
while ((data = bis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/* Output:
设计模å¼ç±»åž‹ï¼š
1. ■创建型(creational)👈ï¸
2. ■结构型(structural)👈ï¸
3. ■行为型(behavioral)👈ï¸
*///~
notes1:
上面是一个简单的测试类示例,展示了如何使用
BufferedInputStream
和FileInputStream
类似装饰模式的方式。
BufferedInputStream
、FilterInputStream
、InputStream
、FileInputStream
这些类在 Java 的 I/O 框架中与装饰模式(Decorator Pattern)有关,但并不严格地完全遵循装饰模式的经典定义。然而,我们可以说它们的设计受到装饰模式的启发(详细分析见进一步分析),特别是FilterInputStream
和它的子类BufferedInputStream
展示了类似装饰模式的行为。装饰模式允许向一个对象动态地添加职责(即功能),同时保持相同的接口。
在 Java I/O 中,
InputStream
是所有输入流的基类,定义了一个读取字节的通用接口。
FilterInputStream
是InputStream
的一个子类,用于包装另一个InputStream
并向其添加额外的功能,而不改变其基本的读取接口。
BufferedInputStream
是FilterInputStream
的一个子类,它提供了一个带有缓冲区的输入流,用于提高读取性能。它通过包装另一个InputStream
(可能是FileInputStream
或其他任何InputStream
的子类)来添加缓冲功能。
notes2:
在这个示例中,
FileInputStream
是原始的数据源,而BufferedInputStream
则作为一个装饰器,为原始数据源添加了缓冲功能。这种关系与装饰模式中的组件(Component)和装饰器(Decorator)之间的关系相似。FilterInputStream
和它的子类,通过组合(即包装另一个InputStream
对象)来扩展功能。可以从源码看出,
FilterInputStream
持有一个InputStream
的实例,通过构造函数进行的初始化,即FilterInputStream
和子类组合了一个InputStream
对象,public class FilterInputStream extends InputStream { /** * The input stream to be filtered. */ protected volatile InputStream in; protected FilterInputStream(InputStream in) { this.in = in; } //... 省略其余代码 .... }
总的来说,虽然
BufferedInputStream
、FilterInputStream
、InputStream
和FileInputStream
的设计受到装饰模式的启发,但它们并不完全遵循装饰模式的经典定义。不过,这种设计确实展示了如何动态地向对象添加功能而不改变其接口的思想。
代码示例2:解决乱码问题
package com.polaris.designpattern.list2.structural.pattern2.decorator.inputstream;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class DecoratorPatternDemoOk {
public static void main(String[] args) {
String exampleFile = System.getProperty("user.dir") +
File.separator +
"src/main/java/com/polaris/designpattern/list2/structural/pattern2/decorator/inputstream" +
"/ExampleFileForRead.txt";
try (
// 创建一个 FileInputStream 对象,用于从文件中读取数据
FileInputStream fis = new FileInputStream(exampleFile);
// 使用 InputStreamReader 读取字符,并指定编码为 UTF-8
Reader isr = new InputStreamReader(fis, StandardCharsets.UTF_8);
// 使用 BufferedReader 包装 InputStreamReader,添加缓冲功能
BufferedReader br = new BufferedReader(isr);
) {
// 读取并处理数据,这里只是简单地打印到控制台
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/* Output:
设计模式类型:
1. ■ 创建型(creational)👈️
2. ■ 结构型(structural)👈️
3. ■ 行为型(behavioral)👈️
*///~
notes:
遇到的乱码问题是因为尝试将文件的字节直接转换为字符((char) data)进行打印,
但是文件的编码(UTF-8)与 Java 默认使用的字符编码(通常是系统默认的,可能不是 UTF-8)不匹配。
在 Java 中,FileInputStream 读取的是原始字节,需要指定正确的字符编码来将这些字节转换为字符串。
为了正确地读取和显示文件内容,应该使用 InputStreamReader
和 BufferedReader
,并指定文件的字符编码,在这个示例,它使用 UTF-8 编码来读取和打印文件内容。
示例中,使用了 InputStreamReader 来读取字符,并指定了编码为 “UTF-8”。
然后,使用BufferedReader
包装了 InputStreamReader
来提供按行读取的功能。
最后,我使用 readLine() 方法读取每一行,并直接打印到控制台。
能够正确地看到文件中的内容,而不会出现乱码。
适配器模式与装饰器模式的区别
装饰器与适配器模式都有一个别名就是包装模式(Wrapper),它们的作用看似都是起到包装一个类或对象的作用,但是使用它们的目的是很不一样的。
- 适配器模式的意义是要将一个接口转变成另外一个接口,它的目的是通过改变接口来达到重复使用的目的;(参考:适配器模式)
- 而装饰器模式不是要改变被装饰对象的接口,而恰恰要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方法而提升性能。所以这两个模式设计的目的是不同的。
搞懂了?那我们玩点带有迷惑型的东西
为什么说Java io类库借鉴了装饰模式,而不是等同于装饰模式
我们之前说Java I/O包下的类库借鉴了装饰器模式,
但是它并不完全等同于经典的装饰器模式,
装饰器模式通常允许递归地组合装饰器对象,而Java I/O中的
FilterInputStream
通常只包装一个InputStream
对象。如果我们区分这一差别,他们实际上是不同的;
严格意义上装饰器模式是可以对一个对象进行多次装饰,之后依旧是原类型,即允许递归的组合装饰器对象。
而java io包中类库却不是多次装饰。
以下进一步分析,下图是之前的类图,如果有必要可以回到之前的代码示例
那java io类库不是使用的装饰器模式么?大家不都说java io类库使用的是装饰器模式么?
其实准确的说java io类库借鉴了装饰器模式比较合理些。
从上面的类图或者回到之前代码示例1,FilterInputStream
的构造方法是protected
修饰的,我们无法直接通过构造函数构造他的实例,因此直接使用它的子类构造,如这里的BufferedInputStream
。
通过传入文件路径字符串
构造了FileInputStream
对象,通过该FileInputStream
实例构造了一个BufferedInputStream
实例,BufferedInputStream
可以看作是对该InputStream
装饰了一层,即增加缓冲功能,就完事了,没有后续的装饰了;
而严格意义上的装饰器模式呢,是允许装饰多层的,装饰完毕依旧是原始的类型。
如果我们区分这一差别,即允许递归地组合装饰器对象(装饰完之后还是之前的类型才对,不能改变类型,不是去构造一个新的类型的实例),java io类库就不符合装饰器模式的要求
也就是如果按照严格意义上来说,对InputStream
对象装饰多次,最终依旧是InputStream
对象,才算是装饰器模式。
显然这里不是,这里经过了String =1=> InputStream(FileInputStream) =2=> InputStream(BufferedInputStream)
第1步之后就不再是String
类型了。只是这里形式上看上去和递归地组合装饰器对象非常相像,new BufferedInputStream(new FileInputStream("path_of_file"))
。
因此这里说是借鉴装饰器模式,且借鉴装饰器模式的部分只就是InputStream(FileInputStream) => InputStream(BufferedInputStream)
这个过程。
略微延伸一点,我们打个比方,
java io类库由扩展了,增加了一个XxxInputStream
类,以实现对InputStream
增加Xxx功能,它需要传入InputStream
类型实例来构造,那么它可能的调用方式是这样的:
InputStream fis = new FileInputStream("path_of_file");
XxxInputStream xis = new XxxInputStream(new BufferedInputStream(fis));
它允许递归地组合装饰器对象,这样他就是严格的装饰器模式了。
如果你说,上面代码示例2,形式上很像递归的组合装饰器对象呐,
InputStream fis = new FileInputStream(exampleFile);
BufferedReader br = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8));
但是实际不是。
即使解决乱码问题,但是那是又引入了另外的接口Reader
,与InputStream
不是一回事,构造InputStreamReader
的实例,通过传入FileInputStream
实例(也就是一个InputStream
实例),就转换成了Reader
类型,就不再是InputStream
类型,然后,通过传入该InputStreamReader
,并指定编码格式UTF-8
,构造了一个带有缓冲功能的BufferedReader
,这里可以看简单看作是BufferedReader
对InputStreamReader
装饰了一层,即缓冲功能,就完事了,没有后续的装饰了,
类比
你大概也发现了,通过上面的比较较真的分析之后,我们对装饰器模式有了更深的认识,就是不管装饰多少次,类型不要发生变化才是装饰器模式。
并不是说只要像这样调用new C(new B(new A()))
形似递归的组合装饰器,就是装饰器模式,只要其中发生了类型变化就不再是严格意义上的装饰器模式了。
可以用穿衣服出门类比:
当前的java io类库中的设计可以视为借鉴了装饰器模式,可以看作只包装一层的装饰器模式,就好比你
只穿外套
就出门了;
而严格的讲,**经典的装饰器模式(允许递归地组合装饰器对象)**是说你穿上内裤
,穿上袜子
,穿上衬衫
,穿上西服
,打上领带
,这每一步操作都是对你
(一个对象,人类的一个实例,即一个人类对象)的装饰
,
即穿上内裤
之后是你,
穿上袜子
是你,
穿上衬衫
是你,
穿上西服
是你,
打上领带
是你。
你
就是你
,是不一样的烟火,对你
进行任何的装饰
之后依旧是那个你
,归来依旧是那个少年!
拓展内容
抽象类相关基础
这部分内容属于总结,不需要死记硬背,用的多了回过头来看都是很简单的内容。
-
抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类
这里提一下:不考虑默认方法(使用
default
关键字定义)和静态方法(使用static
关键字定义)情况下,接口中的方法都是抽象方法。在Java 8及之前的版本中,接口中只能包含抽象方法和常量(即静态且final的变量)
public interface MyInterface { // 抽象方法 void method1(); // Java 8及以后:默认方法 default void method2() { System.out.println("Default implementation of method2"); } // Java 8及以后:静态方法 static void method3() { System.out.println("Static method in interface"); } }
-
构造方法,类方法(用static修饰的方法)不能声明为抽象方法
-
抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类
-
实例化
- 抽象类和接口均不能实例化,但是可以指向(引用)具体实现(非抽象子类)
- 抽象类虽然自身不可以实例化,但是其子类覆盖了所有的抽象方法后,是可以实例化的,所以抽象类的构造函数,适用于给其子类对象进行初始化的
-
构造器:构造函数是对象的基本,没有构造函数就没有对象。
-
若在抽象类中显式的声明了有参构造器,子类继承时就必须写一个构造函数来调用父类的有参构造器;
-
具体子类继承抽象父类,父类的有参构造器子类必须显示调用,即在子类的构造器中显式地
super(param_object)
调用 -
这是因为
构造函数用于构造对象,但是抽象类不能被实例化,
一旦抽象类声明了有参构造器,而此构造器不能直接被调用(除非子类显式调用),就不能被客户端将参数直接通过调用该抽象类的有参构造器传参进去,
这个参数对象只能通过具体子类在构造对象时传入,即子类构造器中必须要显式地调用父类有参构造器super(param_object)
。 -
构造器参数类型可以放大,这样会更通用些,比如你要传入某个实现类的实例,可以将构造器参数类型设置为接口类型,这是多态的一种表现,你可以传入任何属于该类型或该类型的子类型对象。
-
如果父类中有
无参构造器
,在子类中可以不写构造器或显式的调用父类的构造函器,Java会自动调用父类无参构造器。以下代码是对抽象类构造器部分解释的编码。
abstract class Parent{ public Parent(Param param){ } public abstract void m(); } class Son extends Parent{ // param type can be larger. public Son(Param param) { // must invoke manually. super(param); } @Override public void m() { } } interface Param{ } class ParamTypeA implements Param{ } public class Demo { public static void main(String[] args) { Param param = new ParamTypeA(); Son son = new Son(param); } }
-