此笔记是在软件秘笈-设计模式那点事上做的笔记
一.适配器模式
1.设计思路
既有的软件结构具有稳定运行的基础,但是却无法直接利用到新的程序当中,这时就需要一个适配器,在原有内容和新的结果之间沟通,从而达到预期的效果
(1)设计图
即便没有12v的正常电源,在使用一个电源适配器和220v可以达到12v电源的效果,外部应用程序根本不知道
实现思路:添加一个AdapterPower12实现Power12类,可传入电压,无论传入的是Power220或是其他电压,均转换为同样的IPower12的对象
3.对象适配器和类适配器
普遍的采用对象适配器,类适配器使用继承的方式,是一种强关联关系,对象适配器是利用组合的方式,外部对象传入,是一种弱关联关系、根据情况加以选择,提倡使用对象适配器。利于解耦合。
4.设计原则
(1)使用对象组合,面向接口和抽象编程
(2)开-闭 原则
5.使用场合
(1)软件系统结构需要升级或扩展,又不想影响原有系统的稳定运行的时候
(2)转换类之间的差别不太大的时候
(3)想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作的时候
注:
适配器模式和桥接模式
适配器模式是将一种模式转换为另外一种接口的设计模式,桥接模式则是连接功能部分和实现部分的设计模式,桥接模式的用意是要把实现和他的接口分开,以便他们可以独立的变化。桥接模式并不是用来把一个已有的对象接到不相匹配的接口上的,两者是有区别的。
适配器模式和装饰者模式
适配器模式主要是用来填补两个接口之间的差异,而在后面将要讲解的装饰者模式,则是不需要更改接口,就可以新增功能的设计模式。
6.jdk中的使用
java.io.InputStreamReader和java.io.OutputStreamWriter就是比较典型的适配器。
二.桥接模式
1.设计思路
家里有许多灯,难道要把墙刨开重新设计线路,才能装上水晶灯吗?开关连接电源,电线传输电源,点灯只负责照明,各司其职,互不干扰。桥接模式即是如此。
2.设计原则
(1)抽象化,尽量使用对象聚合弱关联,避免使用继承强关联
(2)实现化
抽象化给出的具体实现,就是实现化
(3)脱耦
所谓耦合,就是两个实体的行为的某种强关联。将它们之间的强关联去掉,就是耦合的解脱,将两个角色之间的继承关系改为聚合关系,就是将它们之间的强关联改为弱关联。
一般化分析
将开关和点灯分开。开关控制点灯,开关是个基类。类含有开灯,关灯,照明等抽象方法,开需要一个开灯照明的方法提供给外部应用调用,然后,点灯集成开关基类,具体实现开灯关灯等抽象方法。告诉外部应用现在使用的哪盏灯。
既是在开关的构造方法中引入电灯接口,开关照明通过电灯接口实现。不关心电灯的内部逻辑,实现弱关联
桥接模式的静态类图
3.使用场合
(1)不希望在抽象类和它的实现部分之间有一个固定的绑定关系
(2)类的抽象及实现都应该可以通过生成子类的方法加以扩充
(3)对一个抽象的实现部分的修改应对客户不产生影响,即客户的代码不必重新编译
4.jdk中的使用
java.util.logging中的Handler和Formatter类。就是典型的桥接模式
三.组合模式
1.设计思路
组合模式,将对象组合成树状结构以表示部分-整体的层次结构。组合模式又称部分-整体模式。计算机有目录和文件组成,每个目录都可以是文件,也可以是目录,采用递归的结构来组织,公司存在管理者和普通员工的结构
图中新建一个抽象类,管理者和普通员工均去实现员工
2.设计原则
(1)统一对待个别对象和组合对象
组合模式使我们在处理树状结构的问题中,模糊了简单元素和复杂元素的概念,是客户程序可以像处理简单元素一样来处理复杂元素,从而使客户程序与复杂元素的内部结构解耦。
(2)面向抽象编程
在此例子中,无论是管理者还是普通员工,都是通过员工基类staff进行定义的,这可以大大减少类型不同造成的程序复杂度,组合模式的优势就是在于对单一对象和复杂组合对象都能做到一视同仁,使应用请求不在复杂。
3.使用场合
(1)想表示对象的部分-整体层次结构的时候
(2)希望用户忽略组合对象与单个对象的不同,用户将统一使用组合结构中的所有对象的时候
4.jdk中的使用
java.awt.Container就是很好的组合模式
四.装饰者模式
1.设计思路
将普通馒头加入染色剂,变成染色馒头在卖出去。即在不改变原类文件和使用继承的情况下,动态的扩展一个对象的给你给你个,通过创建一个包装对象,也就是装饰来包裹真实的对象。
简单设计实现
通过定义接口,正常实现一个正常馒头,染色馒头继承正常馒头,都是同一个接口,只是new出来不一样
//正常馒头
IBread nomarl=new NormalBread();
//染色馒头
IBread Corn=new CornBread
抽象实现
为了装饰正常馒头NormalBread,我们需要一个和正常馒头一样的抽象装饰者,AbstractBread,该类和正常馒头一样实现IBread馒头接口。不同的是,抽象类含有一个IBread接口类型的私有属性bread,通过构造方法,将外部IBread接口类型传入,调用不同装饰者实现的方法
//正常馒头
IBread normal=new NormalBread();
//甜蜜素馒头
normal=new SweetBread(normal);
//染色馒头
normal=new CornBread(normal);
包含关系如下图
(1)被装饰者抽象Component:是一个接口或抽象类,就是定义最核心的对象,也是最原始的对象。在装饰模式中,必然有个被提取出来的最核心,最原始,最基本的接口或抽象类,这个类就是装饰者的基类,例如:Ibread接口
(2)被装饰者具体实现ConcreteComponent:这是Component类的一个实现类,我们要装饰的就是这个具体的实现类,例如:本例中正常馒头类NormalBread
(3)装饰者Decorator:一般是一个抽象类,作用是实现接口或者抽象方法,他里面必然有一个指向Component变量的引用,例如:本例中抽象装饰者AbstractBread
(4)装饰者实现ConcreteDecorator1和ConcreteDecorator2:是具体的装饰者类,用来装饰最基本的类。例如:本例中染色装饰者和甜蜜素装饰者
2.设计原则
(1)封装变化部分
合理的利用类继承和组合的方式,灵活的表达了对象之间的依赖关系。装饰者模式应用中“变化”的部分是组件的扩展功能,装饰者和被装饰者完全隔离开来,这样我们就可以任意的改变装饰者和被装饰者。
(2)开-闭原则
对客户端应用程序来说不需要关心内部的实现细节,只要能动态的扩展,实现我们需要的功能即可。并且对原有的软件功能结构没有任何影响
(3)面向抽象编程,不要面向实现编程
不是指具体的抽象类,而是指具有抽象行为的接口或抽象类,在装饰者模式中,装饰者角色就是抽象类实现,面向抽象编程的好处就在于起到了很好的接口隔离作用。在运行时,我们具体操作的也是抽象类型引用,这些都显示了面向抽象编程
(4)优先使用对象组合,而非类继承
装饰者模式合理的使用了对象组合的方式,通过组合灵活的扩展了组件的功能,所有的扩展功能都是通过组合而非继承获得的,降低软件之间的耦合度
3.使用场合
(1)当我们需要为某个现有的对象动态的增加一个新的功能或职责时,可以考虑使用
(2)当某个对象的职责经常发生变化或经常需要动态的增加职责,为了避免变化而增加继承子类扩展的方式,因为这种方式会造成子类膨胀的速度过快,难以控制,此时可以使用装饰者模式。
4.jdk中的使用
java.io
java.io.InputStream
java.io.OutputStream
java.io.Reader
java.io.Writer
扩展:
(1)与适配器模式:装饰者模式是在不改变原有内容的基础上,动态的增加新的行为。而适配器模式则主要用来填补两个接口之间的差异,将一个接口转化为另外一个接口
(2)与策略模式:策略模式是以切换运算法则的方式变换功能的
五.外观模式
1.设计思路
是软件工程中常用的一种软件设计模式,是为子系统中的一组接口提供一个统计的高层接口,是子系统更容易使用。外观模式通过一个外观接口/写子系统中各接口的数据资源,而客户可以通过外观接口读取内部资源库,不与子系统产生交互。又外观处理内部各种关系,提供较高级别的简单接口,外部引用只要请求外观接口就能得到预期的结果。从而不再因为类之间的复杂关系而烦恼。还能兼容系统内部各个功能和互动关系及调用的顺序。
即做一个糖醋排骨,我们需要切肉,炒菜,出锅等,我们需要自己做并把握顺序,外观模式即是厨师,帮我们完成这个步骤。
2.设计原则
(1)迪米特法则——最少知识原则
意思是说一个软件实体应当尽可能少的与其他实体发生相互作用。如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用,如果一个类需要调用另一类的某个方法,可以通过第三个类转发这个调用。根本思想是强调了类之间的松耦合,类之间的耦合越弱,越有利于复用,将客户从一个复杂子系统中解耦。
(2)封装变化部分
外观模式有效的隔离了内部复杂子系统和外部应用请求,封装了对内部子系统的接口调用,使得外部应用调用变得更加方便,简单。
3.使用场合
(1)一个软件系统的复杂度比较高,需要一个更高级别的简单接口简化对子系统的操作时
(2)当使用端与实现类之间有太多的相依性,需要降低使用端与子系统或子系统间的耦合性,增加子系统的独立性时
(3)当子系统是相互依存的,需要层级化子系统,简化子系统之间的相依性的时候,可以使用外观模式
外观模式中的角色
(1)外观角色(Facade):构成系统内部复杂子系统的单一窗口,对系统外部提供更高一级的接口API
(2)子系统角色:系统内部各种子系统各司其职,做好“本职工作”。外观角色的存在对他们并不产生影响。他们听从外观角色的调用,但不会反过来调用外观角色。
(3)客户端角色:作为外部应用的客户端角色,不关心系统内部复杂子系统的运作情况,而只与外观角色之间通信获得数据和结果
4.jdk中的使用
大部分都存在,比如java.util.logging.Logger
扩展:
(1)与抽象工厂模式:抽象工厂模式可以理解为与产生复杂对象有关的外观模式,因为在抽象工厂模式中提供更高一级的调用接口API,供外部应用调用获得对象实例。
(2)与单例模式:外观模式与单例模式联系的比较紧密,因为一般情况下,外观模式经常被设计为单例模式运作,供外部应用调用。
(3)与中介者模式:中介者模式与外观模式比较相似,都充当了“中间者”的身份。不同的是,外观模式是外观单方面调用其他参与者形成更高一级的接口,供外部应用调用。而中介者模式则是扮演系统中各个参与者的中间人,封装各个参与者之间的交互。中介者模式使得系统中各个对象之间不需要显式的相互调用,从而使系统的各个参与者相互解耦。
六.享元模式
1.设计思路
享元模式的方式是以共享的方式高效的支持大量的细粒度对象。通过复用内存中已存在的对象,降低系统创建对象实例的性能消耗。在面向对象的眼中,万物万事一切皆对象,但不可避免的是,采用面向对象的编程方式,可能会增加一些资源和性能上的开销。不过在大多数情况下,这种影响还不是太大。但在特殊情况下,大量细粒度对象的创建,销毁以及存储所造成的资源和性能上的损耗,可能会在系统运行时形成瓶颈,这时,便需要享元模式的解决方案
比如下五子棋,五子棋中只有黑白两种棋子,持续不断new便会有开销,其实只需记录五子棋的黑白以及位置,而黑白两色的对象却是不变的。
如何实现棋子的位置
享元对象共享了元对象,节省了内存空间。然而棋子对象是可以共享的,但是每个棋子的位置是不一样的,是不能共享的。这也是享元模式的两种状态:内蕴状态和外蕴状态
内蕴状态:
享元模式的内蕴状态是不会随环境的改变而改变的,是存储在享元对象内部的状态信息,因此内蕴状态是可以共享的,对于任何一个享元对象来讲,它的值是完全相同的。就像黑子和白子,它代表的状态就是内蕴状态
外蕴状态:
享元对象的第二类状态称为外蕴状态,它是会随环境的改变而改变的,因此是不可以共享的状态,对于不同的享元对象来讲,它的值可能是不同的。享元对象的外蕴状态必须由客户端保存,在享元对象被创建之后,需要使用的时候在传入到享元对象内部。就像五子棋的位置信息,代表的状态就是享元对象的外蕴状态。
五子棋的抽象类
pulic abstarct class AbstractChessman{
protect int x;
protect int y;
protect String chess;
//构造方法
public AbstartChessman(String chess){
this.chess = chess;
}
//实现位置
public abstract void point(int x,int y);
public void show(){
System.out.println(this.x+","+this.y);
}
}
五子棋黑棋
public class Black extends AbstractChessman{
public Black(){
super("B");
}
//黑子实现位置
@Override
public void point(int x,int y){
this.x = x;
this.y = y;
this.show();
}
}
享元类,一个工厂类,利用单例模式
public class FiveChessmanFactory{
private static FiveChessmanFactory five=new FiveChessmanFactory();
//缓存存放共享对象
private final Hashtable<Character,AbstractChessman> cache=new Hashtable<Character,AbstractChessman>();
//私有化构造方法
private FiveChessmanFactory(){};
//获得单例工厂
public static FiveChessmanFactory getInstance(){
return fiveChessmanFactory;
}
//根据字符获得棋子
public AbstractChessman getChess(char c){
AbstractChessman abstractChessman=this.cache.get(c);
if(abstractChessman == null){
switch(c){
case 'B':
abstractChessman = new Black();
break;
case 'C':
abstractChessman =new White();
break;
default:
break;
}
}
if(abstractChessman != null){
//放入缓存
this.cache.put(c, abstractChessman);
}
//返回棋子对象
return abstractChessman;
}
}
最后测试享元生成,得到黑子白字只会生成一次。但位置可以不断累加
2.设计原则
(1)共享细粒度对象,降低内存空间
享元模式的重点在于“共享对象实例,降低内存空间”。不是一需要对象实例就通过new去创建对象,如果可以利用其它对象实例,则应共享对象实例。共享细粒度对象,降低对象内存空间,提供系统性能。
(2)有效的隔离系统中变化部分和不变部分
在封装变化部分之前,我们应该做的是区分系统中哪些是变化部分,哪些是不变部分。只有有效的区分系统中的变化部分和不变部分,才能做到将变化部分隔离出来,然后做到对扩展开放,对修改关闭。在享元模式中,内蕴状态是不变部分,外蕴状态是变化部分。内蕴状态一旦被修改,就能反映到所有用该对象实例的其他地方,而外运状态则是在客户端传入的,是随时变化的内容。因此我们需要将内蕴状态和外蕴状态区别开,有效的利用系统内存空间,又能应对外部请求的变化。
3.使用场合
(1)当系统某个对象的实例较多的时候
(2)在系统设计中,对象实例进行分类后,发现真正有区别的分类很少的时候。
在生活中存在很多实例,例如,在使用拼音输入法的时候,每个字都是new一个对象实例的操作,那内存实例就太多了。一般是给出本地内存资源节省的一个方案,并不适合互联网上分布式应用的情况。不过享元模式对于排他性的要求资源的控制,是一个不错的选择。
4.享元模式中的角色
(1)抽象享元角色:此角色是所有享元类的超类,为这些类规定出需要实现的公共接口。需要外蕴模式的操作可以通过调用商业方案以参数形式传入。类似于抽象五子棋父类
(2)具体享元角色:实现抽象享元角色所规定的的接口。如果有内蕴状态,则必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使享元对象在系统内共享。类似于五子棋中的黑白棋,参与了五子棋的实现。
(3)享元工厂角色:负责创建和管理享元角色。必须保证享元对象可以被系统适当的共享。当一个客户端对象调用另外一个享元对象的时候,享元工厂角色会检查系统中是否有一个符合要求的享元对象。如果已经有了,享元对象就提供这个已有的享元对象,如果没有,就创建。其实就是个单例模式
(4)客户端角色:需要维护一个对所有享元对象的引用,本角色需要自行存储所有享元对象的外蕴状态。类似于查看的客户端。
5.jdk中的使用
如java.lang.Integer中的valueOf方法就是享元模式的具体应用。首先判断参数的范围,如果在缓存范围内,则返回缓存的内容,否则创建一个新对象返回。而缓存类IntegerCache作为Integer类的内部类,已经在类初始化的时候,设置了Integer数组缓存cache[]内容
扩展:
(1)与组合模式:可以使用享元模式共享组合模式中的叶子节点,从而提高系统的处理效率
(2)与单例模式:在享元模式中,一般都是将享元模式设置为单例模式,以降低系统使用空间。除此以外,单例模式本身就是一种享元模式。单例模式只有一个对象实例,被其他对象所共享。
七.代理模式
1.设计思路
代理模式,是一种软件设计模式中的一项基本技巧,在代理模式中,有两个对象参与处理同一请求,接受的请求由代理对象委托给真实对象处理,代理对象控制请求的访问,它在客户端应用程序与真实目标对象之间起到了一个中介桥梁的作用,代理模式使用对象聚合代替继承,有效的降低了软件模块之间的耦合度。
举个例子:购买红酒,我们并不是直接去红酒厂家买红酒,而是通过红酒的代理商完成的购买
红酒厂商提供生产接口,然后有具体的实现接口。红酒的代理商实现红酒厂商的接口并重写方法,以下提供后就代理商的代码
public class RedWineProxy implements IRedWine{
//真正的红酒生产厂商
private final IRedWine redWine;
//代理商出售的红酒的权限
private final boolean permission = true;
//默认构造方法
public RedWineProxy(IRedWine redWine){
this.redWine = redWine;
}
//代理商生产红酒方法,只是从生产工厂拿酒销售
@Override
public void product(){
this.redWine.product();
}
//代理商销售红酒
@Override
public void sell(){
this.redWine.sell();
}
}
通过上述例子,代理模式涉及以下几个角色
(1)抽象角色:声明真实对象和代理对象的共同接口
(2)真实角色:真正处理请求的目标对象
(3)代理角色:代理对象角色内部含有真实对象的引用,从而代理对象可以将请求转为真实对象处理。同时,代理对象在执行真实对象操作的前后还可以添加附加操作。
2.设计原则
(1)延迟加载,提高系统效率
一个系统,特别是大的系统,如果在初始化的时候加载资源过多,会让用户等待时间过长,容易引起用户使用的不满。代理模式具有延迟处理的效果。代理对象和真实对象具有同样的接口,当我们进行某一项具体功能调用时,可以使用代理对象预先加载资源,初始化数据,初始化成功后再对真实对象进行调用,这样就免去了用户等待的抱怨,提高了系统的访问效率,代理模式的另一个优点,可以在调用真实对象处理之前进行校验工作,有效的将真实对象功能进行分离,降低模块的耦合度。如果新增了功能处理,则无须修改真实对象功能,只修改代理对象功能即可实现。例如:某些注册环节最终是插入操作,判断逻辑可以添加在代理角色中,这样换另外一种注册方法也可以使用真正的添加功能。
(2)单一职责原则
代理模式使用代理对象将真实对象分离开,可以在对象中做权限判断处理及其他一些逻辑功能,而具体的动作执行仍是调用真实对象来处理。这就使得代理对象和真实对象的功能职责单一化。分离了不同处理行为,降低了功能模块之间的耦合度,也降低了程序的复杂度,方便了后期软件的维护工作。在代理模式中,代理对象和真实对象实现了同样的接口,是面抽象的编程,也符合设计模式的依赖颠倒原则,即依赖抽象,而不是依赖具体实现类。
3.使用场合
(1)远程代理:为一个对象在不同地址空间提供局部代理
(2)虚拟代理:若一个对象的创建非常耗时,可以通过代理对象去调用,在真实对象创建前,返回一个假的调用,等真实对象创建好了,这时返回给客户端的就是一个真实对象的相应方法调用。
(3)保护代理:控制对原始对象的访问。
(4)只能指引:取代了简单的指针,它在访问对象时执行一些附加操作。
在代理模式中,代理对象和真实对象参与处理同一请求,接收的请求由代理对象委托给真实对象处理,代理对象控制请求的访问,它在客户端应用程序与真实目标对象之间起到了一个中介桥梁的作用。代理模式使用对象聚合代替继承,有效的降低了软件模块之间的耦合度。
4.jdk中的使用
在jdk中引用的动态代理基础,就是一个系统在没有统一接口的情况下,在运行时,动态的对那些接口不同的对象提供代理支持。动态代理的核心是java.lang.reflect.InvocationHandler接口,要使用动态代理就必须实现该接口。这个接口的委托任务是在invoke(Object proxy,Method m,Objectp[] args)方法里实现的。
jdk中的动态代理是在运行时生成代理对象,在使用java.lang.reflect.Proxy生成动态代理对象时必须提供一组代理对象需要实现的接口,以及实现java.lang.reflect.InvocationHandler接口的真实处理对象handler给它,然后通过Proxy.newProxyInstance的调用,动态生成的代理对象就实现了提供的这些接口,当然,可以将该动态代理对象作为其中任意一种接口来使用。注意:这个生成的动态代理对象不会做实质性的工作。实际的工作是由真实对象handler来执行的。
java的实现:
//要代理的接口
public interface ITarget{
//操作方法
public void operation();
}
//实际的处理类,由于使用动态代理,不需要实现ITarget接口,而是实现InvocationHandler接口
public class TargetImpl implements InvocationHandler{
//实现方法
@Override
public Object invoke(Object proxy,Method mothed,Object[] args) throws Throwable{
System.out.println("--method:"+method.getName());
System.out.println("--动态代理类实现");
return null;
}
}
//目前为止,ITarget和TargetImpl实现类没有任何关联,下面就需要动态代理ITarget接口了
public static void main(String[] args){
//真实处理对象
InvocationHandler handler=new TargetImpl();
//创建代理类实例对象
ITarget target = (ITarget)Proxy.newProxyInstance(ITarget.class.getClassLoader(),new Class[]{ ITarget.class },handler);
//操作方法
target.operation();
}
在客户端应用程序中,我们首先创建了一个真实的处理对象handler,当然,其类型是InvocationHandler。然后开始动态的创建代理对象,代理接口就是ITarget,实际代理的类型就是ITarget类型。最后调用operation方法结果是:
--method:operation
--动态代理类实现
动态代理内部是使用反射机制来完成动态代理功能的。动态代理即是面向方面编程,即AOP。上面的例子,TargetImpl是最终的行为执行体,在做这个核心动作之前和之后,是不是可以进行额外的控制处理呢?AOP的思想就是面向方面编程,动态代理使用的就是实现AOP的基础。在上面的示例中,我们完全可以抽象出一个动态代理基类,由基类完成核心功能,那样我们就能处理其他方面,而不至于影响核心功能的逻辑。在springAOP中既是这种思路,基类既是各个controller类,即最终执行体,而handler既是代理类
扩展:
与装饰者模式:在装饰者模式中也是实现了相同的接口,来修饰被装饰者,从这一点来说很相似。但装饰者模式主要是附加其他的功能,用于修饰被装饰者。而代理模式强调的是代理本人作业,减少对实际对象的操作,而附加功能不是代理模式的重点。