前面三期我们主要介绍了4中创建型模式:单例模式、工厂模式、建造者模式、原生模式。这周我们开始进入下一大块儿的模式学习——结构性模式。
从程序的结构上实现松耦合,从而可以扩大整体的类结构,用来解决更大的问题。
适配器模式、代理模式、桥接模式、组合模式、装饰模式、外观模式、享元模式
将一个类的接口转换成客户希望的另外一个接口。adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。举个生活中的常见例子,读卡器是作为内存卡和笔记本之间的适配器。我们将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。
(1)目标接口(Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口
(2)需要适配的类(Adaptee):需要适配的类或适配者类。
(3)适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成目标接口。
(1)经常用来做旧系统改造和升级。
(2)如果我们的系统开发之后再也不需要维护,那么很多模式都是没有必要的,但是,维护一个系统的代价往往是开发一个系统的数倍。
(3)适配器不是在详细设计时考虑的,而是解决正在服役的项目的问题。
假设我们现在有一台年代久远的电脑,只能读取SD卡中的内容,然而随着时间飞逝,出现了TF卡,同样想要在这台电脑上读取卡中的内容,那么我们就需要使用适配器作为一个中转,使得此台电脑还可以读取TF卡中的内容。
(1)我们先定义一个SD卡接口
public interface SDCard { void readMessge();}
(2)实现SD卡接口的一个具体类
public class SDObject implements SDCard{ @Override public void readMessge() { System.out.println("I am SDObject!"); }}
(3)我们再定义一个电脑接口,只能读取SD卡
public interface Computer { void readSD(SDCard sdCard);}
(4)实现电脑接口,创建一个具体实现类
public class ComputerObj implements Computer{ @Override public void readSD(SDCard sdCard) { if (sdCard == null) { try { throw new Exception(); } catch (Exception e) { e.printStackTrace(); } }else { sdCard.readMessge(); } }}
(5)此时,我们就已经完成了一个只有SD卡接口的电脑的创建。现在如果我们需要再增加一个读取TF卡内容的功能,那么我们就需要使用相应的适配器来完成这种功能。首先还是需要创建一个TF卡的接口。
public interface TFCard { void readMessage();}
(6)实现TF接口,并且创建一个具体的实现类
public class TFObject implements TFCard{ @Override public void readMessage() { System.out.println("I am TFObject!"); }}
(7)创建一个适配器接口。此接口需要和电脑的SD卡接口对接,所以需要实现SDCard接口,而传输的内容却来自于TFCard,所以在适配器的内部,需要增加一个TF卡对象作为私有属性,在适配器的内部进行真实数据的传输。
public class TFadapteeSD implements SDCard { private TFCard tf; public TFadapteeSD(TFCard tf) { super(); this.tf = tf; } @Override public void readMessge() { if(tf == null) { try { throw new Exception(); } catch (Exception e) { e.printStackTrace(); } }else { tf.readMessage(); } }}
(8)最后我们可以对上述的适配器模式进行一个简单的测试
public class Demo { public static void main(String[] args) { //直接利用电脑的SD卡接口,读取SD卡内容 Computer c = new ComputerObj(); SDCard sd = new SDObject(); c.readSD(sd); //使用新增的适配器来完成TF卡的读取操作 TFCard tf = new TFObject(); TFadapteeSD tas = new TFadapteeSD(tf);//创建一个适配器 c.readSD(tas); }}
查看一下结果:
tips:首先使用Computer对象c读取SDCard对象sd的内容,可以兼容。后面又创建一个TFCard对象tf,通过适配器,使得最后c也读取到了对象tf的内容。适配器模式完成了两个不同接口的对接。
适配器模式属于一种补救模式,在一个系统中,如果大量的出现适配器,会导致整个系统的逻辑及其混乱。因为系统调用的适配器接口,其真实内部的调用内容源自于其他接口,这就使得整体的系统逻辑分析和判断十分麻烦。所以在一个系统最初的设计时,并不会去考虑使用适配器,只有在后期系统功能的扩展时,为了达到不去更改源代码的目的,才会适当的增加一定量的适配器,来使得系统兼容新的产品类信息。
(1)通过代理,控制对对象的访问.
(2)可以详细控制访问某个(某类)对象的方法,在调用这个方法前做前置处理,调用这个方法后做后置处理。(即:AOP的微观实现)
(3)AOP(Aspect Oriebted Programming 面向切面编程)的核心实现机制。
(1)抽象角色:定义代理角色和真实角色的公共对外方法。
(2)真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。关注真正的业务逻辑。
(3)代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。将统一的流程控制放到代理角色中处理。
(1)安全代理:屏蔽对真实角色的直接访问。
(2)远程代理:通过代理类处理远程方法调用(RMI)
(3)延迟加载:先加载轻量级的代理对象,真正需要再加载真实对象。
(1)静态代理(静态定义代理类):属于我们自己定义好的代理类
(2)动态代理(动态生成代理类):属于程序自动生成的代理类(一般都使用动态代理)
以一个歌手和其经纪人为背景。此时歌手就属于真实角色,而经纪人就属于代理角色。在一场活动中,会有很多流程需要走,但是只有唱歌这一个环节需要歌手真实的出面完成任务。所以我们首先定义一个接口,包含有整个演唱会所需要的全部流程,然后对于真实角色和代理角色分别去进行实现这些方法。
(1)流程接口
public interface Star { void confer();//面谈 void signContract();//签约 void bookTicket();//订票 void sing();//唱歌 void collectMoney();//收尾款}
(2)真实角色实现接口中的全部方法
public class RealStar implements Star{ @Override public void confer() { System.out.println("RealStar.confer()"); } @Override public void signContract() { System.out.println("RealStar.signContract()"); } @Override public void bookTicket() { System.out.println("RealStar.bookTicket()"); } @Override public void sing() { System.out.println("RealStar.sing()"); } @Override public void collectMoney() { System.out.println("RealStar.collectMoney()"); }}
(3)代理角色实现接口中的全部方法
public class ProxyStar implements Star { private RealStar realstar; public ProxyStar(RealStar realstar) { super(); this.realstar = realstar; } @Override public void confer() { System.out.println("ProxyStar.confer()"); } @Override public void signContract() { System.out.println("ProxyStar.signContract()"); } @Override public void bookTicket() { System.out.println("ProxyStar.bookTicket()"); } @Override public void sing() { realstar.sing(); } @Override public void collectMoney() { System.out.println("ProxyStar.collectMoney()"); }}
tips:在代码中我们可以看到,代理角色中的所有方法都是自己的方法,唯独在sing方法上,代理角色调用的是真实角色的sing方法。
(4)对代理模式进行测试
public class Client { public static void main(String[] args) { RealStar realStar = new RealStar(); ProxyStar proxyStar = new ProxyStar(realStar); proxyStar.confer(); proxyStar.signContract(); proxyStar.bookTicket(); proxyStar.sing(); proxyStar.collectMoney(); }}
我们来查看一下结果:
tips:在所有的流程中,只有sing()方法的真实调用时RealStar,其他的所有方法都是属于代理角色中的方法。所以在真个流程中,只是将最重要的sing()交给RealStar,其他方法代理角色全部代替,这就属于是代理模式。
在静态代理的基础上,我们保留真实角色RealSatr和Star接口,然后创建一个类StarHandler,并且实现InvocationHandler接口
public class StarHandler implements InvocationHandler{ Star realStar; public StarHandler(Star realStar) { super(); this.realStar = realStar; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object object = null; System.out.println("真正的方法执行前!"); System.out.println("面谈,签合同,预付款,订机票"); if(method.getName().equals("sing")) { object = method.invoke(realStar, args); } System.out.println("真正的方法执行以后!"); System.out.println("收尾款"); return object; }}
测试代码:
public class Client { public static void main(String[] args) { Star realStar = new RealStar(); StarHandler handler = new StarHandler(realStar); Star proxy = (Star) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[] {Star.class }, handler); proxy.sing(); }}
查看结果:
tips:在使用动态代理的时候,我们将所有代理角色的活动可以提前写在StarHandler中的invoke方法中,然后在客户端创建handler对象,将其传入程序自己生成的代理对象proxy中。在proxy调用star方法的时候,会在内部调用invoke方法,程序按照invoke方法中的流程依次执行。
抽象角色中(接口)声明的所有方法都被转移到一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。
商城系统中常见的商品分类,以电脑为类,如何良好的处理商品分类的问题?我们可以用多层继承结构实现,例如:
上图显示了一个电脑分类的现状,分别有戴尔和联想两个品牌,每个品牌都有三个产品,笔记本,台式机,iPad。所以在使用多层集成结构的时候,总共需要6个类,假若需要再增加一个三星系列,那么首先需要增加一个三星品牌类,然后再在这个品牌类下面增加3个产品系列。这样的结构会面对着几个明显的问题。
(1)扩展问题(类个数膨胀问题)
如果要增加一个新的电脑类型:智能手机,则要增加各个品牌下面的类;如果要增加一个新的品牌,也要增加各种电脑类型的类。
(2)违反单一职责原则
所有的子类都含有品牌和机型两个变化因素,有两个引起这个类变化的原因。
为了解决上面的问题,我们提出了桥接模式
处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的集成结构,使各个维度可以独立的扩展在抽象层上建立关联。
我们将每一个维度设计成为独立的集成结构,分析上面的场景,拥有两个维度,分别是品牌和电脑类型,所以我们分析之后,可以分别建立品牌维度——联想、戴尔,以及电脑类型维度——笔记本电脑、台式机、iPad。下面我们依次实现这两个维度。
(1)品牌维度
public interface Brand { void sale();}
class Lenovo implements Brand{ @Override public void sale() { System.out.println("销售联想电脑!"); }}
class Dell implements Brand{ @Override public void sale() { System.out.println("销售戴尔电脑!"); }}
(2)电脑类型维度
public class Computer { protected Brand brand; public Computer(Brand brand) { super(); this.brand = brand; } public void sale() { brand.sale(); }}
class Desktop extends Computer{ public Desktop(Brand brand) { super(brand); } @Override public void sale() { super.sale(); System.out.println("销售台式机!"); }}
class Laptop extends Computer{ public Laptop(Brand brand) { super(brand); } @Override public void sale() { super.sale(); System.out.println("销售笔记本电脑!"); } }
class Ipad extends Computer{ public Ipad(Brand brand) { super(brand); } @Override public void sale() { super.sale(); System.out.println("销售平板电脑!"); } }
(3)使用客户端进行检测
public class Client { public static void main(String[] args) { Brand brand = new Lenovo(); Computer c = new Desktop(brand); c.sale(); Computer c2 = new Laptop(new Dell()); c2.sal
tips:
1.在整个桥接模式中,我们分别以Brand和Computer建立了两个不同维度的类。在Computer类中,自定义一个brand属性,然后在sale方法中调用brand的sale方法, 并且增加自己独有的方法。通过这样的做法,就可以很容易的对类进行扩展。假如需要新增华硕的笔电、台式机、iPad时,仅需要在Brand中增加一个华硕品牌就好了,并不需要对Computer类进行任何的改动。这样就使得桥接模式具有高可扩展性。
2.使用桥接模式之后,各个类之间的关系如下所示:
(1)桥接模式可以取代多层继承的方案。多层继承违背了单一职责原则,复用性较差,类的个数也非常多。桥接模式可以极大的减少子类的个数,从而降低管理和维护的成本。
(2)桥接模式极大的提高了系统可扩展性,在两个变化维度中任意扩展一个维度买都不需要修改原有的系统,符合开闭原则。
把部分和整体的关系用树形结构来表示,从而使客户端可以使用统一的方式处理部分对象和整体对象。
(1)抽象构件角色:定义了叶子和容器构件的共同点
(2)叶子构件角色:无子节点
(3)容器构件角色:有容器特征,可以包含子节点,一般容器构件中会拥有一个list容器,进行存储所有的叶子节点,并且在查找文件的时候使用遍历该list的方法进行搜索。
(1)组合模式为处理树形结构提供了完美的解决方案,描述了如何将容器和叶子进行递归组合,使得用户在使用时可以一致性的对待容器和叶子。
(2)当容器对象的而制定方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员,并调用执行。其中,使用了递归调用的机制对整个结构进行处理。
在我们使用杀毒软件的时候,对每个文件夹下面的每个文件进行杀毒处理时,也属于树形结构的处理,一般也是利用组合模式对所有的文件进行处理。我们利用这个背景,来对组合模式进行一个模拟。
(1)首先我们需要建立一个抽象构件,提供一个处理所有文件的方法,然后再建立相应的文件类型(相当于叶子构件)实现抽象构件,最后再建立文件夹(相当于容器构件)存放文件,这样就可以形成一个树形结构。
public interface AbstractFile { void killVirus();}
//文本文件杀毒class TextFile implements AbstractFile{ private String name ; public TextFile(String name) { super(); this.name = name; } @Override public void killVirus() { System.out.println("对文本文件:"+name+",进行杀毒!"); }}
//图片文件class ImgFile implements AbstractFile{ private String name; public ImgFile(String name) { super(); this.name = name; } @Override public void killVirus() { System.out.println("对图片文件:"+name+",进行杀毒!"); }}
//视频文件class VideoFile implements AbstractFile{ private String name; public VideoFile(String name) { super(); this.name = name; } @Override public void killVirus() { System.out.println("对视频文件:"+name+",进行杀毒!"); }}
//文件夹class Folder implements AbstractFile{ private String name; private List<AbstractFile> list = new ArrayList<AbstractFile>(); public Folder(String name) { super(); this.name = name; } public void add(AbstractFile a) { list.add(a); } public void remove(AbstractFile a) { list.remove(a); } public AbstractFile getChild(int index) { return (AbstractFile) list.get(index); } @Override public void killVirus() { System.out.println("----------开始对文件夹“"+name+"”进行杀毒----------"); for(AbstractFile temp:list) { temp.killVirus(); } }}
tips:
1.在模拟这个背景的时候,首先构建了一个AbstractFile接口,在其中定义了一个killVirus方法,充当抽象构件的角色。然后我们建立了三个类:文本文件,图片文件,视频文件。使用这三个类充当我们组合模式中的叶子节点。最后又创建了一个文件夹类,充当容器构件的角色。
2.在Folder类中,我们在killVirus方法中进行了一个递归操作,当文件夹下面拥有文件夹时,会直接进行递归操作,再次调用子类文件夹的killVirus方法。
3.在Folder类中,我们使用了一个List容器来存储Folder中的每一个叶子节点,在遍历的时候更加方便。
(2)简单的测试一下
public class Client { public static void main(String[] args) { AbstractFile f1,f2,f3,f4; f1 = new TextFile("歌词.txt"); f2 = new ImgFile("风景.img"); f3 = new VideoFile("雷神.avi"); f4 = new VideoFile("钢铁侠.avi"); Folder f5 = new Folder("漫威电影"); Folder f6 = new Folder("全部文件"); f5.add(f3); f5.add(f4); f6.add(f1); f6.add(f2); f6.add(f5); f6.killVirus(); }}
查看结果:
tips:
(1)在代码中我们一共创建了4个文件,以及两个文件夹,依次使用add方法将所有的文件与文件夹进行存储操作,最后形成一个树形结构。
(2)通过代码我们可以看出,Folder对象f6属于整个树形结构的根节点,f1,f2,f3,f4,属于叶子节点,f5属于一个容器节点。所以f6中,存放有两个文件以及一个文件夹。
(3)在最后调用f6的killVirus方法的时候,程序直接将内部的所有文件全部进行了遍历,这就是组合模式的一种优点整体和局部的操作方法是一样的。这样客户端不论是处理单独的文件,还是处理文件夹,都是调用killVirus方法,大大简化了客户端对不同AbstractFile的处理。