设计模式实战应用之三:装饰者模式
2014-01-03 21:17
483 查看
装饰者模式的定义
装饰者模式是应用最普遍的设计模式之一。伟大的 Java 缔造者们将设计模式的应用发挥到了极致,作为解释型语言的 Java 从诞生到今天始终能够作为最主流与应用最广泛的语言力压其他众多的开发语言,与缔造者们不遗余力地提高其健壮性、高性能是分不开的,而设计模式在其中无疑起了举足轻重的作用。Java API 中对装饰者模式应用的举例:Java I/O 库中 BufferedInputStream、DataInputStream、LineNumberInputStream、PushbackInputStream 等装饰者类对于 InputStream 的封装;JavaEE API 中的 ServletRequestWrapper、HttpServletRequestWrapper 对于 ServletRequest 的封装 (Struts2 中的 StrutsRequestWrapper 对 HttpServletRequestWrapper 进行了继承扩展,对 ServletRequest 进一步封装,也属于装饰者模式的应用之一);Java Swing 中对于装饰者模式的应用更是举不胜数,装饰者模式被誉为构建 Swing 应用要用到的四大设计模式之一 (见下图)。
Gof 把装饰者模式归类到对象结构型模式,《设计模式:可复用面向对象软件的基础》对装饰者模式做出了明确的定义:“Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.” 翻译过来就是:“动态地给一个对象附加额外的职责。装饰者为功能的扩展提供了一个比使用子类继承更加灵活的选择。”
why 装饰者模式?
我们有时想要为一些对象添加一些职责,然后我们又不想影响到整个类。使用继承是扩展职责的一种方法。使用继承,父类的特性可以轻松被多个子类的实例所使用。但继承并不总是能够实现最有弹性和提供最好的维护,因为子类继承来的行为是在编译时静态决定的。所以使用继承来进行功能职责的扩展,并不灵活。
这样说是不是过于官话?好像有点,可能过几天我自己回过头来都看不懂。好吧,那就用举个例子的方式再说一遍。
程序员小白接到了朋友介绍过来的菜农甲老板的一个需求:要有个车以拉菜去卖。
这对于编程熟练工小白小菜一碟,设计类 A,给 A 加上车轮、车身。两天后,甲老板拉着小车走了。
掂着手里鲜红的票子,小白尝到了接私活的甜头,于是主动去菜市场寻找私活的机会。
和大部分程序员不一样,小白同学的沟通能力还不错,他找到了有这方面需求的果农乙老板。
乙老板担心小白的实力,害怕自己的钱会白花,于是叫小白拿个成功案例出来看看。
小白叫来了拉小车的甲老板,乙老板看后还算满意,但乙老板说,自己的果园离菜市场有比较长的一段距离,最好不要每次拉着小车来回跑。
看官,笔者插句话,换作你,你会怎么做呢?你会不会重新去设计一个类,然后重新造轮子、车身继而再想办法满足乙老板的需求呢?
如果你真是这么想的,那看官你的水平笔者有点不太敢恭维了。
我们的小白同学是这么干的。既然乙老板对类 A 还是满意的,只是提出了一些更多的需求 (这就是 Gof 所说的 “给一个对象附加额外的职责” 了),但是这个需求甲老板并不需要,所以不能影响甲老板。那么 A 类完全可以拿来复用,而不需要再去重新设计一个类。这难不倒熟练工小白同学:小白同学新设计了类 B,B 继承自类 A,但小白同学给类 B 多加了一个轮,然后再加了一个脚蹬和链条,于是乙老板的小车完成了。
乙老板很满意。小白同学不仅拿到了应得的报酬,而且在菜市场赢得了很好的口碑。
瓜农丙老板慕名而来,他对乙老板的那种小车比较感兴趣,也想定做一个。但他家在另一个城市,每天自己来回地蹬着小车太累了。于是小白同学的类 C 出来了,与 B 不同的是扩展出来一个电瓶。
因为有了多个成功案例,小白同学的口碑一下子传播开了,不仅菜市场的老板们赶来定制小车,就连另一条街上的家具市场的老板们也陆续来找小白了。有一些老板可以直接用 A、B 或者 C 打发,但更多老板提出了新的需求,于是类 D、E、F ... 先后出炉。
于是小白同学光接私活的收入就已经远远超出了他在公司工作所拿到的薪酬。就在小白同学准备辞职自己单干的时候,发现自己已经有了近百个小车类,除了最初的那个类 A,其他类都具有自己扩展出来的一些新功能。有的类里边的功能还是从其他类里边拷贝过来的,因为被拷贝的那个类所继承的东西不适合现有老板的定制。有时候要修改一个功能,连小白自己都得找半天才能找到需要修改的地方,而且改完以后总是有其他老板先后找上门来,因为他们不需要这种修改,而小白自己改的时候竟然也不知道会影响到哪些小车。
看起来是不是很糟糕呢?看来小白同学使用继承来扩展功能的方法并不是多么高明呢。
把系统设计成一些互相合作的类有一个常见的弊端:需要维护相关对象之间的一致性。为了维护一致性而使各类紧密耦合,大大降低了模块的可复用性,这是我们不愿意看到的。
装饰者模式是将要被扩展功能的对象嵌入到另一个对象中,由这个对象 (后者) 添加职责。我们称这个嵌入的对象为装饰。这个装饰和它所装饰的接口一致,因此它对使用该对象 (被装饰对象) 的客户透明。透明性使得你可以递归地嵌套多个装饰,从而可以添加任意多的功能。
装饰者模式的使用场合
动态、透明地给特定对象添加职责,也就是说,不会影响到其他对象。
用于那些可以随时撤销的职责。
当用子类进行扩展是不切实际的时候。有时需要大量的独立扩展并将导致支持各种组合的子类数量大爆炸 (比如小白的例子)。或者一个类的定义可能是隐藏的是子类无法获取到的。
《关于塞班 S40 非智能手机画布的重构计划》需求
大约 4 或 5 年前,当安卓还只是一个初出茅庐的小伙,当诺基亚的塞班雄踞全球手机市场 60% 份额的时候,作者有幸作为 Key Engineer 参与并最终主导了某家互联网公司的垂直搜索服务的塞班 S40 客户端的开发。
当然,如今塞班已在安卓、iPhone 的崛起中日落西山,就连诺基亚也免不了被收购的命运,但那些程序背后的设计思想却并没有随之没落,甚至愈如浓醇的老酒,愈发散发出迷人的醇香。这不正如我们今天讨论的设计模式么,前人在修炼内功的时候就已经考虑到我们这些后来人了,总结了这么一套内功心法,避免了我们重复走他们走过的错路、弯路。
啰里啰嗦了一大堆,越扯越远。总之就是一个意思,就是要对笔者四年前的一个 Java ME 项目进行重构,虽然那个项目早已随着塞班的没落而灰飞烟灭。
其时公司已有一个 js 写的网页版的地图 (类似于百度地图)。公司的三个核心服务:周边搜索、公交查询和驾车导航需要相应的手机客户端,公司已经有了一个 WAP 版的客户端,现在想也能够提供 Java ME 客户端。当时的诺基亚、索爱、三星等等主流手机都是塞班操作系统,而当时的 S40、S60 就是塞班的主流了,而 Java ME 是 S40、S60 上应用开发的不二之选。
周边搜索大部分是在和用户进行文本交互,以及和服务器进行一些信息交换,这一块用 Kuix 或者 LWUIT 等框架可以轻松实现。而地图部分的绘制则需要用到 javax.microedition.lcdui.Canvas 了。根据业务需求,总共涉及了三个基础画布,分别是 WelcomCanvas (显示欢迎界面)、PmapCanvas (绘制地图) 和 HelpCanvas (显示帮助信息),PmapCanvas 是核心画布类,它封装了各种按键事件 (比如方向键等等)、地图移动、地图缩放等基本地图操作,然后是三个核心业务 (周边搜索、公交查询和驾车导航) 所需的一些画布,比如 BusCanvas (公交线路画布)、BusSearchCanvas (周边公交站点结果)、ChangeCityCanvas (切换城市)、DIYCanvas (手动标注地图)、VisitoronCanvas (自动定位) 等等,因为这些具体业务相关画布大都是在 PmapCanvas 的基础上增加或修改一些操作,所以当时设计的时候它们都继承自 PmapCanvas。
WelcomCanvas 源码:
PmapCanvas 源码:
PmapCanvas 的扩展功能类之一,画公交站点的画布 BusSearchCanvas 源码:
《关于塞班 S40 非智能手机地图画布的重构计划》分析
这个跟小白接私活的故事很像,只是画布类没有小白的小车类那么多,所以继承的缺陷还没有充分表露出来。但是这些子类可复用性太低,因为每个类都有自己新添的功能和继承来的功能,如果你在其他子类中想复用一下,恐怕只有手工拷贝、复制了。更糟糕的是,随着业务的扩展,需求的增加,笔者预言,这些画布到小白的那种混乱的地步也只是时间问题。而把 BusSearchCanvas 等这些扩展类做成装饰者,可以让我们的系统灵活起来,而且复用性得到提高。于是笔者打算这样进行重构:WelcomCanvas 等三个基础画布不动,增加一个抽象的 CanvasDecorator 装饰者类,BusSearchCanvas 等类改为继承 CanvasDecorator。
《关于塞班 S40 非智能手机地图画布的重构计划》类设计
《关于塞班 S40 非智能手机地图画布的重构计划》源码实现
WelcomCanvas 等三个基础画布不变。CanvasDecorator 部分源码如下:
装饰者之一 BusSearchCanvas 部分源码如下:
其他几个 BusCanvas 等装饰者源码重构类似于 BusSearchCanvas,这里不再赘述。
装饰者模式是应用最普遍的设计模式之一。伟大的 Java 缔造者们将设计模式的应用发挥到了极致,作为解释型语言的 Java 从诞生到今天始终能够作为最主流与应用最广泛的语言力压其他众多的开发语言,与缔造者们不遗余力地提高其健壮性、高性能是分不开的,而设计模式在其中无疑起了举足轻重的作用。Java API 中对装饰者模式应用的举例:Java I/O 库中 BufferedInputStream、DataInputStream、LineNumberInputStream、PushbackInputStream 等装饰者类对于 InputStream 的封装;JavaEE API 中的 ServletRequestWrapper、HttpServletRequestWrapper 对于 ServletRequest 的封装 (Struts2 中的 StrutsRequestWrapper 对 HttpServletRequestWrapper 进行了继承扩展,对 ServletRequest 进一步封装,也属于装饰者模式的应用之一);Java Swing 中对于装饰者模式的应用更是举不胜数,装饰者模式被誉为构建 Swing 应用要用到的四大设计模式之一 (见下图)。
Gof 把装饰者模式归类到对象结构型模式,《设计模式:可复用面向对象软件的基础》对装饰者模式做出了明确的定义:“Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.” 翻译过来就是:“动态地给一个对象附加额外的职责。装饰者为功能的扩展提供了一个比使用子类继承更加灵活的选择。”
why 装饰者模式?
我们有时想要为一些对象添加一些职责,然后我们又不想影响到整个类。使用继承是扩展职责的一种方法。使用继承,父类的特性可以轻松被多个子类的实例所使用。但继承并不总是能够实现最有弹性和提供最好的维护,因为子类继承来的行为是在编译时静态决定的。所以使用继承来进行功能职责的扩展,并不灵活。
这样说是不是过于官话?好像有点,可能过几天我自己回过头来都看不懂。好吧,那就用举个例子的方式再说一遍。
程序员小白接到了朋友介绍过来的菜农甲老板的一个需求:要有个车以拉菜去卖。
这对于编程熟练工小白小菜一碟,设计类 A,给 A 加上车轮、车身。两天后,甲老板拉着小车走了。
掂着手里鲜红的票子,小白尝到了接私活的甜头,于是主动去菜市场寻找私活的机会。
和大部分程序员不一样,小白同学的沟通能力还不错,他找到了有这方面需求的果农乙老板。
乙老板担心小白的实力,害怕自己的钱会白花,于是叫小白拿个成功案例出来看看。
小白叫来了拉小车的甲老板,乙老板看后还算满意,但乙老板说,自己的果园离菜市场有比较长的一段距离,最好不要每次拉着小车来回跑。
看官,笔者插句话,换作你,你会怎么做呢?你会不会重新去设计一个类,然后重新造轮子、车身继而再想办法满足乙老板的需求呢?
如果你真是这么想的,那看官你的水平笔者有点不太敢恭维了。
我们的小白同学是这么干的。既然乙老板对类 A 还是满意的,只是提出了一些更多的需求 (这就是 Gof 所说的 “给一个对象附加额外的职责” 了),但是这个需求甲老板并不需要,所以不能影响甲老板。那么 A 类完全可以拿来复用,而不需要再去重新设计一个类。这难不倒熟练工小白同学:小白同学新设计了类 B,B 继承自类 A,但小白同学给类 B 多加了一个轮,然后再加了一个脚蹬和链条,于是乙老板的小车完成了。
乙老板很满意。小白同学不仅拿到了应得的报酬,而且在菜市场赢得了很好的口碑。
瓜农丙老板慕名而来,他对乙老板的那种小车比较感兴趣,也想定做一个。但他家在另一个城市,每天自己来回地蹬着小车太累了。于是小白同学的类 C 出来了,与 B 不同的是扩展出来一个电瓶。
因为有了多个成功案例,小白同学的口碑一下子传播开了,不仅菜市场的老板们赶来定制小车,就连另一条街上的家具市场的老板们也陆续来找小白了。有一些老板可以直接用 A、B 或者 C 打发,但更多老板提出了新的需求,于是类 D、E、F ... 先后出炉。
于是小白同学光接私活的收入就已经远远超出了他在公司工作所拿到的薪酬。就在小白同学准备辞职自己单干的时候,发现自己已经有了近百个小车类,除了最初的那个类 A,其他类都具有自己扩展出来的一些新功能。有的类里边的功能还是从其他类里边拷贝过来的,因为被拷贝的那个类所继承的东西不适合现有老板的定制。有时候要修改一个功能,连小白自己都得找半天才能找到需要修改的地方,而且改完以后总是有其他老板先后找上门来,因为他们不需要这种修改,而小白自己改的时候竟然也不知道会影响到哪些小车。
看起来是不是很糟糕呢?看来小白同学使用继承来扩展功能的方法并不是多么高明呢。
把系统设计成一些互相合作的类有一个常见的弊端:需要维护相关对象之间的一致性。为了维护一致性而使各类紧密耦合,大大降低了模块的可复用性,这是我们不愿意看到的。
装饰者模式是将要被扩展功能的对象嵌入到另一个对象中,由这个对象 (后者) 添加职责。我们称这个嵌入的对象为装饰。这个装饰和它所装饰的接口一致,因此它对使用该对象 (被装饰对象) 的客户透明。透明性使得你可以递归地嵌套多个装饰,从而可以添加任意多的功能。
装饰者模式的使用场合
动态、透明地给特定对象添加职责,也就是说,不会影响到其他对象。
用于那些可以随时撤销的职责。
当用子类进行扩展是不切实际的时候。有时需要大量的独立扩展并将导致支持各种组合的子类数量大爆炸 (比如小白的例子)。或者一个类的定义可能是隐藏的是子类无法获取到的。
《关于塞班 S40 非智能手机画布的重构计划》需求
大约 4 或 5 年前,当安卓还只是一个初出茅庐的小伙,当诺基亚的塞班雄踞全球手机市场 60% 份额的时候,作者有幸作为 Key Engineer 参与并最终主导了某家互联网公司的垂直搜索服务的塞班 S40 客户端的开发。
当然,如今塞班已在安卓、iPhone 的崛起中日落西山,就连诺基亚也免不了被收购的命运,但那些程序背后的设计思想却并没有随之没落,甚至愈如浓醇的老酒,愈发散发出迷人的醇香。这不正如我们今天讨论的设计模式么,前人在修炼内功的时候就已经考虑到我们这些后来人了,总结了这么一套内功心法,避免了我们重复走他们走过的错路、弯路。
啰里啰嗦了一大堆,越扯越远。总之就是一个意思,就是要对笔者四年前的一个 Java ME 项目进行重构,虽然那个项目早已随着塞班的没落而灰飞烟灭。
其时公司已有一个 js 写的网页版的地图 (类似于百度地图)。公司的三个核心服务:周边搜索、公交查询和驾车导航需要相应的手机客户端,公司已经有了一个 WAP 版的客户端,现在想也能够提供 Java ME 客户端。当时的诺基亚、索爱、三星等等主流手机都是塞班操作系统,而当时的 S40、S60 就是塞班的主流了,而 Java ME 是 S40、S60 上应用开发的不二之选。
周边搜索大部分是在和用户进行文本交互,以及和服务器进行一些信息交换,这一块用 Kuix 或者 LWUIT 等框架可以轻松实现。而地图部分的绘制则需要用到 javax.microedition.lcdui.Canvas 了。根据业务需求,总共涉及了三个基础画布,分别是 WelcomCanvas (显示欢迎界面)、PmapCanvas (绘制地图) 和 HelpCanvas (显示帮助信息),PmapCanvas 是核心画布类,它封装了各种按键事件 (比如方向键等等)、地图移动、地图缩放等基本地图操作,然后是三个核心业务 (周边搜索、公交查询和驾车导航) 所需的一些画布,比如 BusCanvas (公交线路画布)、BusSearchCanvas (周边公交站点结果)、ChangeCityCanvas (切换城市)、DIYCanvas (手动标注地图)、VisitoronCanvas (自动定位) 等等,因为这些具体业务相关画布大都是在 PmapCanvas 的基础上增加或修改一些操作,所以当时设计的时候它们都继承自 PmapCanvas。
WelcomCanvas 源码:
package com.defonds.canvas; import java.io.IOException; import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.Image; import com.defonds.midlet.MapMIDlet; /** * * 项目名称:Basic 类名称:WelcomCanvas 类描述:The class represents the region of the screen * that has been alloted to the game. 创建人:Defonds 创建时间:2009-10-20 上午11:16:44 * 修改人:Defonds 修改时间:2009-10-20 上午11:16:44 修改备注: * * @version * */ public class WelcomCanvas extends Canvas { // ------------------------------------------------------- // fields. /** * logo image of defonds. */ private Image image; // ------------------------------ // Initialization. /** * Constructor. */ public WelcomCanvas(MapMIDlet map) { try { image = Image.createImage("/huanying.png"); } catch (IOException e) { e.printStackTrace(); } } /* * (non-Javadoc) * * @see * javax.microedition.lcdui.Canvas#paint(javax.microedition.lcdui.Graphics * ) */ protected void paint(Graphics arg0) { arg0.drawImage(image, 0, 0, 0); System.out.println(getWidth()+"***"+getHeight()); //arg0.setColor(131,131,131); // arg0.drawString("使用过程中将会产生上网流量",120, image.getHeight()+75, Graphics.TOP // | Graphics.HCENTER); } }
PmapCanvas 源码:
package com.defonds.canvas; import java.io.IOException; import java.util.Vector; import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.Image; import com.defonds.bean.ImageBean; import com.defonds.midlet.MapMIDlet; import com.defonds.util.ImageManager; import com.defonds.util.Mapbar; import com.defonds.util.ThreadPool; import com.defonds.util.Tools; import com.defonds.util.getCityXmlTool; /** * * 项目名称:Basic * 类名称:PmapCanvas * 类描述:基础画布,其他画布的基类 * 创建人:Defonds * 创建时间:2009-10-22 下午02:13:16 * 修改人:Defonds * 修改时间:2009-10-22 下午02:13:16 * 修改备注: * @version * */ public class PmapCanvas extends Canvas { // ------------------------------------------------------- // fields. /** * 手机所在经度坐标. */ protected volatile double lon = Double.parseDouble(MapMIDlet.jd); /** * 手机所在纬度坐标. */ protected volatile double lat = Double.parseDouble(MapMIDlet.wd); /** * 缩放级别.初始化设定为 12.可以在 1 -16 范围内任意取值. */ protected volatile int zoom = 12; /** * 下载图片的宽度 */ protected final int imageWidth = 300; /** * Vector 容器用于存放下载图片. */ protected Vector myVector; /** * 要画的图片 */ protected ImageBean imageBean; /** * 要画的图片 */ protected Image image; /** * 要下载图片的字节流 */ protected byte[] byteofimg; /** * 空白出现倒计时. * blankYdown 向下还有多少象素显示空白. * blankYup 向上还有多少象素显示空白. * blankXleft 向左还有多少象素显示空白. * blankXright 向右还有多少象素显示空白. */ protected volatile int blankYdown = 0; protected volatile int blankYup = 0; protected volatile int blankXleft = 0; protected volatile int blankXright = 0; /** * 键盘处理事件需要的变量.action:GameAction */ protected volatile int action; /** * 是否显示地图图片标识位. */ protected boolean showMap = false; /** * 每按一下向下键需要移动的象素. */ protected final int pixels = 10; /** * 图片缓存管理 */ protected ImageManager imageManager = null; // /** // * 是否下载图片标识位 // * 如果连续按键,此标识为为 false,不允许下载图片 // * 只有按钮松开时,才允许重新下载图片 // */ // protected volatile boolean format = true; /** * 背景图片,当移动出空白时,才可以看到此图片 */ protected Image background; /** * 线程池类 */ ThreadPool p = ThreadPool.getInstance(); // ---------------------------------------------------------- // Initialization. /** * Constructor. */ public PmapCanvas() { /** * 图片管理器初始化 */ imageManager = ImageManager.getInstance(); /** * 背景图片初始化 */ background = imageManager.getImage("map_loading"); } /** * 进入联网状态. */ public void startOnline() { /** * 手机地图首次初始化 */ myVector = imagesFormat(); showMap = true; repaint(); } /** * 按0键返回我的位置 */ protected void toMyPosition() { this.lat = Double.parseDouble(MapMIDlet.wd); this.lon = Double.parseDouble(MapMIDlet.jd); /** * 更改 movJD,movWD.并把 cityName 改为根据 JD,WD 获得的值 */ MapMIDlet.movJD = MapMIDlet.jd; MapMIDlet.movWD = MapMIDlet.wd; Runnable task2 = new Runnable() { public void run() { String xml = null; String url = Tools.getCity(MapMIDlet.movJD, MapMIDlet.movWD, null, null); xml = Tools.loadHttpString(url); if (xml != null) { getCityXmlTool parserXml = new getCityXmlTool(); String city = parserXml.parserXml(xml); MapMIDlet.cityName = city; //System.out.println("获取新城市名称:" + MapMIDlet.cityName); } } }; p.addTask(task2, "getCityNameThread"); Runnable task = new Runnable() { public void run() { myVector = imagesFormat(); repaint(); } }; p.addTask(task,"myPositionThread"); /* Thread myPositionThread = new Thread() { public void run() { myVector = imagesFormat(); repaint(); } }; myPositionThread.start();*/ } /** * pmapCanvas画布进行图片放大操作. */ protected void increase() { if(zoom < 13) { zoom ++; Runnable task = new Runnable() { public void run() { myVector = imagesFormat(); repaint(); } }; p.addTask(task,"increaseThread"); /* Thread increaseThread = new Thread() { public void run() { myVector = imagesFormat(); repaint(); } }; increaseThread.start();*/ } } /** * pmapCanvas画布进行图片缩小操作. */ protected void decrease() { if(zoom > 1) { zoom --; Runnable task = new Runnable() { public void run() { myVector = imagesFormat(); repaint(); } }; p.addTask(task,"decreaseThread"); /* Thread decreaseThread = new Thread() { public void run() { myVector = imagesFormat(); repaint(); } }; decreaseThread.start();*/ } } /** * 图片初始化. */ protected Vector imagesFormat() { Vector forVector = new Vector(); //System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@@@@@有请求过来,开始下载新图片"); /** * mapbar 传递图片信息,图片初始坐标, http 下载 url 信息每次在这里初始化. */ //System.out.println("绘制地图,传送过来的经度:" + lon + " 传送过来的纬度:" + lat); forVector = Mapbar.retrieveDynamicImage(getWidth(), getHeight(), lon, lat, zoom, "png"); Vector forVector2 = new Vector(); for(int i = 0;i < forVector.size();i ++) { ImageBean imageBean2 = (ImageBean)forVector.elementAt(i); /** * 图片每次在这里进行下载. */ try { byteofimg = Tools.loadHttpFile(imageBean2.getUrl()); imageBean2.setByteOfImage(byteofimg); forVector2.addElement(imageBean2); } catch (IOException e){} /** * blankYdown 向下空白倒计时每次在这里进行初始化. */ if(imageBean2.getOrdinate() + imageWidth > getHeight()) { blankYdown = imageBean2.getOrdinate() + imageWidth - getHeight(); } /** * blankXleft 向右空白倒计时每次在这里进行初始化. */ if(imageBean2.getAbscissa() + imageWidth > getWidth()) { blankXright = imageBean2.getAbscissa() + imageWidth - getWidth(); } /** * blankYup 向上空白倒计时每次在这里进行初始化. */ if(imageBean2.getOrdinate() < 0) { //blankYup = imageWidth + imageBean2.getOrdinate(); blankYup = (-1) * imageBean2.getOrdinate(); } /** * blankXleft 向左空白倒计时每次在这里进行初始化. */ if(imageBean2.getAbscissa() < 0) { //blankXleft = imageWidth + imageBean2.getAbscissa(); blankXleft = (-1) * imageBean2.getAbscissa(); } } return forVector2; } /** * 随着向下键被按住,拖拉,图片坐标变化. * 传入的变量: * gVector:存放图片的容器. * length:每次要移动的长度. */ protected Vector imagesChangeXdown(Vector gVector,int length) { Vector chVector = new Vector(); for(int i = 0;i < gVector.size();i ++) { imageBean = (ImageBean)gVector.elementAt(i); int temp = imageBean.getOrdinate(); imageBean.setOrdinate(temp - length); chVector.addElement(imageBean); } /** * 屏幕中心点移动 * 向下键被按住,各下载的图片的纵坐标减小,而手机屏幕中心点纬度减小 */ lat = Mapbar.getLatitude(lat, length * (-1), zoom); if(MapMIDlet.movChange) { MapMIDlet.movWD = Double.toString(lat); } return chVector; } /** * 随着向右键被按住,拖拉,图片坐标变化. * 传入的变量: * gVector:存放图片的容器. * length:每次要移动的长度. */ protected Vector imagesChangeYright(Vector gVector,int length) { Vector chVector = new Vector(); for(int i = 0;i < gVector.size();i ++) { imageBean = (ImageBean)gVector.elementAt(i); int temp = imageBean.getAbscissa(); imageBean.setAbscissa(temp - length); chVector.addElement(imageBean); } /** * 屏幕中心点移动 * 向右键被按住,各下载的图片的横坐标减小,而手机屏幕中心点经度增加 */ lon = Mapbar.getLongitude(lon, length, zoom); if(MapMIDlet.movChange) { MapMIDlet.movJD = Double.toString(lon); } return chVector; } /** * 随着向上键被按住,拖拉,图片坐标变化. * 传入的变量: * gVector:存放图片的容器. * length:每次要移动的长度. */ protected Vector imagesChangeXup(Vector gVector,int length) { Vector chVector = new Vector(); for(int i = 0;i < gVector.size();i ++) { imageBean = (ImageBean)gVector.elementAt(i); int temp = imageBean.getOrdinate(); imageBean.setOrdinate(temp + length); chVector.addElement(imageBean); } /** * 屏幕中心点移动 * 向上键被按住,各下载的图片的纵坐标增加,而手机屏幕中心点纬度增加 */ lat = Mapbar.getLatitude(lat, length, zoom); if(MapMIDlet.movChange) { MapMIDlet.movWD = Double.toString(lat); } return chVector; } /** * 随着向左键被按住,拖拉,图片坐标变化. * 传入的变量: * gVector:存放图片的容器. * length:每次要移动的长度. */ protected Vector imagesChangeYleft(Vector gVector,int length) { Vector chVector = new Vector(); for(int i = 0;i < gVector.size();i ++) { imageBean = (ImageBean)gVector.elementAt(i); int temp = imageBean.getAbscissa(); imageBean.setAbscissa(temp + length); chVector.addElement(imageBean); } /** * 屏幕中心点移动 * 向左键被按住,各下载的图片的横坐标增加,而手机屏幕中心点经度减小 */ lon = Mapbar.getLongitude(lon, length * (-1), zoom); if(MapMIDlet.movChange) { MapMIDlet.movJD = Double.toString(lon); } return chVector; } /** * 重写 Canvas 键盘事件函数 keyPressed. */ protected void keyPressed(int keyCode) { switch(keyCode) { case Canvas.KEY_NUM1://------------------------------缩小 decrease(); break; case Canvas.KEY_NUM3://----------------------------- 放大 increase(); break; case Canvas.KEY_NUM0://----------------------------- 返回我的位置 toMyPosition(); break; } action = getGameAction(keyCode); switch(action) { case Canvas.DOWN: /** * 每次先判断一下是否要出现空白. */ if(blankYdown < 0) { myVector = imagesFormat(); } myVector = imagesChangeXdown(myVector, pixels); blankYdown = blankYdown - pixels; repaint(); break; case Canvas.RIGHT: /** * 每次先判断一下是否要出现空白. */ if(blankXright < 0) { myVector = imagesFormat(); } myVector = imagesChangeYright(myVector, pixels); blankXright = blankXright - pixels; repaint(); break; case Canvas.UP: /** * 每次先判断一下是否要出现空白. */ if(blankYup < 0) { myVector = imagesFormat(); } myVector = imagesChangeXup(myVector, pixels); blankYup = blankYup - pixels; repaint(); break; case Canvas.LEFT: /** * 每次先判断一下是否要出现空白. */ if(blankXleft < 0) { myVector = imagesFormat(); } myVector = imagesChangeYleft(myVector, pixels); blankXleft = blankXleft - pixels; repaint(); break; } } /** * 重写 Canvas 键盘事件函数 keyReleased */ protected void keyReleased(int keyCode) { /** * 如果出现空白,即行下载 */ if(blankYdown < 0 || blankXright < 0 || blankYup < 0 || blankXleft < 0) { myVector = imagesFormat(); repaint(); } } /** * 重写 Canvas 键盘事件函数 keyRepeated. */ protected void keyRepeated(int keyCode) { switch(keyCode) { case Canvas.KEY_NUM1://------------------------------缩小 decrease(); break; case Canvas.KEY_NUM3://----------------------------- 放大 increase(); break; case Canvas.KEY_NUM0://----------------------------- 返回我的位置 toMyPosition(); break; } action = getGameAction(keyCode); switch(action) { case Canvas.DOWN: // /** // * 每次先判断一下是否要出现空白. // */ // if(blankYdown < 0) // { // myVector = imagesFormat(); // } myVector = imagesChangeXdown(myVector, pixels); blankYdown = blankYdown - pixels; repaint(); break; case Canvas.RIGHT: // /** // * 每次先判断一下是否要出现空白. // */ // if(blankXright < 0) // { // myVector = imagesFormat(); // } myVector = imagesChangeYright(myVector, pixels); blankXright = blankXright - pixels; repaint(); break; case Canvas.UP: // /** // * 每次先判断一下是否要出现空白. // */ // if(blankYup < 0) // { // myVector = imagesFormat(); // } myVector = imagesChangeXup(myVector, pixels); blankYup = blankYup - pixels; repaint(); break; case Canvas.LEFT: // /** // * 每次先判断一下是否要出现空白. // */ // if(blankXleft < 0) // { // myVector = imagesFormat(); // } myVector = imagesChangeYleft(myVector, pixels); blankXleft = blankXleft - pixels; repaint(); break; } } /* (non-Javadoc) * @see javax.microedition.lcdui.Canvas#paint(javax.microedition.lcdui.Graphics) */ protected void paint(Graphics arg0) { if(!showMap) { Tools.clearScreen(arg0, this); String welcome = "载入中... ..."; arg0.setColor(0); arg0.drawString(welcome, 5, 5, Graphics.TOP | Graphics.LEFT); //arg0.drawImage(image, 0, 0, Graphics.TOP | Graphics.LEFT); } else { /** * 先清屏 */ Tools.clearScreen(arg0, this); /** * 然后将背景图片画在画布上 */ arg0.drawImage(background, 0, 0, Graphics.TOP | Graphics.LEFT); /** * 遍历 myVector,将地图图片绘制在画布上. */ Tools.clearScreen(arg0, this); for (int i = 0;i < myVector.size();i ++) { ImageBean imageBean = (ImageBean)myVector.elementAt(i); Image image = Image.createImage(imageBean.getByteOfImage(), 0, imageBean.getByteOfImage().length); arg0.drawImage(image, imageBean.getAbscissa(), imageBean.getOrdinate(), Graphics.TOP | Graphics.LEFT); } } } //------------------------------------------------------ //The getters and setters. public double getLon() { return lon; } public void setLon(double lon) { this.lon = lon; if(MapMIDlet.movChange) { MapMIDlet.movJD = Double.toString(lon); } } public double getLat() { return lat; } public void setLat(double lat) { this.lat = lat; if(MapMIDlet.movChange) { MapMIDlet.movWD = Double.toString(lat); } } public int getZoom() { return zoom; } public void setZoom(int zoom) { this.zoom = zoom; } }
PmapCanvas 的扩展功能类之一,画公交站点的画布 BusSearchCanvas 源码:
package com.defonds.canvas; import java.util.Vector; import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.Font; import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.Image; import com.defonds.bean.BusBean; import com.defonds.bean.ImageBean; import com.defonds.util.Mapbar; import com.defonds.util.Tools; /** * * 项目名称:Basic * 类名称:BusSearchCanvas * 类描述:周边公交站点结果 * 已知调用到此类的地方有:com.defonds.form.searchInfoForm * 创建人:Defonds * 创建时间:2009-11-9 下午05:40:52 * 修改人:Defonds * 修改时间:2009-11-9 下午05:40:52 * 修改备注: * @version * */ public class BusSearchCanvas extends PmapCanvas { //---------------------------------------- //fields /** * Vector 容器用于存放公交站点 BusBean(接收外部数据) * 注明:与 mapbar 地图信息的获取方式不同:公交站点所有信息,将由调用本 BusSearchCanvas 类者提供 */ private Vector bsVector; /** * Vector 容器用于存放公交站点 BusBean(内部数据调整) */ private Vector tmpVector; /** * 设置绘画字符串字体的格式 */ private Font bsFont; /** * 字符串大于这个长度时,将被截取 */ private final int maxLength = 16; /** * 钉子的图片宽度 */ private final int pointWidth = 15; /** * 提示框的宽度,高度 */ private final int bubbleHeight = 20; /** * 封装公交站点搜索结果的 Javabean */ private BusBean tmpBean; /** * 代表公交站点的钉子图片 */ private Image tmpImage; //----------------------------------------------------- //methods. /** * 创建一个新的实例 BusSearchCanvas. */ public BusSearchCanvas() { /** * 设置绘画字符串字体的格式在这里进行初始化操作 */ bsFont = Font.getFont(Font.FACE_SYSTEM,Font.FACE_SYSTEM,Font.SIZE_LARGE); /** * 容器用于存放公交站点 BusBean(内部数据调整)在这里进行初始化 */ tmpVector = new Vector(); /** * 代表公交站点的钉子图片在这里进行初始化操作 */ tmpImage = imageManager.getImage("point"); } /** * 公交站点 bsVector 初始化 */ private void busSearchFormat() { //tmpVector = null; Vector ttt = new Vector(); for(int i = 0;i < bsVector.size();i ++) { tmpBean = (BusBean)bsVector.elementAt(i); int dx = Mapbar.getDx(lon, tmpBean.getLongitude(), zoom); int dy = Mapbar.getDy(lat, tmpBean.getLatitude(), zoom); tmpBean.setX(getWidth() / 2 + dx); tmpBean.setY(getHeight() / 2 - dy); /** * 字符串截取,防止屏幕中出现换行,或一行长度大于屏幕宽度 */ if(tmpBean.getName().length() > maxLength) { tmpBean.setName(tmpBean.getName().substring(0, maxLength)); } /** * 提示框宽度初始化 */ tmpBean.setBubbleWidth(bsFont.stringWidth(tmpBean.getName()) + 20); /** * 提示框高度初始化 */ tmpBean.setBubbleHeight(bubbleHeight); //-------------------------------------------------- //提示框长度控制;优先级:左 > 右 /** * 不允许字符串被屏幕右端截断 */ while(tmpBean.getX() + tmpBean.getBubbleWidth() > getWidth()) { tmpBean.setX(tmpBean.getX() - 1); } /** * 不允许钉子被屏幕左端截断 */ while(tmpBean.getX() - pointWidth < 0) { tmpBean.setX(tmpBean.getX() + 1); } ttt.addElement(tmpBean); } tmpVector = ttt; } /** * 随着向下键被按住,公交站点 Y 坐标发生改变 */ private Vector busYchangeDown() { Vector tmV = new Vector(); for(int i = 0;i < tmpVector.size();i ++) { tmpBean = (BusBean)tmpVector.elementAt(i); tmpBean.setY(tmpBean.getY() - pixels); tmV.addElement(tmpBean); } return tmV; } /** * 随着向右键被按住,公交站点 X 坐标发生改变 */ private Vector busXchangeRight() { Vector tmV = new Vector(); for(int i = 0;i < tmpVector.size();i ++) { tmpBean = (BusBean)tmpVector.elementAt(i); tmpBean.setX(tmpBean.getX() - pixels); tmV.addElement(tmpBean); } return tmV; } /** * 随着向上键被按住,公交站点 Y 坐标发生改变 */ private Vector busYchangeUp() { Vector tmV = new Vector(); for(int i = 0;i < tmpVector.size();i ++) { tmpBean = (BusBean)tmpVector.elementAt(i); tmpBean.setY(tmpBean.getY() + pixels); tmV.addElement(tmpBean); } return tmV; } /** * 随着向左键被按住,公交站点 X 坐标发生改变 */ private Vector busXchangeLeft() { Vector tmV = new Vector(); for(int i = 0;i < tmpVector.size();i ++) { tmpBean = (BusBean)tmpVector.elementAt(i); tmpBean.setX(tmpBean.getX() + pixels); tmV.addElement(tmpBean); } return tmV; } /** * 重写PmapCanvas的startOnline方法 */ public void startOnline() { /** * 手机地图首次初始化 */ myVector = imagesFormat(); /** * 公交站点首次初始化 */ busSearchFormat(); showMap = true; repaint(); } /** * 重写PmapCanvas的increase方法 */ protected void increase() { if(zoom < 13) { zoom ++; Runnable task = new Runnable() { public void run() { /** * 地图下载后进行重置 */ myVector = imagesFormat(); /** * 根据目前屏幕中心点坐标,重置公交站点在手机屏幕上的坐标 */ busSearchFormat(); repaint(); } }; p.addTask(task,"increaseThread"); /*Thread increaseThread = new Thread() { public void run() { *//** * 地图下载后进行重置 *//* myVector = imagesFormat(); *//** * 根据目前屏幕中心点坐标,重置公交站点在手机屏幕上的坐标 *//* busSearchFormat(); repaint(); } }; increaseThread.start();*/ } } /** * 重写PmapCanvas的decrease方法 */ protected void decrease() { if(zoom > 1) { zoom --; Runnable task = new Runnable() { public void run() { /** * 地图下载后进行重置 */ myVector = imagesFormat(); /** * 根据目前屏幕中心点坐标,重置公交站点在手机屏幕上的坐标 */ busSearchFormat(); repaint(); } }; p.addTask(task,"decreaseThread"); /* Thread decreaseThread = new Thread() { public void run() { *//** * 地图下载后进行重置 *//* myVector = imagesFormat(); *//** * 根据目前屏幕中心点坐标,重置公交站点在手机屏幕上的坐标 *//* busSearchFormat(); repaint(); } }; decreaseThread.start();*/ } } /** * 重写了 PmapCanvas/Canvas 的 keyPressed 方法 */ protected void keyPressed(int keyCode) { switch(keyCode) { case Canvas.KEY_NUM1://------------------------------缩小 decrease(); break; case Canvas.KEY_NUM3://----------------------------- 放大 increase(); break; case Canvas.KEY_NUM0://----------------------------- 返回我的位置 toMyPosition(); break; } action = getGameAction(keyCode); switch(action) { case Canvas.DOWN: /** * 每次先判断一下是否要出现空白. */ if(blankYdown < 0) { myVector = imagesFormat(); } /** * 地图坐标变化 */ myVector = imagesChangeXdown(myVector, pixels); /** * 公交站点信息坐标变化 */ tmpVector = busYchangeDown(); blankYdown = blankYdown - pixels; repaint(); break; case Canvas.RIGHT: /** * 每次先判断一下是否要出现空白. */ if(blankXright < 0) { myVector = imagesFormat(); } myVector = imagesChangeYright(myVector, pixels); /** * 公交站点信息坐标变化 */ tmpVector = busXchangeRight(); blankXright = blankXright - pixels; repaint(); break; case Canvas.UP: /** * 每次先判断一下是否要出现空白. */ if(blankYup < 0) { myVector = imagesFormat(); } myVector = imagesChangeXup(myVector, pixels); /** * 公交站点信息坐标变化 */ tmpVector = busYchangeUp(); blankYup = blankYup - pixels; repaint(); break; case Canvas.LEFT: /** * 每次先判断一下是否要出现空白. */ if(blankXleft < 0) { myVector = imagesFormat(); } myVector = imagesChangeYleft(myVector, pixels); /** * 公交站点信息坐标变化 */ tmpVector = busXchangeLeft(); blankXleft = blankXleft - pixels; repaint(); break; } } /** * 重写了 PmapCanvas/Canvas 的 keyRepeated 方法 */ protected void keyRepeated(int keyCode) { switch(keyCode) { case Canvas.KEY_NUM1://------------------------------缩小 decrease(); break; case Canvas.KEY_NUM3://----------------------------- 放大 increase(); break; case Canvas.KEY_NUM0://----------------------------- 返回我的位置 toMyPosition(); break; } action = getGameAction(keyCode); switch(action) { case Canvas.DOWN: // /** // * 每次先判断一下是否要出现空白. // */ // if(blankYdown < 0) // { // myVector = imagesFormat(); // } /** * 地图坐标变化 */ myVector = imagesChangeXdown(myVector, pixels); /** * 公交站点信息坐标变化 */ tmpVector = busYchangeDown(); blankYdown = blankYdown - pixels; repaint(); break; case Canvas.RIGHT: // /** // * 每次先判断一下是否要出现空白. // */ // if(blankXright < 0) // { // myVector = imagesFormat(); // } myVector = imagesChangeYright(myVector, pixels); /** * 公交站点信息坐标变化 */ tmpVector = busXchangeRight(); blankXright = blankXright - pixels; repaint(); break; case Canvas.UP: // /** // * 每次先判断一下是否要出现空白. // */ // if(blankYup < 0) // { // myVector = imagesFormat(); // } myVector = imagesChangeXup(myVector, pixels); /** * 公交站点信息坐标变化 */ tmpVector = busYchangeUp(); blankYup = blankYup - pixels; repaint(); break; case Canvas.LEFT: // /** // * 每次先判断一下是否要出现空白. // */ // if(blankXleft < 0) // { // myVector = imagesFormat(); // } myVector = imagesChangeYleft(myVector, pixels); /** * 公交站点信息坐标变化 */ tmpVector = busXchangeLeft(); blankXleft = blankXleft - pixels; repaint(); break; } } /** * 重写PmapCanvas/Canvas的paint方法 */ protected void paint(Graphics arg0) { if(!showMap) { Tools.clearScreen(arg0, this); arg0.setColor(0); arg0.drawString("载入中... ...", 5, 5, Graphics.TOP | Graphics.LEFT); } else { /** * 先清屏 */ Tools.clearScreen(arg0, this); /** * 然后将背景图片画在画布上 */ arg0.drawImage(background, 0, 0, Graphics.TOP | Graphics.LEFT); /** * 遍历 myVector,将地图图片绘制在画布上. */ for (int i = 0;i < myVector.size();i ++) { imageBean = (ImageBean)myVector.elementAt(i); image = Image.createImage(imageBean.getByteOfImage(), 0, imageBean.getByteOfImage().length); arg0.drawImage(image, imageBean.getAbscissa(), imageBean.getOrdinate(), Graphics.TOP | Graphics.LEFT); } /** * 遍历 tmpVector,将公交信息绘制在画布上 */ for(int i = 0;i < tmpVector.size();i ++) { tmpBean = (BusBean)tmpVector.elementAt(i); /** * 画图钉 */ arg0.drawImage(tmpImage, tmpBean.getX() - pointWidth, tmpBean.getY(), Graphics.TOP | Graphics.LEFT); /** * 画提示框 */ arg0.setColor(219, 169, 54);//土*** arg0.fillRect(tmpBean.getX(), tmpBean.getY(),tmpBean.getBubbleWidth() , tmpBean.getBubbleHeight()); arg0.setColor(255,0,0);//红色 arg0.drawRect(tmpBean.getX(), tmpBean.getY(), tmpBean.getBubbleWidth(), tmpBean.getBubbleHeight()); arg0.setFont(bsFont); arg0.drawString(tmpBean.getName(), tmpBean.getX() + 10, tmpBean.getY(), Graphics.TOP | Graphics.LEFT); } } } //------------------------------------------------------------ //The getters and setters. public Vector getBsVector() { return bsVector; } public void setBsVector(Vector bsVector) { this.bsVector = bsVector; } }
《关于塞班 S40 非智能手机地图画布的重构计划》分析
这个跟小白接私活的故事很像,只是画布类没有小白的小车类那么多,所以继承的缺陷还没有充分表露出来。但是这些子类可复用性太低,因为每个类都有自己新添的功能和继承来的功能,如果你在其他子类中想复用一下,恐怕只有手工拷贝、复制了。更糟糕的是,随着业务的扩展,需求的增加,笔者预言,这些画布到小白的那种混乱的地步也只是时间问题。而把 BusSearchCanvas 等这些扩展类做成装饰者,可以让我们的系统灵活起来,而且复用性得到提高。于是笔者打算这样进行重构:WelcomCanvas 等三个基础画布不动,增加一个抽象的 CanvasDecorator 装饰者类,BusSearchCanvas 等类改为继承 CanvasDecorator。
《关于塞班 S40 非智能手机地图画布的重构计划》类设计
《关于塞班 S40 非智能手机地图画布的重构计划》源码实现
WelcomCanvas 等三个基础画布不变。CanvasDecorator 部分源码如下:
public abstract class CanvasDecorator extends Canvas { protected abstract void paint(Graphics arg0); // 这里重写了 Canvas,所有的装饰者都要重新实现 }
装饰者之一 BusSearchCanvas 部分源码如下:
public class BusSearchCanvas extends CanvasDecorator { Canvas canvas; public BusSearchCanvas (Canvas canvas) { this.canvas = canvas; } protected abstract void paint(Graphics arg0) { canvas.paint(arg0); // 先画基础画布 /** * 遍历 tmpVector,将公交信息绘制在画布上 */ for(int i = 0;i < tmpVector.size();i ++) { tmpBean = (BusBean)tmpVector.elementAt(i); /** * 画图钉 */ arg0.drawImage(tmpImage, tmpBean.getX() - pointWidth, tmpBean.getY(), Graphics.TOP | Graphics.LEFT); /** * 画提示框 */ arg0.setColor(219, 169, 54);//土*** arg0.fillRect(tmpBean.getX(), tmpBean.getY(),tmpBean.getBubbleWidth() , tmpBean.getBubbleHeight()); arg0.setColor(255,0,0);//红色 arg0.drawRect(tmpBean.getX(), tmpBean.getY(), tmpBean.getBubbleWidth(), tmpBean.getBubbleHeight()); arg0.setFont(bsFont); arg0.drawString(tmpBean.getName(), tmpBean.getX() + 10, tmpBean.getY(), Graphics.TOP | Graphics.LEFT); } } }
其他几个 BusCanvas 等装饰者源码重构类似于 BusSearchCanvas,这里不再赘述。
相关文章推荐
- 设计模式实战应用之三:装饰者模式
- 设计模式实战应用之五:工厂方法模式
- 设计模式中的模板方法模式在Ruby中的应用实例两则
- 装饰者模式的应用
- Spring容器装饰者模式应用之实现业务类与服务类自由组合的解决方案
- 设计模式(5)------装饰者设计模式(IO流的应用)
- 设计模式应用与发展之门面模式(java)
- 设计模式实战应用之五:工厂方法模式
- 《设计模式》读书笔记:装饰者模式
- Spring容器装饰者模式应用之实现业务类与服务类自由组合的解决方式
- 设计模式(十):Decorator装饰者模式 -- 结构型模式
- 设计模式--装饰者模式(在IO体系中的应用)
- 设计模式-装饰者模式(Decorator)理解和在Android中的应用
- 设计模式感触之代理模式应用
- 设计模式实战应用之二:观察者模式
- 设计模式实战应用之一:策略模式
- 设计模式实战应用之五:工厂方法模式
- 微软WCF应用高级进阶(分布式+异步调用+安全+通信模式)配销售管理平台项目实战
- 设计模式系列之二:装饰者模式(Decorator Pattern)
- 设计模式解析与实战之单列模式