Java设计模式:桥接模式

问题提出

生活中有很多事物集合,设为A1,A2......Am ,而每个事物都有功能F1,F2....Fn.

例如邮局的发送业务。简单模拟有两类事物:信件和包裹,均有平邮和挂号邮寄功能。程序设计中如何来描述这些功能呢?或许一般会与下面代码相似。

public class A1 {
    void F1(){}
    void F2(){}
}
public class A2 {
    void F1(){}
    void F2(){}
}

很明显,若有m个事物,n个功能,按照这个方法,一共要写m个类,功能方法累积起来有m*n个。这样明显不太可取。那么为了更好地解决上面的问题,桥接模式就是一种重要的方法之一。

桥接模式是关于怎样将抽象部分与它的实现部分相分离,使它们都可以独立地变化的成熟模式。上面的方法的根本缺陷就是在具体类中封装了F1()或是F2()方法。因此必定会有重复的代码。解决这个问题的重要策略就是利用“语义”,通过接口或者抽象类进行抽象。

对面上面的例子,可以描述为:

1)邮局有发送功能;发送功能有两种方式,平邮和挂号。

public interface IPost {    // 邮局
    public void post();     //发送功能
}
public class SimplePost implements IPost {      //平信发送
    public void post(){
        System.out.println("This is simplePost");
    }
}

public class MarkPost implements IPost {        //挂号发送
    public void post(){
        System.out.println("This is Mark post");
    }
}

2)邮局发送的物品有两种:信件和包裹

public abstract class AbstractThing {       //抽象事物
    private IPost obj;                      //有抽象发送功能
    public AbstractThing(IPost obj){
        this.obj = obj;
    }
    public void post(){
        obj.post();
    }
}
public class Letter extends AbstractThing {
    public Letter(IPost obj){
        super(obj);
    }
}

public class Parcel extends AbstractThing {
    public Parcel(IPost obj){
        super(obj);
    }
}

其实这种设计是与现实生活中的描述是一致的,“邮局有邮寄功能”,“邮局可以邮寄信件和包裹”。信件和包裹是两个不同的事务,但是它们又有共享的功能,也可能有相异的功能。共享的功能能封装在一个类,但由于该类不能代表一个具体事物,所以定义为Abstract抽象类是最合适的。该类共享多态成员obj,表明了事物共享平邮和挂号发送功能,所以该类是桥接模式的核心!

具体UML图

大概是因为AbstractThing类中的成员obj,它像是桥梁一样,使事物类和功能类联系起来,所以这种模式被叫做桥接模式。

从上面的UML图可以看出来,创建一个具体事物类我们必须先选择一个具体的发送功能,再选择一个事物类,最后才完成真正发送过程。总结来说,桥接模式是一个多条件选择问题,而这些条件或有次序先后,或是没有。比如人们在买衣服的时候,分别再上衣和裤子中各自挑选,先挑裤子或是先挑衣服。再比如人们要从广州坐火车去西安,在北京中转。那么我们会先从广州到北京的火车挑选一班车,再从北京到西安挑选一班车。其实我们大多数下意识地处理这类问题的方法与桥接模式的设计很是相似。

而对于需求变化,对于事物,只需要删除或者添加Abstract派生的类即可。邮寄方式同理也是如此。

public class NewThing extends AbstractThing {
    public NewThing(IPost obj){
        super(obj);
    }
}

看到这里,桥接模式的基本概念就这么简单,但是实际应用中却很多细节需要注意和处理。上面只是开胃菜,下面让我们品尝桥接模式的主菜!

1.桥接模式强调“包含”代替“继承”

日志是一类非常重要的文件,现在要求实现两个功能    1)将信息字符串字节保存到日志       2)将加密后的字符串保存到文件中

方法1 继承

public class LogFile {              //将信息直接保存到日志文件中
    public void save(String msg){
    }
}
public class Encrypt extends LogFile {
    public void save(String msg){       //加密信息保存到文件
        msg = encrypt(msg);
        super.save(msg);
    }
    public String encrypt(String msg){
        //加密处理
        return msg;
    }
}

方法2 包含

public class Encrypt2 {
    LogFile lf;
    public Encrypt2(LogFile lf){
        this.lf = lf;
    }
    public void save(String msg){
        msg = encrypt(msg);
        lf.save(msg);
    }
    public String encrypt(String msg){
        return msg;
    }
}

