装饰模式
1、 问题引入
要实现一个简单的个人形象系统,使用控制台输出的形式,简单说明搭配着装
Person
package com.shier.decorate; /** * @author Shier * CreateTime 2023/4/16 22:30 */ public class Person { private String name; public Person(String name) { this.name = name; } public void wearTShirts() { System.out.print("大T恤"); } public void wearBigTrouser() { System.out.print("垮裤"); } public void wearSneakers() { System.out.print("球鞋"); } public void wearSuit() { System.out.print("西装"); } public void wearTie() { System.out.print("领带"); } public void wearLeatherShoes() { System.out.print("皮鞋"); } public void wearBdk() { System.out.print("背带裤"); } public void show() { System.out.println(name + "的装扮"); } }
测试类:
/** * @author Shier * CreateTime 2023/4/16 22:35 */ public class DecorateTest { public static void main(String[] args) { Person person = new Person("Ikun"); person.show(); System.out.print("第一套装扮:"); person.wearBigTrouser(); person.wearBdk(); System.out.println(); System.out.print("第二套装扮:"); person.wearTie(); person.wearSuit(); person.wearLeatherShoes(); } }
输出结果:
问题来了:新的需求出现就要修改原来的
Person
类,这样就违背了开闭原则
2、 改进程序
修改后的程序结构图如下:
将服饰抽象成一个类,让那么其他服饰类去继承这个服饰抽象类
Person类:
/** * @author Shier * CreateTime 2023/4/16 22:30 */ public class Person { private String name; public Person(String name) { this.name = name; } public void show() { System.out.println(name + "的装扮"); } }
服饰抽象类:
/** * @author Shier * CreateTime 2023/4/16 22:46 */ public abstract class Finery { public abstract void show(); }
Bdk 类
/** * @author Shier * CreateTime 2023/4/16 22:47 */ public class Bdk extends Finery{ @Override public void show() { System.out.print("背带裤"); } }
其他的服饰类一样,在此不多写了,都是重复的
客户端如下:
public class DecorateTest { public static void main(String[] args) { Person person = new Person("Ikun"); person.show(); System.out.print("第一套装扮:"); Finery tShirts = new TShirts(); Finery bigTrouser = new BigTrouser(); Finery sneakers = new Sneakers(); tShirts.show(); bigTrouser.show(); sneakers.show(); System.out.println(); System.out.print("第二套装扮:"); Suit suit = new Suit(); Bdk bdk = new Bdk(); suit.show(); bdk.show(); } }
输出的结构如下:
如上的程序还是有问题的,
什么问题呢?你看在客户端处都在调用很多的show方法,重复代码过多,最重要的是这些show执行的顺序。
那么我们就需要把所需的功能按正确的顺序串联起来进行控制 ,下面就用装饰模式去修改它
3、装饰模式
装饰模式(Decorator Pattern)是一种结构型设计模式。它允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式是作为现有的类的一个包装,创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。装饰器模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。装饰器模式针对的问题是动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式相比生成子类更为灵活。装饰模式不改变接口的前提下,增强所考虑的类的性能。总之,装饰模式可以让我们动态地给一个对象添加一些额外的职责,而不需要在继承体系中创造许多子类。可以看做是一种对继承的补充或者替代方案。
3.1 装饰模式的结构图
- Component:定义一个对象接口,可以给这些对象动态地添加职责
ConcreteComponent:定义了一个具体的对象,也可以给这个对象添加一些职责
Decorator:装饰抽象类,继承了Component,从外类来扩展Component类的功能,但对于Component来说,是无须知道Decorator的存在的
ConcreteDecorator:具体的装饰对象,起到给Component添加职责的功能
如果只有一个ConcreteComponent类而没有抽象的Component类,那么Decorator类可以是ConcreteComponent的一个子类。同样道理,如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。
3.2 原理
装饰模式是利用SetComponent来对对象进行包装,每个装饰对象的实现就和如何使用这个对象分离开了,每个装饰对象只关心自己的功能,不需要关心如何被添加到对象链当中。
在装饰模式中引入了装饰类,装饰类和被装饰类都要实现同一个接口或者继承同一个父类。具体来说,装饰类继承自抽象构件类,并且包含一个指向具体构件对象的引用,它包含了一个与抽象构件类相同的接口,即在装饰模式中,所有的装饰器都需要实现被装饰者实现的接口。因此客户端可以针对抽象构件角色进行编程,而不需要关心具体构件的类型。
装饰模式通过组合的方式,将装饰类和被装饰类进行组合,从而实现动态地添加新的功能。在运行时,装饰类动态地为被装饰类添加新的行为或者修改原有的行为,这样就可以在不修改现有代码的情况下,对需要修改的代码进行扩展和修改。这种方式比继承更加灵活和可控,避免了类爆炸问题,同时也实现了代码的复用。
3.3 使用装饰模式改进以上程序
再次修改的代码结构UML图如下:
ICharactor接口(Component)
/** * @author Shier * CreateTime 2023/4/16 23:08 * 人物形象接口 */ public interface ICharacter { public void show(); }
Person类(ConcreteComponent)
/** * @author Shier * CreateTime 2023/4/16 22:30 */ public class Person implements ICharacter{ private String name; public Person(String name) { this.name = name; } public void show() { System.out.println(name + "的装扮"); } }
Finery类(Decorator):
/** * @author Shier * CreateTime 2023/4/16 22:46 */ public class Finery implements ICharacter { private ICharacter character; public void decorate(ICharacter iCharacter) { this.character = iCharacter; } public void show(){ if (this.character !=null){ this.character.show(); } } }
具体服饰类(ConcreteDecorator):
/** * @author Shier * CreateTime 2023/4/16 22:47 */ public class Bdk extends Finery { @Override public void show() { System.out.print("背带裤"); super.show(); } }
每一个服饰类都一样的,不再过多写出来
测试类如下:
/** * @author Shier * CreateTime 2023/4/16 22:35 */ public class DecorateTest { public static void main(String[] args) { Person kun = new Person("Ikun"); System.out.print("第一套装扮:"); // 穿球鞋的Ikun Sneakers sneakers = new Sneakers(); sneakers.decorate(kun); // 在穿球鞋的Ikun基础上,再给他穿一条背带裤 Bdk bdk = new Bdk(); bdk.decorate(sneakers); bdk.show(); System.out.println(); System.out.print("第二套装扮:"); // 穿西装的ikun Suit suit = new Suit(); suit.decorate(kun); // 在穿西装的ikun基础上再传一个皮鞋 LeatherShoes leatherShoes = new LeatherShoes(); leatherShoes.decorate(suit); // 在穿西装和皮鞋的基础上带零领带 Tie tie = new Tie(); tie.decorate(leatherShoes); tie.show(); } }
测试结构如下:
到此就实现了装饰模式去打扮ikun
如果还有新的需求过来,比如说ikun现在向买一个草帽带一下,只要增加一个草帽类,再去测试类当中给ikun穿上。ikun很满意
草帽类如下:
/** * @author Shier * CreateTime 2023/4/16 23:23 */ public class Strawhat extends Finery { public void show() { System.out.print("草帽"); super.show(); } }
测试类:
测试结果如下:
4、商场收银程序升级
装饰模式有一个重要的优点,把类中的装饰功能从类中搬移去除,这样可以简化原有的类。
所有商场系统的UML如改成如下:
IISale接口(Component)
/** * @author Shier * CreateTime 2023/4/16 23:32 */ public interface ISale { public double acceptCash(double price, int num); }
CashSuper原来是抽象类,改成普通类,但实现ISale接口。
/** * @author Shier * CreateTime 2023/4/10 21:51 */ public class CashSuper implements ISale { private ISale sale; // 装饰对象 public void decorate(ISale iSale) { this.sale = iSale; } public double acceptCash(double price, int num) { double result = 0d; if (this.sale != null) { // 若装饰对象存在,则执行装饰对象的算法 result = this.sale.acceptCash(price, num); } return result; } }
CashNormal,相当于ConcreteComponent,是最基本的功能实现,也就是单价×数量的原价算法。
/** * @author Shier * CreateTime 2023/4/10 21:54 * * 正常收费,原价返回 */ public class CashNormal implements ISale { // 原价 public double acceptCash(double price,int num){ return price * num; } }
另外两个CashSuper的子类算法,都在计算后,再增加一个super.acceptCash(result, 1)返回。
public class CashRebate extends CashSuper { private double moneyRebate = 1d; //初始化时必需输入折扣率。八折就输入0.8 public CashRebate(double moneyRebate) { this.moneyRebate = moneyRebate; } //计算收费时需要在原价基础上乘以折扣率 public double acceptCash(double price, int num) { double result = price * num * this.moneyRebate; return super.acceptCash(result, 1); } }
重点在CashContext类,因为涉及组合算法,所以用装饰模式的方式进行包装,这里需要注意包装的顺序,先打折后满多少返多少,与先满多少返多少,再打折会得到完全不同的结果。
/** * @author Shier * CreateTime 2023/4/11 22:55 */ public class CashContext { /** * CashSuper对象 */ private ISale cashSuper; /** * 通过构造方法,传入具体的收费策略 */ public CashContext(int cashType) { //根据用户输入,将对应的策略对象作为参数传入CashContent对象中 switch (cashType) { case 1: cashSuper = new CashNormal(); break; case 2: cashSuper = new CashRebate(0.8d); break; case 3: cashSuper = new CashRebate(0.7d); break; case 4: cashSuper = new CashReturn(300d, 100d); break; case 5: // 先打8折,再满300反100 CashNormal cn = new CashNormal(); CashReturn cr = new CashReturn(300d, 100d); CashRebate cr2 = new CashRebate(0.8d); cr.decorate(cn); cr2.decorate(cr); this.cashSuper = cr2; break; case 6: // 先满200反50,再打七折 CashNormal cn2 = new CashNormal(); CashRebate cr3 = new CashRebate(0.7d); CashReturn cr4 = new CashReturn(200d, 40d); cr3.decorate(cn2); cr4.decorate(cr3); this.cashSuper = cr4; break; default: break; } } /** * 根据不同的收费策略返回不同的结构 */ public double getResult(double price, int num) { return cashSuper.acceptCash(price, num); } }
客户端不用改变
最终测试结果如下:
5、装饰模式总结
优点
装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。
可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。
缺点:
使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
适用环境:
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类)
用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
4. 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。
缺点:
使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
适用环境:
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类)