设计模式 -- 装饰模式
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式 -- 装饰模式相关的知识,希望对你有一定的参考价值。
设计模式 -- 装饰模式(Decorator)
------------------------------------------------
第一部分: 引入装饰模式
1.1 4S店部门奖金的计算
随着某汽车4S店的规模越来越大、效益越来越好,现在4S店的领导为了留住优秀的销售员工和提高员工工作的积极性,决定每个月都根据员工的工作情况来发放奖金,具体的发放规则如下:
1.每个人当月的业务奖金 = 当月销售额 * 3%
2.每个人累计奖金 = 总的回款额 * 1%
3.团队奖金 = 团队总销售额 * 1%
(PS:我们只是关注设计模式的学习,对于奖金的计算方法进行了最大程度上的简化.)
1.2不用模式的解决方案
首先思考一下,每位销售人员的奖金由多个部分的奖金组成,只需要把每一部分的奖金计算出来然后算一个总和就能得到每位销售人员的奖金总和.
人员构成:
销售经理:小王
销售代表:张三
销售代表:李四
本月销售额、总的回款额分别为:
王五 40000元、 20000元
张三 30000元、 10000元
李四 20000元、 8000元
为了演示方便,我们先准备点测试数据,在内存中模拟数据库
import java.util.HashMap; import java.util.Map; /** * 模拟数据 * @author Administrator * */ public class TestDB { //记录每个人的月度销售额 public static Map<String,Double> monthSaleMoney = new HashMap<String,Double>(); //记录每个人的月度总的回款额 public static Map<String,Double> backSaleMoney = new HashMap<String,Double>(); private TestDB(){ } static{ monthSaleMoney.put("王五", 40000.0); monthSaleMoney.put("张三", 30000.0); monthSaleMoney.put("李四", 20000.0); backSaleMoney.put("王五", 20000.0); backSaleMoney.put("张三", 10000.0); backSaleMoney.put("李四", 8000.0); } }
按照奖金计算的规则,实现奖金计算
/** * 计算奖金的对象 * @author Administrator * */ public class Prize { /** * 计算某个人每月获取的奖金总额 * @param user * @return */ public double calcPrize(String user){ double money = 0.0; //计算当月业务奖金 money += monthPrize(user); //计算累计奖金 money += sumPrize(user); //计算团队奖金 if(this.isManager(user)){ money += groupPize(user); } return money; } /** * 计算个人当月的业务奖金 * @param user 需要计算奖金的人员 * @return 当月的业务奖金 */ private double monthPrize(String user){ double money = 0.0; money += TestDB.monthSaleMoney.get(user) * 0.03; System.out.println(user + " 当月业务奖金: " + money); return money; } /** * 计算个人当月累计奖金 * @param user * @return */ private double sumPrize(String user){ double money = 0.0; money += TestDB.backSaleMoney.get(user) * 0.01; System.out.println(user + " 当月累计奖金" + money); return money; } /** * 计算当月团队奖金 * @param user * @return */ private double groupPize(String user){ double money = 0.0; for(double d : TestDB.monthSaleMoney.values()){ money += d; } double total = money * 0.01; System.out.println(user + " 当月团队奖金:" + total); return total; } /** * 判断人员是普通人员还是业务经理 * @param user 被判断的人员 * @return true 表示是业务经理, false表示是普通人 */ private boolean isManager(String user){ if("王五".equals(user)){ return true; } return false; } }
写一个客户端来测试一下:
public class Client { public static void main(String[] args) { Prize p = new Prize(); double zs = p.calcPrize("张三"); System.out.println("张三当月奖金总额为:" + zs); System.out.println("------------------"); double ls = p.calcPrize("李四"); System.out.println("李四当月奖金总额为:" + ls); System.out.println("------------------"); double ww = p.calcPrize("王五"); System.out.println("王五当月奖金总额为:" + ww); } }
运行结果:
张三 当月业务奖金: 900.0 张三 当月累计奖金100.0 张三当月奖金总额为:1000.0 ------------------ 李四 当月业务奖金: 600.0 李四 当月累计奖金80.0 李四当月奖金总额为:680.0 ------------------ 王五 当月业务奖金: 1200.0 王五 当月累计奖金200.0 王五 当月团队奖金:900.0 王五当月奖金总额为:2300.0
1.3上面的实现方式有何问题
看了上面的实现就是把每个规则都用程序实现出来,但是想想,如果奖金的计算方式经常发生改动,几乎每个季度都会有小的调整,每年都有大 的调整,这就要求软件的实现要足够灵活,要能够很快的进行相应的调整和修改,否则就不能满足实际业务的需要。
把奖金计算可能遇到的问题总结一下:
1. 计算逻辑的复杂(这个复杂是业务上的复杂是我们不能控制的)
2. 要有足够的灵活性,可以方便的增加和减少功能(程序可以控制)
3. 要能够动态的组合计算方式(程序可以控制)
我们的主要任务就是解决 第2 、 3个问题,把这两个问题抽象一下: 如何才能够透明地给一个对象添加功能,并实现功能的动态组合?
------------------------------------------------
第二部分、解决方案
2.1使用装饰模式来解决问题
装饰模式的定义 : 透明的给一个对象添加功能,并能够实现功能的动态组合.
应用装饰模式来解决问题的思路:
上面的奖金计算业务虽然经过了简化,但是还需要解决的主要问题:透明地给对象增加功能,并实现功能的动态组合,所谓透明地给一个对象增加功能,换句话说就是要给一个对象增加功能,但是不能让这个对象知道,也就是不能去改变这个对象。而实现了给一个对象透明地增加功能,自然就实现了功能的动态组合,比如,原来的对象有A功能,现在透明地给它增加了一个B功能,是不是就相当于动态组合了A和B功能呢
在装饰模式的实现中,为了能够实现和原来使用被装饰对象的代码无缝结合,是通过定义一个抽象类,让这个类实现与被装饰对象相同的接口,然后在具体的实现类中,转调被装饰的对象,在转调的前后添加新的功能,这就实现了给被装饰对象增加功能.
2.2装饰模式的结构和说明
1. Component : 组件对象的接口,可以给这些对象动态地添加职责
2. ConcreteComponent : 具体的组件对象,实现组件对象接口,通常就是被装饰的原始对象,也就是可以给这个对象添加职责
3. Decorator : 所有装饰器的抽象父类,需要定义一个与组件对象一致的接口,并持有一个Component对象,其实就是持有一个被装饰的对象
4. ConcreteDecorator : 实际的装饰器对象,实现具体要向被装饰对象添加的功能.
2.3装饰模式的示例代码
(1)组件对象的接口定义
1 /** 2 * 组件对象的接口,可以给这些对象动态地添加职责 3 * @author Administrator 4 * 5 */ 6 public abstract class Component { 7 public abstract void operation(); 8 }
(2)具体的实现组件对象
/** * 具体实现组件对象接口的对象 * @author Administrator * */ public class ConcreteComponent extends Component { @Override public void operation() { //相应的功能处理 } }
(3)抽象的装饰器对象
1 /** 2 * 装饰器接口,维持一个指向组件对象的接口对象,并定义一个与组件对象一致的接口 3 * @author Administrator 4 * 5 */ 6 public abstract class Decorator extends Component{ 7 8 //持有组件对象 9 private Component compoent; 10 11 /** 12 * 构造方法,传入组件对象 13 * @param compoent 14 */ 15 public Decorator(Component compoent){ 16 this.compoent = compoent; 17 } 18 19 @Override 20 public void operation() { 21 //转发请求给组件对象,可以在转发前后执行一些附近动作 22 compoent.operation(); 23 } 24 }
(4)具体的装饰器实现对象
/** * 装饰器的具体实现对象,向组件对象添加职责 * @author Administrator * */ public class ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component compoent) { super(compoent); } @Override public void operation() { //调用父类的方法,可以在调用前后执行一些附近动作 super.operation(); } }
/** * 装饰器的具体实现对象,向组件对象添加职责 * @author Administrator * */ public class ContextDecoratorB extends Decorator { public ContextDecoratorB(Component compoent) { super(compoent); } @Override public void operation() { //调用父类的方法,可以在调用前后执行一些附近动作 super.operation(); } }
2.4使用装饰模式重写示例
了解了装饰模式的基本知识后,考虑该如何使用装饰模式来重写前面的示例.
要使用装饰模式来重写前面的示例,大致会有以下的改变:
1.需要定义一个组件对象的接口,在接口中定义计算奖金的业务方法,因为外部就是使用这个接口来操作装饰模式构成的对象结构中的对象。
2.需要添加一个基本的实现组件接口的对象,可以让它返回奖金为0就可以了。
3.把各个计算奖金的规则当作装饰器对象,需要为它们定义一个统一的抽象的装饰器对象,方便约束各个具体的装饰器接口
4.把各个计算奖金的规则实现成为具体的装饰器对象
下面看看示例的整体结构:
(1) 计算奖金的组件接口
/** * 计算奖金的组件接口 * @author Administrator * */ public abstract class Component { /** * 计算某个获取的奖金 * @param user * @return */ public abstract double calcPrize(String user); }
(2)基本的实现计算奖金的类
/** * 基本的实现计算奖金的类,也是被装饰器装饰的对象 * @author Administrator * */ public class ConcreteComponent extends Component { @Override public double calcPrize(String user) { //只是一个默认的实现,默认没有奖金 return 0; } }
(3)定义抽象的装饰器
/** * 装饰器的接口,需要和被装饰的对象实现相同的接口 * @author Administrator * */ public abstract class Decorator extends Component { //持有被装饰的组件的对象 protected Component component; /** * 通过构造方法传入被装饰的对象 * @param component 被装饰的对象 */ public Decorator(Component component){ this.component = component; } @Override public double calcPrize(String user) { //转调组件对象的方法 return component.calcPrize(user); } }
(4)当月业务奖金装饰器
/** * 装饰器对象,计算当月业务奖金 * @author Administrator * */ public class MonthPrizeDecorator extends Decorator { public MonthPrizeDecorator(Component component) { super(component); } @Override public double calcPrize(String user) { //先获取前面运算出来的奖金 double money = super.calcPrize(user); //然后计算当月业务奖金,按照人员来计算 double prize = TestDB.monthSaleMoney.get(user) * 0.03; System.out.println(user + "当月的业务奖金为:" + prize); return prize + money; } }
(5)累计奖金装饰器
/** * 累计奖金装饰器 * @author Administrator * */ public class SumPrizeDecorator extends Decorator { public SumPrizeDecorator(Component component) { super(component); } @Override public double calcPrize(String user) { //1.先获取前面运算出来的奖金 double money = super.calcPrize(user); //2.然后计算累计奖金 double prize = TestDB.backSaleMoney.get(user) * 0.01; System.out.println(user + " 累计奖金为:" + prize); return prize + money; } }
(6)团队奖金装饰器
/** * 团队奖金装饰器 * @author Administrator * */ public class GroupPrizeDecorator extends Decorator { public GroupPrizeDecorator(Component component) { super(component); } @Override public double calcPrize(String user) { //1.先获取前面运算出来的奖金 double money = super.calcPrize(user); //2.然后计算当月团队业务奖金 double group = 0.0; for(double d : TestDB.monthSaleMoney.values()){ group += d; } double prize = group * 0.01; System.out.println(user + " 当月团队奖金为: " + prize); return money + prize; } }
(7) 测试类
/** * 使用装饰模式 * @author Administrator * */ public class Client { public static void main(String[] args) { //先创建计算基本奖金的类,这也是被装饰的对象 Component c1 = new ConcreteComponent(); //然后对计算的基本奖金进行装饰,这里要组合各个装饰对象 //说明,各个装饰之间最好是不要有先后顺序的限制 //也就是先装饰谁和后装饰谁都应该是一样的 //先组合普通业务人员的奖金计算 Decorator d1 = new MonthPrizeDecorator(c1); Decorator d2 = new SumPrizeDecorator(d1); double zs = d2.calcPrize("张三"); System.out.println("==========张三应得奖金为:" + zs); double ls = d2.calcPrize("李四"); System.out.println("==========李四应得奖金为: " + ls); //如果是业务奖金,还需要一个计算团队的奖金计算 Decorator d3 = new GroupPrizeDecorator(d2); double ww = d3.calcPrize("王五"); System.out.println("=========王五应得奖金为:" + ww); } }
模拟数据类
/** * 模拟数据 * @author Administrator * */ public class TestDB { //记录每个人的月度销售额 public static Map<String,Double> monthSaleMoney = new HashMap<String,Double>(); //记录每个人的月度总的回款额 public static Map<String,Double> backSaleMoney = new HashMap<String,Double>(); private TestDB(){ } static{ monthSaleMoney.put("王五", 40000.0); monthSaleMoney.put("张三", 30000.0); monthSaleMoney.put("李四", 20000.0); backSaleMoney.put("王五", 20000.0); backSaleMoney.put("张三", 10000.0); backSaleMoney.put("李四", 8000.0); } }
----------------------------------------------
三、第三部分:模式讲解
3.1认识装饰模式
1.装饰模式的功能:
a.能够实现动态的、透明的为一个对象添加功能,当一个对象被装饰过后,从外部使用系统的角度来看,就不再是使用原始的那个对象了,而是使用被一系列的装饰器装饰过后的对象
b.通过不同的动态组合,可以灵活的改变一个对象的功能(只要动态组合的装饰器发生了改变,那么所得到的对象的功能也就发生了改变)
c.装饰器类可以复用,可以给一个对象多次增加同一个装饰器,也可以用同一个装饰器装饰不同的功能
2.对象组合
a. 一个类的功能的扩展方式,可以是通过继承,也可以是通过功能更强大、更灵活的对象组合的方式
b. 在面向对象的设计中,有一条基本的规则就是"尽量使用对象组合,而不是对象继承"来扩展和复用功能.
3.2 Java中的装饰模式应用
装饰模式在Java中最典型的应用,就是I/0流,下面简单示例一下使用流式操作读取文件内容:
import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.FileInputStream; public class IOTest { public static void main(String[] args) throws Exception { //流式读取文件 DataInputStream din = null; try{ din = new DataInputStream(new BufferedInputStream(new FileInputStream("xixi.txt"))); //然后就可以获取文件内容了 byte bs[] = new byte[din.available()]; din.read(bs); String content = new String(bs); System.out.println("文件内容 ===== " + content); } finally{ din.close(); } } }
仔细观察上面的代码,会发现最里层是一个FileInputStream对象,然后把它传递给一个BufferedInputStream对象,经过BufferedInputStream处理,再把处理后的对象传递给了DataInputStream对象进行处理,这个过程其实就是装饰器的组装过程,FileInputStream对象相当于原始的被装饰的对象,而BufferedInputStream对象和DataInputStream对象则相当于装饰器.
下图展示了Java的I/O对象层次图(InputStream部分)
Java的I/O的InputStream部分对象层次图,它的结构和装饰模式的结构几乎是一样的。
1、InputStream就相当于装饰模式中的Component
2、FileInputStream、ObjectInputStream、StringBufferInputStream、ByteArrayInputStream、PipeInputStrea,这些对象是直接继承了InputStream的,这些对象相当于装饰模式中的ConcreteComponent,就是被装饰器修饰的对象.
3、FilterInputStream、BufferedInputStream、LineNumberInputStream和PushbackInputStream就相当于装饰模式中的ConcreteDecorator了,另外FilterInputStream和它的子类对象的构造器都是传入组件InputStream类型,这样就完全符合前面讲述的装饰器的结构了
既然I/O流部分是采用装饰模式实现的,如果我们想要添加新的功能,只需要实现新的装饰器,然后在是要的时候组合进去就可以了,示例代码如下:
来个功能简单点的,实现把英文加密存放吧,就是把英文的字符向后移动两个位置,比如,a变成c,b变成d,以此类推,最后的y变成了a,z变成了b,
import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; public class EncryptOutputStream extends FilterOutputStream { public EncryptOutputStream(OutputStream os) { //调用父类的构造方法 super(os); } public void write(int a) throws IOException{ //先统一向后移动两位 a = a + 2; //97是小写a的码值 if(a >= (97+26)){ //如果大于,表示已经是y或者z了,减去26就回到了a或者b a = a - 26; } //调用父类的方法 super.write(a); } }
import java.io.BufferedOutputStream; import java.io.DataOutputStream; import java.io.FileOutputStream; public class Client { public static void main(String[] args) throws Exception { DataOutputStream dout = new DataOutputStream( new EncryptOutputStream( new BufferedOutputStream( new FileOutputStream("myEncrypy.txt")))); dout.write("abcdxyz".getBytes()); dout.close(); } }
运行结果:
3.3装饰模式和AOP
装饰模式和AOP在思想上有共同之处,下面先简单介绍一下AOP的基础知识
1.什么是AOP --- 面向方面编程
AOP是一种编程范式,提供从另一个角度来考虑程序结构以完善面向对象编程(OOP).
程序开发过程时在各个模块之中,存在一些共性的功能,比如日志管理、事务管理等,如下图:
这个时候,在思考这些共性功能的时候,是从横向来思考问题,与通常面向对象的纵向思考角度不同,很明显,需要有新的解决方案,这个时候AOP站出来了. AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任,比如,事务处理、日志管理、权限控制等,封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性.
3.4装饰模式的优缺点
装饰模式有以下优点:
1.比继承更灵活 : 从为对象添加功能的角度来看,装饰模式比继承更灵活。继承是静态的,而且一旦继承所有子类都有一样的功能。而装饰模式采用把功能分类到每个装饰器当中,然后通过对象组合的方式,在运行时动态地组合功能,每个被装饰的对象最终有哪些功能,是由运行期动态组合的功能来决定的.
2.更容易复用功能 : 装饰模式把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器只实现一个功能,使实现装饰器变得简单,更重要的是这样有利于装饰器功能的复用,可以给一个对象增加多个同样的装饰器,也可以把一个装饰器用来装饰不同的对象,从而实现复用装饰器的功能. 3.简化高层定义 : 装饰模式可以通过组合装饰器的方式,为对象添加任意多的功能。
装饰模式的缺点:会产生很多细粒度对象, 装饰模式是把一系列复杂的功能,分散到每个装饰器当中,一般一个装饰器只实现一个功能,这样会产生很多细粒度的对象,而且功能越复杂,需要的细粒度对象越多.
3.5思考装饰模式
1.装饰模式的本质: 动态组合
动态是手段,组合才是目的。这里的组合有两个意思,一个是动态功能的组合,也就是动态进行装饰的组合;另外一个是指对象组合,通过对象组合来实现为被装饰对象透明地增加功能。
总之,装饰模式是通过把复杂功能简单化、分散化,然后在运行期间,根据需要来动态组合的这样一个模式
以上是关于设计模式 -- 装饰模式的主要内容,如果未能解决你的问题,请参考以下文章