文章目录
设计模式——结构型设计模式
代理模式
定义:为其他对象提供一种代理以控制对这个对象的访问
简单实现
我多年没有回过哈尔滨了,很是想念哈尔滨秋林红肠的味道。但是本人工作一直很忙抽不开身,不能够亲自回哈尔滨购买,于是就托在哈尔滨的朋友帮我购买秋林红肠。
在代理模式中有如下角色。
- Subject:抽象主题类,声明真实主题与代理的共同接口方法。
- RealSubject:真实主题类,代理类所代表的真实主题。客户端通过代理类间接地调用真实主题类的方法。
- Proxy:代理类,持有对真实主题类的引用,在其所实现的接口方法中调用真实主题类中相应的接口方法执行。
- Client:客户端类。
抽象主题类
抽象主题类具有真实主题类和代理的共同接口方法,共同的方法就是购买
真实主题类
这个购买者LiuWangShu,实现了IShop接口提供的 buy()方法,如下所示
代理类
我找的代理类同样也要实现IShop接口,并且要持有被代理者,在buy()方法中调用了被代理者的buy()方法
客户端类
客户端类的代码就是代理类包含了真实主题类(被代理者),最终调用的都是真实主题类(被代理者)实现的方法。在上面的例子中就是 LiuWangShu 类的 buy()方法,所以运行的结果就是“购买”。
动态代理实现
从编码的角度来说,代理模式分为静态代理和动态代理。上面的例子是静态代理,在代码运行前就已经存在了代理类的class编译文件;
从编码的角度来说,代理模式分为静态代理和动态代理。上面的例子是静态代理,在代码运行前就已经存在了代理类的class编译文件
Java 提供了动态的代理接口 InvocationHandler,实现该接口需要重写invoke()方法。
在动态代理类中我们声明一个Object的引用,该引用指向被代理类,我们调用被代理类的具体方法在invoke()方法中执行。
客户端类代码:
调用 Proxy.newProxyInstance()来生成动态代理类,调用 purchasing 的 buy 方法会调用DynamicPurchasing的invoke方法。
类型和优点
- 代理模式从编码的角度来说可以分为静态代理和动态代理
代理模式的优点主要有以下几点:
- 真实主题类就是实现实际的业务逻辑,不用关心其他非本职的工作。
- 真实主题类随时都会发生变化;但是因为它实现了公共的接口,所以代理类可以不做任何修改就能够使用。
装饰模式
装饰模式是结构型设计模式之一,其在不必改变类文件和使用继承的情况下,动态地扩展一个对象的功能,是继承的替代方案之一。它通过创建一个包装对象,也就是装饰来包裹真实的对象。
定义:动态的给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更灵活。
装饰模式中有如下角色:
- Component:抽象组件,可以是接口或是抽象类,被装饰的最原始的对象。
- ConcreteComponent:组件具体实现类。Component的具体实现类,被装饰的具体对象。
- Decorator:抽象装饰者,从外类来拓展Component类的功能,但对于Component来说无须知道Decorator的存在。在它的属性中必然有一个private变量指向Component抽象组件。
- ConcreteDecorator:装饰者的具体实现类。
简单实现
杨过本身就会全真剑法,有两位武学前辈洪七公和欧阳锋分别向杨过传授过打狗棒法和蛤蟆功,这样杨过除了会全真剑法,还会打狗棒法和蛤蟆功。洪七公和欧阳锋就起到了“装饰”杨过的作用。
抽象组件
作为武侠,肯定要会使用武功。我们先定义一个武侠的抽象类,里面有使用武功的抽象方法:
组件具体实现类
被装饰的具体对象,在这里就是被教授武学的具体武侠,也就是杨过。杨过作为武侠当然也会武学
抽象装饰者
抽象装饰者保持了一个对抽象组件的引用,方便调用被装饰对象中的方法。在这个例子中就是武学前辈要持有武侠的引用,方便教授他武学并使他融会贯通:
装饰者具体实现类
使用场景及优缺点
使用场景
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 需要动态地给一个对象增加功能,这些功能可以动态地撤销。
- 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
优点
- 通过组合而非继承的方式,动态地扩展一个对象的功能,在运行时选择不同的装饰器,从而实现不同的行为。
- 有效避免了使用继承的方式扩展对象功能而带来的灵活性差、子类无限制扩张的问题。
- 具体组件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体组件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开放封闭原则”。
缺点
- 因为所有对象均继承于Component,所以如果Component内部结构发生改变,则不可避免地影响所有子类(装饰者和被装饰者)。如果基类改变,则势必影响对象的内部。
比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。所以,只在必要的时候使用装饰模式。 - 装饰层数不能过多,否则会影响效率。
外观模式
外观模式也被称为门面模式。当我们开发Android的时候,无论是做SDK还是封装API,大多都会用到外观模式,它通过一个外观类使得整个系统的结构只有一个统一的高层接口,这样能降低用户的使用成本。
定义:要求一个子系统的外部与内部的通信必须通过一个统一的对象进行。此模式提供一个高层的接口,使得子系统更易于使用。
在外观模式中有如下角色。
- Facade:外观类,知道哪些子系统类负责处理请求,将客户端的请求代理给适当的子系统对象。
- Subsystem:子系统类,可以有一个或者多个子系统。实现子系统的功能,处理外观类指派的任务,注意子系统类不含有外观类的引用。
简单实现
前面列举了武侠的例子,本节还举武侠的例子。首先,我们把武侠张无忌当作一个系统。张无忌作为一个武侠,他本身分为3个系统,分别是招式、内功和经脉。
子系统类
张无忌有很多武学和内功,怎么将它们搭配并对外界隐藏呢?接下来查看外观类。
外观类
这里的外观类就是张无忌,他负责将自己的招式、内功和经脉通过不同的情况合理地运用,代码如下所示:
- 当张无忌使用乾坤大挪移或者七伤拳的时候,比武的对手显然不知道张无忌本身运用了什么,同时张无忌也无须重新计划使用七伤拳的时候要怎么做。
- 如果每次使用七伤拳或者乾坤大挪移时都要计划怎么做很显然会增加成本并贻误战机。另外,张无忌也可以改变自己的内功、招式和经脉,这些都是对比武的对手有所隐藏的。
- 外观模式本身就是将子系统的逻辑和交互隐藏起来,为用户提供一个高层次的接口,使得系统更加易用,同时也隐藏了具体的实现。这样即使具体的子系统发生了变化,用户也不会感知到。
使用场景及优缺点
使用场景
- 使用外观模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,则可以让其通过外观接口进行通信,减少子系统之间的依赖关系。
- 子系统往往会因为不断地重构演化而变得越来越复杂。我们可以使用外观类提供一个简单的接口,对外隐藏子系统的具体实现并隔离变化。
- 当维护一个遗留的大型系统时,可能这个系统已经非常难以维护和拓展;但因为它含有重要的功能,所以新的需求必须依赖于它,这时可以使用外观类,为设计粗糙或者复杂的遗留代码提供一个简单的接口,让新系统和外观类交互,而外观类负责与遗留的代码进行交互。
优点
- 减少系统的相互依赖,所有的依赖都是对外观类的依赖。
- 对用户隐藏了子系统的具体实现,减少用户对子系统的耦合。
- 加强了安全性,子系统中的方法如果不在外观类中开通,就无法访问到子系统中的方法。
享元模式
享元模式是池技术的重要实现方式,它可以减少应用程序创建的对象,降低程序内存的占用,提高程序的性能。
定义:使用共享对象有效地支持大量细粒度的对象
在享元模式中有如下角色:
- Flyweight:抽象享元角色,同时定义出对象的外部状态和内部状态的接口或者实现。
- ConcreteFlyweight:具体享元角色,实现抽象享元角色定义的业务。
- FlyweightFactory:享元工厂,负责管理对象池和创建享元对象。
简单实现
某著名网上商城卖商品,如果每个用户下单都生成商品对象,这显然会耗费很多资源。如果赶上“双11”,那恐怖的订单量会生成很多商品对象,更何况商城卖的商品种类繁多,这样就极易产生“Out Of Memory”。因此,我们采用享元模式来对商品的创建进行优化。
抽象享元角色
抽象享元角色是一个商品接口,它定义了showGoodsPrice方法来展示商品的价格:
具体享元角色
定义类Goods,它实现IGoods 接口,并实现了showGoodsPrice方法,如下所示:
享元工厂
客户端调用
在客户端中调用 GoodsFactory 的 getGoods 方法来创建 Goods 对象,并调用 Goods 的showGoodsPrice方法来显示产品的价格,如下所示:
运行结果如下:
创建商品,key为:iphone7 价格为5199元
使用缓存,key为:iphone7 价格为5199元
使用缓存,key为:iphone7 价格为5999元
从输出可以看出,只有第一次是创建Goods对象,后面因为key值相同,所以均使用了对象池中的Goods对象。在这个例子中,name作为内部状态是不变的,并且作为Map的key值是可以共享的。而showGoodsPrice 方法中需要传入的 version值则是外部状态,它的值是变化的。
使用场景
- 系统中存在大量的相似对象。
- 需要缓冲池的场景。
适配器模式
概述
定义:适配器模式把一个类的接口变换成客户端所期待的另一种接口, 从而使原本因接口不匹配而无
法在一起工作的两个类能够在一起工作。
使用场景
- 系统需要使用现有的类,而此类的接口不符合系统的需要,即接口不兼容。
- 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些
可能在将来引进的类一起工作。 - 需要一个统一的输出接口,而输入端的类型不可预知。
角色
- Target: 目标角色,也就是所期待得到的接口。注意:由于这里讨论的是类适配器模式,因
此目标不可以是类。 - Adaptee: 现在需要适配的接口。
- Adapter: 适配器角色,也是本模式的核心。适配器把源接口转换成目标接口。显然,这一
角色不可以是接口,而必须是具体类。
简单示例
类适配器模式
笔记本电脑的电源一般都是用5V电压,但是我们生活中的电线电压一般都是220V。这个时候就出现了不匹配的状况,在软件开发中我们称之为接口不兼容,此时就需要适配器来进行一个接口转换。在软件开发中有一句话正好体现了这点:任何间题都可以加一个中间层来解决。这个层我们可以理解为这里的Adapter 层,通过这层来进行一个接口转换就达到了兼容的目的。
在上述电源接口这个示例中,5V电压就是Target接口,220V 电压就是Adaptee类,而将电压
从220V转换到5V就是Adapter.
Target角色给出了需要的目标接口,而Adaptee类则是需要被转换的对象。Adapter 则是将
Volt220转换成Target的接口。对应的Target的目标是要获取5V的输出电压,而Adaptee正常输出
电压是220V,此时就需要电源适配器类将220V的电压转换为5V电压,解决接口不兼容的问题。
对象适配器模式
与类的适配器模式一样,对象的适配器模式把被适配的类的API转换成为目标类的API,与类的适配器模式不同的是,对象的适配器模式不是使用继承关系连接到Adaptee类,而是使用代理关系连接到Adaptee类。
从图20-2中可以看出,Adaptee 类(Volt220) 并没有getVolt50方法,而客户端则期待这个方法。为使客户端能够使用Adaptee类,需要提供一个包装类Adapter。这个包装类包装了一个Adaptee的实例,从而此包装类能够把Adaptee的API与Target类的API衔接起来。Adapter 与Adaptee是委派关系,这决定了适配器模式是对象的。示例代码:
- 这种实现方式直接将要被适配的对象传递到Adapter中,使用组合的形式实现接口兼容的效果。
- 这比类适配器方式更为灵活,它的另一个好处是被适配对象中的方法不会暴露出来,而类适配器由于继承了被适配对象,因此,被适配对象类的函数在Adapter类中也都含有,这使得Adapter类出现一些奇怪的接口,用户使用成本较高。因此,对象适配器模式更加灵活、实用。.