那究竟方法1和方法2,哪种方法比较合适呢?对比邮递那个demo,不难看出方法2才是桥接模式的设计体现。为什么使用继承不恰当?因为当父类更改时,有可能会影响到Encrypt子类;而方法2中LogFile类,Encrypt类之间是包含关系。当LogFile类改变时,只要接口方法不改变,就不会影响Encrypt类。这是体现桥接模式的最基本思想!

2.JDK中的桥接模式

JDK中有很多应用桥接模式的地方,例如Collections类中的sort()方法。具体源码如下

Public static<T extends Comparable<? Super T>> voidsort(List<T> list){
    Object[] a = list.toArray();
    Arrays.sort(a);
    ListIterator<T> i = list.listIterator();
    for(int j=0;j<a.length;j++{
        I.next();
        i.set((T)a[j]);
    }
}    

这里或许有点难理解,集合和数组都需要排序,但是集合中的排序是通过使用数组的排序实现的。为什么呢?因为集合和数组没必要都实现排序,没必要去写重复的方法。于是使集合类中都包含了数组成员,通过先转换为数组进行排序,后再填充为集合对象,这种结构类似于桥接模式的结构。

3.桥接模式中利用反射机制

1)反射机制获得功能类

public abstract class ReflectAbstractThing {
    IPost obj;
    public ReflectAbstractThing(String reflectName) throws Exception{
        //利用反射机制加载功能类
        //注意类名必须是完整路径
        obj = (IPost)Class.forName(reflectName).newInstance();
    }
    public void post(){
        obj.post();
    }
}

2)反射机制获得功能类和事物类

public abstract class AbstractThing {
    IPost obj ;
    public void createPost(String funcName) throws Exception{
        //利用反射机制加载功能类
        obj = (IPost)Class.forName(funcName).newInstance();
    }
    public void post(){
        obj.post();
    }
}
public class ThingManage {
    AbstractThing thing;
    AbstractThing createThing(String thingName) throws Exception{
        //反射机制加载事物类对象
        thing = (AbstractThing)Class.forName(thingName).newInstance();
        return thing;
    }
}

若使用反射机制加载事物类对象,需要增加事物管理类ThingManage来创建事物类实例,另外还要注意这时候AbstractThing中并不是通过构造方法来加载功能类对象,而是普通的类方法。因为这时候需要先加载事物类对象,再加载功能类对象,而不能同时加载。

应用场景:编写功能类,要求能读本地或远程URL文件,文件类型是文本文件或是图像文件。

该功能可由桥接模式完成。事物类指本地文件以及URL文件类;功能类指读文本文件,读图像文件。下面提出两种实现方法

方法1

1)抽象功能类AbstractRead

public abstract class AbstractRead<T> {
    public abstract T read(String strPath) throws Exception;
}

2)具体功能实现类

public class ImgRead extends AbstractRead<byte[]> {
    public byte[] read(String strPath) throws Exception{
        return null;
    }
}

public class TextRead extends AbstractRead<String > {
    public String read(String strPath) throws Exception{
        return null;
    }
}

3)抽象事物类

public class AbstractThing{
    AbstractRead reader;
    public AbstractThing(AbstractRead reader){
        this.reader = reader;
    }
    Object read() throws Exception{
        return reader.read("s");
    }
}

这是按照上面桥接模式UML图转化过来写的。但是仔细分析,难免就发生问题了。在具体实现类TextRead和ImgRead中无法写代码。在TextRead中,多态方法read()参数是文件路径strPath。常规思路是必须根据strPath获得字节输入流InputStream对象,但是对于本地文件,URL文件获得InputStream对象是不一样的。

本地文件获取:InputStream in = new FileInputStream(strPath);

URL:URL u = new URL(strPath); InputStream in = u.openStream();

有人会觉得,直接在多态方法read()再添加一个标志位就可以了。如下

public String read(String strPath,int type) throws Exception{
        InputStream in = null;
        switch(type){
            case 1:
                in = new FileInputStream(strPath);break;
            case 2:
                URL u = new URL(strPath);
                in = u.openStream();
        }

        //其他代码
        return "";
    }

这样看起来好像真的解决了上面提到的问题,但是这样修改的话,老生常谈的道理就是那么在每个实现类(ImageRead)都需要重写一遍这样的代码。毋庸置疑,这种解决方法实不可取的。

又有人觉得既然本地读写文本,图像和URL上是不同,那么直接封装四个类不就可以吗? 如下

class TextRead extends AbstractRead<String>{}

class URLTextRead extends AbstractRead<String>{}

class ImgRead extends AbstractRead<String>{}

class URLImgRead extends AbstractRead<String>{}

