装饰器模式
动机
在某些情况下我们可能会“过度地使用继承来扩展对象的功能”,
由于继承为类型引入的静态特质,使得这种扩展方式缺乏灵活性;
并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展
功能的组合)会导致更多子类的膨胀。
如何使“对象功能的扩展”能够根据需要来动态地实现?同时避
免“扩展功能的增多”带来的子类膨胀问题?从而使得任何“功能
扩展变化”所导致的影响将为最低?
定义
动态(组合)地给一个对象增加一些额外的职责。就增加功
能而言,Decorator模式比生成子类(继承)更为灵活(消
除重复代码 & 减少子类个数)。
重构到模式
【需求】奖金计算体系
某业务部门通过调整奖金的计算方式来激励士气;奖金计算体系如下:
- 每个人当月的业务奖金 = 当月的销售额*3%
- 每个人累计奖金 = 总的回款额*0.1%
- 团队奖金 = 团队总销售额*1%
不用模式的解决方案
上述代码逻辑很简单,仔细想想有什么问题?
- 对于奖金计算方式复杂罢了,不过是实现起来会困难点,相对而言还是比较好解决的。
- 最痛苦的是这些奖金的计算方式经常发生变化。几乎每个季度都要小调整,每年都要大调整,这就要求软件的实现要足够灵活,要能够很快进行
相应的调整和修改,否则就不能满足实际业务的需要。
举个例子说明,现在根据业务需要,增加一个"环比增长奖金"。过了两个月,业务奖励政策发生了变化,不再需要这个奖金了,或者换了一个新的
奖金方式,那么软件就要把这个功能从软件中去掉,然后再实现新的功能。
那么上面的要求该怎么实现呢?
方案1.通过继承来扩展功能
方案2.在计算奖金对象Prize类中增加或者删除代码,这个方案违背了开-闭原则
还有一个问题就是就是在运行期间,不同人员参与的奖金计算方式也不同。比如业务经理,除了计算个人奖金外,还要参加团队奖金的计算,这就
意味着需要在运行期间动态组合需要计算的部分,也就会有一大堆的if-else判断。
总结:奖金计算面临如下问题:
- 计算逻辑复杂
- 要有足够的灵活性,可以方便的增加或减少功能
- 要能动态的组合计算方式,不同的人参与不同的运算
把上面的问题抽象一下,假设若有一个计算奖金的对象,现在需要能够灵活的给它增加和减少功能,还需要能够动态组合功能,每个功能就相当于
在计算奖金的某个部分。
现在的问题就是,如何才能够透明地给一个对象增加功能,并实现功能的动态组合?
装饰模式模板代码
模板代码如下:
步骤1.组件对象的接口定义:
public abstract class Component {
public abstract void operation();
}
步骤2.具体实现组件对象的示意:
public class ConcreteComponent extends Component {
@Override
public void operation() {
// 响应功能处理
}
}
步骤3.创建抽象的装饰器对象
public abstract class Decorator extends Component{
// 持有组件对象
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
// 转发请求给组件对象,可以在转发前后执行一些附加动作
component.operation();
}
}
步骤4.添加两个具体的装饰器对象,一个示意添加状态,一个示意添加职责
public class ConcreteDecoratorA extends Decorator{
private String addedState;
public String getAddedState() {
return addedState;
}
public void setAddedState(String addedState) {
this.addedState = addedState;
}
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operation() {
// 调用父类的方法,可以在调用前后执行一些附加动作
// 在这里进行处理的时候,可以使用添加的状态
super.operation();
}
}
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
private void addedBehavior() {
// 需要添加的职责实现
}
@Override
public void operation() {
// 调用父类的方法,可以在调用前后执行一些附加动作
super.operation();
addedBehavior();
}
}
上述代码,只是一个装饰器代码的模板代码,需要注意的是抽象装饰器类Decorator类的代码:继承了Component同时又组合了Component对象
public abstract class Decorator extends Component{
// 持有组件对象
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
// 转发请求给组件对象,可以在转发前后执行一些附加动作
component.operation();
}
}
装饰器模式重写示例
装饰器模式重写上面的案例,大致会有以下变化:
- 需要定义一个组件对象接口,在这个接口中定义奖金的计算方法,因为外部就是使用这个接口来操作装饰器模式构成的对象结构中的对象
- 需要添加一个基本的实现组件接口的对象,可以让它返回奖金为0。
- 把各个计算奖金规则当做装饰器对象,需要为它们定义一个统一的抽象的装饰器对象,方便约束各个具体的装饰器的接口
- 把各个计算奖金的规则实现成为具体的装饰器对象
下面看看整体的结构调整,以便于整体理解和把握
步骤1.创建计算奖金的组件接口和基本的实现对象
public abstract class Component {
/***
* 计算某人在某段时间内的奖金,有些参数在演示中并不会使用,
* @param user 被计算奖金人员
* @param begin 计算奖金开始时间
* @param end 计算奖金结束时间
* @return
*/
public abstract double calcPrize(String user, Date begin, Date end);
}
public class ConcreteComponent extends Component {
@Override
public double calcPrize(String user, Date begin, Date end) {
// 只是一个默认的实现,默认没有奖金
return 0;
}
}
步骤2.创建抽象的装饰器,需要和被装饰的对象实现同样的接口
public abstract class Decorator extends Component{
// 持有被装饰的组件的对象
protected Component component;
// 通过构造函数注入component对象
public Decorator(Component component) {
this.component = component;
}
public double calcPrize(String user, Date begin, Date end){
// 转调组件对象的方法
return component.calcPrize(user,begin,end);
}
}
步骤3.创建各个装饰器对象
public class GroupPrizeDecorator extends Decorator{
public GroupPrizeDecorator(Component component) {
super(component);
}
@Override
public double calcPrize(String user, Date begin, Date end) {
//1.先获取前面运算出来的奖金
double money = super.calcPrize(user, begin, end);
double group = 0.0;
for (double d:DbHolder.mapMonthSaleMoney.values()) {
group += d;
}
double prize = group * 0.01;
System.out.println(user + "当月团队业务奖金:"+prize);
return money + prize;
}
}
public class MonthPrizeDecorator extends Decorator{
public MonthPrizeDecorator(Component component) {
super(component);
}
@Override
public double calcPrize(String user, Date begin, Date end) {
//1.先获取前面运算出来的奖金
double money = super.calcPrize(user, begin, end);
//2.然后计算当月业务奖金,按人员和日期去获取当月业务额,然后乘以3%
double prize = DbHolder.mapMonthSaleMoney.get(user) * 0.03;
System.out.println(user+"当月业务奖金:"+prize);
return money+prize;
}
}
public class SumPrizeDecorator extends Decorator {
public SumPrizeDecorator(Component component) {
super(component);
}
@Override
public double calcPrize(String user, Date begin, Date end) {
// 1.先获取前面运算出来的奖金
double money = super.calcPrize(user,begin,end);
// 简单演示,假定大家的累计业务额都是1000000元
double prize = 1000000.00*0.001;
System.out.println(user + "累计奖金:"+prize);
return money+prize;
}
}
步骤4.创建客户端测试
public class App {
public static void main(String[] args) {
// 先创建计算基本奖金的类,这也是被装饰的对象
Component c1 = new ConcreteComponent();
// 然后对计算的基本奖金进行装饰,这里要组合各个装饰
// 说明,各个装饰者之间最好不要有先后顺序的限制,也就是先装饰谁后装饰谁都应该是一样的
Decorator decorator1 = new MonthPrizeDecorator(c1);
Decorator decorator2 = new SumPrizeDecorator(decorator1);
// 注意:这里只需使用最后组合好的对象去调用业务方法即可,会依次调用回去
// 目前日期对象没有用上,传null即可
double zs = decorator2.calcPrize("张三",null,null);
System.out.println("--------张三应得奖金:"+zs);
double ls = decorator2.calcPrize("李四",null,null);
System.out.println("--------李四应得奖金:"+ls);
Decorator decorator3 = new GroupPrizeDecorator(decorator2);
double ww = decorator3.calcPrize("王五",null,null);
System.out.println("--------王五应得奖金:"+ww);
}
}
运行日志:
张三当月业务奖金:300.0
张三累计奖金:1000.0
--------张三应得奖金:1300.0
李四当月业务奖金:600.0
李四累计奖金:1000.0
--------李四应得奖金:1600.0
王五当月业务奖金:900.0
王五累计奖金:1000.0
王五当月团队业务奖金:600.0
--------王五应得奖金:2500.0
类继承方式设计带来的问题
【需求】假如我们需要为游戏中开发一种坦克,除了各种不同型号的坦克外,我们还希望在不同场合中为其增加以下一种或多种功能;
比如红外线夜视功能,比如水陆两栖功能,比如卫星定位功能等等。
按照继承的作法如下:
步骤1.创建抽象型Tank
public abstract class Tank {
public abstract void shot();
public abstract void run();
}
步骤2.创建各种型号Tank
public class T50 extends Tank{
@Override
public void shot() {
System.out.println("T50坦克平均每秒射击5发子弹");
}
@Override
public void run() {
System.out.println("T50坦克平均每时运行30公里");
}
}
public class T75 extends Tank{
@Override
public void shot() {
System.out.println("T75坦克平均每秒射击10发子弹");
}
@Override
public void run() {
System.out.println("T75坦克平均每时运行35公里");
}
}
public class T90 extends Tank{
@Override
public void shot() {
System.out.println("T90坦克平均每秒射击20发子弹");
}
@Override
public void run() {
System.out.println("T90坦克平均每时运行40公里");
}
}
步骤3.各种不同功能的组合:比如IA具有红外功能接口、IB具有水陆两栖功能接口、IC具有卫星定位功能接口。我们以T50型号坦克为例:
a.创建具有红外功能T50坦克 T50A类
public class T50A extends T50 implements IA {
@Override
public void ia() {
System.out.println("具有红外功能T50坦克");
}
}
b.创建具有水陆两栖功能T50坦克 T50B类
public class T50B extends T50 implements IB {
@Override
public void ib() {
System.out.println("具有水陆两栖功能T50坦克");
}
}
c.创建具有卫星定位功能T50坦克 T50C类
public class T50C implements IC {
@Override
public void ic() {
System.out.println("具有卫星定位功能T50坦克");
}
}
d.创建具有红外功能和水陆两栖功能T50坦克 T50AB类
public class T50AB extends T50 implements IA,IB {
@Override
public void ia() {
System.out.println("具有红外功能T50坦克");
}
@Override
public void ib() {
System.out.println("具有水陆两栖功能T50坦克");
}
}
e.创建具有红外功能和卫星定位功能T50坦克 T50AC类
public class T50AC extends T50 implements IA,IC {
@Override
public void ia() {
System.out.println("具有红外功能T50坦克");
}
@Override
public void ic() {
System.out.println("具有卫星定位功能T50坦克");
}
}
f.创建具有水陆两栖功能和卫星定位功能T50坦克 T50BC类
public class T50BC extends T50 implements IB,IC {
@Override
public void ib() {
System.out.println("具有水陆两栖功能T50坦克");
}
@Override
public void ic() {
System.out.println("具有卫星定位功能T50坦克");
}
}
g.创建具有红外功能和水陆两栖功能和卫星定位功能T50坦克 T50ABC类
public class T50ABC extends T50 implements IA,IB,IC {
@Override
public void ia() {
System.out.println("具有红外功能T50坦克");
}
@Override
public void ib() {
System.out.println("具有水陆两栖功能T50坦克");
}
@Override
public void ic() {
System.out.println("具有卫星定位功能T50坦克");
}
}
上述代码我只是以T50型坦克为例,各种功能接口组合在一起,产生了这个多的类,由此可见,如果用继承实现,子类会爆炸式地增长。
在这个案例中我们“过度地使用了继承来扩展对象的功能”,由于继承为类型引入的静态物质,使得这种扩展方式缺乏灵活性;
并且随着子类的增多(扩展功能的增多),各种子类的组合(扩展功能组合)会导致更多子类的膨胀(多继承)。
本质
装饰器模式的本质:动态组合
JAVA中装饰模式应用
在java中典型的装饰模式的应用(I/O),简单回忆一下流操作,示意代码如下:
public class IoTest {
public static void main(String[] args) {
// 流式读取文件
DataInputStream din = null;
try {
// 装饰器模式具体体现
din = new DataInputStream(new BufferedInputStream(new FileInputStream("file.txt")));
// 然后就可以读取获取文件中的内容
byte[] bs = new byte[din.available()];
din.read(bs);
String content = new String(bs);
System.out.println("文件内容===="+content);
}catch (Exception e) {
e.printStackTrace();
}finally {
try {
din.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
既然I/O流部分是采用装饰器模式实现,如果我们想自己添加新的功能,只需要实现新的装饰器,然后在使用的时候组合进去就行了,也就是说
我们可以自定义一个装饰器,然后和JDK中已有的流的装饰器一起使用。能行吗?
动手实现I/O流的装饰器-第一版
实现对文件流的简单加密代码如下:
public class EncryptOutputStream extends OutputStream{
private OutputStream outputStream;
public EncryptOutputStream(OutputStream outputStream) {
this.outputStream = outputStream;
}
@Override
public void write(int a) throws IOException {
// 先统一向后移动两位
a = a+2;
// 97是小写的a的码值
if (a >= (97+26)) {
a= a-26;
}
this.outputStream.write(a);
}
}
public class App {
public static void main(String[] args) throws Exception{
// 流式输出文件
DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(new EncryptOutputStream(new FileOutputStream("decorator_file.txt"))));
// 输出内容
dout.write("abcdxyz".getBytes());
dout.close();
}
}
运行日志如下:
cdefzab
很好,文件按照我们的简单的加密算法被加密了,再试试看,不是说装饰器可随意组合吗?换一个组合方式看看,比如把BufferedOutputStream
和我们自己的装饰器在组合的时候换个位置,示例代码如下:
public class App {
public static void main(String[] args) throws Exception{
// 流式输出文件
// DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(new EncryptOutputStream(new FileOutputStream("decorator_file.txt"))));
// 装饰器随意组合测试
DataOutputStream dout = new DataOutputStream(new EncryptOutputStream(new BufferedOutputStream(new FileOutputStream("decorator_file.txt"))));
// 输出内容
dout.write("abcdxyz".getBytes());
dout.close();
}
}
查看运行日志发现文件中什么都没有生成。
源码分析:
当执行到"dout.write(“abcdxyz”.getBytes())"这句话的时候,会调用DataOutputStream的write方法,把数据输出到EncryptOutputStream
中,EncryptOutputStream流也就是我们自己实现的流,没有缓存,经过处理后继续输出,把数据输出到BufferedOutputStream流中;由于
BufferedOutputStream流是带一个缓存流,它默认缓存8192字节,也就是说默认流中的缓存数据到了8192字节,它才会自动输出缓存中的数据;
而目前要输出的字节肯定不到8192字节,因此数据被缓存在BufferedOutputStream流中了,而不会自动输出。
当执行到"dout.close()"这句话的时候会调用关闭DataOutputStream流,这会转调到传入DataOutputStream流中的close方法,也就是
EncryptOutputStream的close方法,而EncryptOutputStream的close方法继承自OutputStream,在OutputStream的close方法实现中,是个
空方法,什么都没有做。因此,这种实现方式没有flush流的数据,也就不会输出文件内容。
动手实现I/O流的装饰器-第二版
要让我们写的装饰器和其他java中的装饰器一样使用,最合理方案就是:让我们的装饰器继承装饰器的父类,也就是FilterOutputStream类
public class EncryptOutputStream extends FilterOutputStream {
public EncryptOutputStream(OutputStream outputStream) {
super(outputStream);
}
@Override
public void write(int a) throws IOException {
// 先统一向后移动两位
a = a+2;
// 97是小写的a的码值
if (a >= (97+26)) {
a= a-26;
}
super.write(a);
}
}
装饰器模式与AOP
下面演示一下使用装饰器模式,把一些公共的功能,比如权限的控制、日志的记录等透明地添加到业务功能模块中去,做出类似AOP的效果。
步骤1.定义业务接口和简单的数据模型
public interface GoodsSaleEbi {
public boolean sale(String user,String customer,SaleModel saleModel);
}
public class SaleModel {
private String goods;
private int saleNum;
public String getGoods() {
return goods;
}
public void setGoods(String goods) {
this.goods = goods;
}
public int getSaleNum() {
return saleNum;
}
public void setSaleNum(int saleNum) {
this.saleNum = saleNum;
}
@Override
public String toString() {
return "SaleModel{" +
"goods='" + goods + '\'' +
", saleNum=" + saleNum +
'}';
}
}
步骤2.定义具体业务实现对象、
public class GoodsSaleEbo implements GoodsSaleEbi {
@Override
public boolean sale(String user, String customer, SaleModel saleModel) {
System.out.println(user+"保存了"+customer+"购买"+saleModel+"的销售数据");
return true;
}
}
步骤3.把公共功能定义装饰器,定义抽象的父类
public abstract class Decorator implements GoodsSaleEbi {
protected GoodsSaleEbi ebi;
public Decorator(GoodsSaleEbi ebi) {
this.ebi = ebi;
}
}
步骤4.实现权限控制的装饰器
public class CheckDecorator extends Decorator{
public CheckDecorator(GoodsSaleEbi ebi) {
super(ebi);
}
@Override
public boolean sale(String user, String customer, SaleModel saleModel) {
// 简单点,只让张三执行这个功能
if (!"张三".equals(user)) {
System.out.println("对不起"+user+",你没有保存销售单的权限");
// 就不再调用被装饰对象的功能了
return false;
} else {
return this.ebi.sale(user,customer,saleModel);
}
}
}
public class LogDecorator extends Decorator {
public LogDecorator(GoodsSaleEbi ebi) {
super(ebi);
}
@Override
public boolean sale(String user, String customer, SaleModel saleModel) {
// 执行业务功能
boolean f = this.ebi.sale(user,customer,saleModel);
//在执行业务功能后记录日志
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
System.out.println("日志记录:"+user+"于"+df.format(new Date())+"时保存了一条销售记录,客户是"+customer+",购买记录是"+saleModel);
return f;
}
}
步骤5.测试
public class App {
public static void main(String[] args) {
// 得到业务接口,组合装饰器
GoodsSaleEbi ebi = new CheckDecorator(new LogDecorator(new GoodsSaleEbo()));
// 准备测试数据
SaleModel saleModel = new SaleModel();
saleModel.setGoods("Moto 手机");
saleModel.setSaleNum(2);
// 调用业务功能
ebi.sale("张三","张三丰",saleModel);
ebi.sale("李四","张三丰",saleModel);
}
}
运行日志:
张三保存了张三丰购买SaleModel{goods='Moto 手机', saleNum=2}的销售数据
日志记录:张三于2024-05-07 11:18:05 661时保存了一条销售记录,客户是张三丰,购买记录是SaleModel{goods='Moto 手机', saleNum=2}
对不起李四,你没有保存销售单的权限
要点总结
通过采用组合而非继承的手法, Decorator模式实现了在运行时 动态扩展对象功能的能力,而且可以根据需要扩展多个功能。避免 了使用继承带来的“灵活性差”和“多子类衍生问题”。
Decorator类在接口上表现为is-a Component的继承关系,即 Decorator类继承了Component类所具有的接口。但在实现上又 表现为has-a Component的组合关系,即Decorator类又使用了 另外一个Component类。
Decorator模式的目的并非解决“多子类衍生的多继承”问题, Decorator模式应用的要点在于解决“主体类在多个方向上的扩展 功能”——是为“装饰”的含义。