出现这些代码根本原因是对桥接模式的理解不够深入。本文前面提到的邮局等样例只是为了更好的理解桥接模式的基本思想,但是在具体程序设计中需要根据细节仔细考虑。记住,功能类的抽取至关重要!!!必须把多个事物类完成某个功能类的共性部分抽取出来,才能作为桥接模式的功能类,这是理解桥接模式最重要的地方。

回到问题的最初,关键思路是弄清楚本地读文件,URL读文件功能类有什么异同之处。读文件一般有三步:打开文件,读文件,关闭文件。打开文件为了获取InputStream对象in,只有这一步与URL不同。另外,如果缓冲区大小与文件大小一致的,读文件的效率就搞,因此必须获得文件长度,这对本地,URL文件获取方法也是不一样的。

1)抽象功能类

public interface IRead<T> {
    public T read() throws Exception;
}

2)具体功能实现类

package BridgeModel.Example.ReadFileRight;

/**
 * Created by lenovo on 2017/4/20.
 */
public class TextRead implements IRead<String> {
    AbstractStream stream;
    public TextRead(AbstractStream stream){
        this.stream = stream;
    }
    public String read() throws Exception{
        byte buf[] = stream.readBytes();
        String s = new String(buf);
        return s;
    }
}

public class ImgRead implements IRead<byte[]> {
    AbstractStream stream;
    public ImgRead(AbstractStream stream){
        this.stream = stream;
    }
    public byte[] read() throws Exception{
        return stream.readBytes();
    }
}

这里内部封装自定义流类AbstractStream,它是动态变化的,可以指向本地文件,也可以指向URL文件。

public class AbstractStream {
    protected InputStream in;
    protected int size;
    protected byte[] readBytes() throws Exception{
        byte buf[] = new byte[size];
        in.read(buf);
        return buf;
    }
    public void close() throws Exception{
        in.close();
    }
}

package BridgeModel.Example.ReadFileRight;

import java.io.File;
import java.io.FileInputStream;

/**
 * Created by lenovo on 2017/4/20.
 */
public class NativeStream extends AbstractStream {
    //指向本地文件流
    public NativeStream(String strFile) throws Exception{
        File f = new File(strFile);
        size = (int)f.length();
        in = new FileInputStream(f);
    }
}

package BridgeModel.Example.ReadFileRight;

import java.net.HttpURLConnection;
import java.net.URL;

/**
 * Created by lenovo on 2017/4/20.
 */
public class URLStream extends AbstractStream {
    //指向URL文件流
    public URLStream(String strFile) throws Exception{
        URL url = new URL(strFile);
        in = url.openStream();
        HttpURLConnection urlcon = (HttpURLConnection)url.openConnection();
        size = urlcon.getContentLength();
    }
}

3)抽象事物类

public class AbstractThing {
    IRead read;
    public AbstractThing(IRead read){
        this.read = read;
    }
    Object read() throws Exception{
        return read.read();
    }
}

4)具体事物类

package BridgeModel.Example.ReadFileRight;

/**
 * Created by lenovo on 2017/4/20.
 */
public class NativeFile extends AbstractThing {
    public NativeFile(IRead read){
        super(read);
    }

}

public class URLFile extends AbstractThing {
    IRead read;
    public URLFile(IRead read){
        super(read);
    }
}

5)简单测试代码(record.txt自己随便写吧哈哈)

package BridgeModel.Example.ReadFileRight;

/**
 * Created by lenovo on 2017/4/20.
 */
public class Test {
    public static void main(String[] args) throws Exception{
        AbstractStream in = new NativeStream("record.txt");
        TextRead textRead = new TextRead(in);
        AbstractThing thing = new NativeFile(textRead);
        String s = (String)thing.read();
        in.close();
        System.out.println(s);
    }
}

时间: 2024-08-05 03:02:21

Java设计模式:桥接模式的相关文章

java设计模式--桥接模式

桥接模式(bridge):适用于多层继承机构 ,最少两个维度. 比如 日志管理 1.按格式分类 操作日志,交易日志 2.按距离分类 本地记录日志,异地记录日志 普通的多层次继承 //普通多层次结构继承 public interface Computer { void sale(); } //台式机 class Desktop implements Computer{ public void sale() { System.out.println("台式机"); } } //笔记本 cl

Java 设计模式 -- 复合模式之二

接着上文的鸭鸣例子:Java 设计模式 -- 复合模式之一 上文中,我们的鸭鸣实现了 装饰者模式  适配器模式  工厂模式的结合 现在,又需要进行改动了,上文,是可以统计一群鸭子的叫声,现在需要能够观察个别鸭子的行为 引入观察者模式: 任何想被观察的Quackable都必须实现下面的接口 public interface QuackObservable { public void registerObserver(Observer observer); public void notifyobs

一起学java设计模式--代理模式(结构型模式)

代理模式 应用软件所提供的桌面快捷方式是快速启动应用程序的代理,桌面快捷方式一般使用一张小图片来表示(Picture),通过调用快捷方式的run()方法将调用应用软件(Application)的run()方法.使用代理模式模拟该过程,绘制类图并编程实现. package ProxyPattern; interface Software { void run(); } class Application implements Software { public void run() { Syste

Java设计模式-代理模式之动态代理(附源码分析)

Java设计模式-代理模式之动态代理(附源码分析) 动态代理概念及类图 上一篇中介绍了静态代理,动态代理跟静态代理一个最大的区别就是:动态代理是在运行时刻动态的创建出代理类及其对象.上篇中的静态代理是在编译的时候就确定了代理类具体类型,如果有多个类需要代理,那么就得创建多个.还有一点,如果Subject中新增了一个方法,那么对应的实现接口的类中也要相应的实习该方法,不符合设计模式原则. 动态代理的做法:在运行时刻,可以动态创建出一个实现了多个接口的代理类.每个代理类的对象都会关联一个表示内部处理

Java设计模式-代理模式之动态代理(附源代码分析)

Java设计模式-代理模式之动态代理(附源代码分析) 动态代理概念及类图 上一篇中介绍了静态代理,动态代理跟静态代理一个最大的差别就是:动态代理是在执行时刻动态的创建出代理类及其对象. 上篇中的静态代理是在编译的时候就确定了代理类详细类型.假设有多个类须要代理.那么就得创建多个. 另一点,假设Subject中新增了一个方法,那么相应的实现接口的类中也要相应的实现这些方法. 动态代理的做法:在执行时刻.能够动态创建出一个实现了多个接口的代理类.每一个代理类的对象都会关联一个表示内部处理逻辑的Inv

Java设计模式-代理模式之静态代理

Java设计模式-代理模式之静态代理 概念 为另一个对象提供一个替身或占位符以提供对这个对象的访问,使用代理模式创建代表对象,让代表对象控制某对象的访问,被代理对象可以是远程的对象.创建开销大的对象或需要安全控制的对象 远程代理控制访问远程对象 虚拟代理控制访问创建开销大的资源 保护代理基于权限控制对资源的访问 看如下的类图: 仔细看上面的类图,首先是Subject它为RealSubject和Proxy提供了接口,通过实现同一个接口,Proxy在RealSubject出现的地方取代它,这点和适配

js设计模式——桥接模式

定义:将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化. 常用场景:在js中,桥接模式常用于事件监听器和ajax请求的解耦,以便于进行单元测试. 举个栗子 普通方法. var btn=$('#btn'); btn.on('click',function () { $.ajax({ url:'test.html', data:{ id:this.id }, dataType:'html', success:function(data){ con

5分钟读书笔记之 - 设计模式 - 桥接模式

补充一点知识: 私有变量 在对象内部使用'var'关键字来声明,而且它只能被私有函数和特权方法访问.私有函数 在对象的构造函数里声明(或者是通过var functionName=function(){...}来定义),它能被特权函数调用(包括对象的构造函数)和私有函数调用.特权方法 通过this.methodName=function(){...}来声明而且可能被对象外部的代码调用.可以使用:this.特权函数() 方式来调用特权函数,使用 :私有函数()方式来调用私有函数.公共属性 通过thi

Java设计模式--生成器模式

将一个复杂对象的构建与它的表示分离,使同样的构建过程可以创建不同的表示. Builder Pattern Separate the construction of a complex object from its representation so that the same construction process can create different representations. 类图 模式的结构与使用 生成器模式的结构中包括四个角色. 产品(Product):具体生成器要构造的复

java 之 桥接模式(大话设计模式)

桥接模式定义为:将抽象部分与它的实现部分分离,使它们都可以独立的变化. 第一次看设计模式的时候,不是很清楚这句话的意思,随着笔者的不断开发,发现有一种场景, 继承关系多了,不易于维护父类,而笔者认为桥接模式的出现很好的解决了这个问题.用聚合解决继承的父类难维护性.先看下桥接模式设计图. 大话设计模式-类图 以上类图很简单易懂,先看下笔者的demo /** * 操作接口 */ public interface IOperate { public void operate(); } /** * 品牌