结构类设计模式

创建类模式将对象的创建和使用分离开来,结构类设计模式主要是为了解决类或对象之间的组合关系的,没有创建类设计模式那么简单,有的还比较复杂,尽量简洁的方式介绍,仍然是以前的方式,不重要的一提而过。

结构类设计模式包含的设计模式如下图:


结构类设计模式

简单说明:

  1. 代理模式,代理让我想到的像写专利代理,也许我们对自己写的专利内容比较熟悉,但是,我们对专利文档的格式要求,规范要求不熟悉,如果我们自己来申请专利就很麻烦,专利代理替我们修改相关的专利文档,提交给专利局审批,方便了我们,将专利申请前后的建档等工作隐藏在我们专利文档的前后,让我们可以专注于文档编写。

还比如我们开发中如果要记录前后的调用日志,进行访问鉴权都可以用代理模式。

  1. 桥接模式,桥接模式有两个方式:1)将抽象和它实现的实例分离开来,可以灵活替换不同的实现实例,经典的例子就是JDBC驱动,我们把JDBC的驱动常用的操作抽象,比如连接获取,语句准备,语句执行,结果集的获取的抽象的一系列操作,和它们涉及到的具体实践MySqlDriver,OracleDriver等实现分离开来,采用配置的方式灵活替换;2)一个类存在了二个或二个以上独立变化的维度,如果采用一个类增加了耦合度,我们把这两个变化的维度分离开来,让他们可以独立变化。

3.装饰器模式,正如名字一样,装饰嘛,从字面理解是为现有的东西增加点点缀,更好看些,装饰器模式也类似,就是为了现有的功能增加些其他功能,比如Java的IO中的BufferedInputStream 即为读取流增加带缓存的功能。

  1. 组合模式,如果数据可以表示成树状结构,比如部门和员工,那么我们可以采用让它们继承相同基类的办法,用统一的遍历不区分的处理组合和个体结构。

  2. 享元模式,内存中有大量相同的各类对象,各类对象都是不变的,我们可以将同一类的对象只保存一份,达到节约内存的目标,比如如果做个网络象棋游戏,我们可以把棋子的基本信息只保留一份,被各个棋局所共享。

  3. 适配器模式,适配适配故名思意,是为了改变现有的接口,让原来的接口适应新的接口形式,生活中我们常用的转接头就是个例子,适配的原因在于我们无法改变老的接口,也无法改变新的接口,只能建个中间层做转换。

7.门面模式,我们在开发接口的时候,为了更好的复用,将接口的粒度设置的比较小,但是比较小的话,外部使用起来不方面,所以为了让外部调用起来更方面,我们把几个小粒度的接口,再包装下成大的方便使用的接口,这个就是门面模式。

一 代理模式

正如我们上面所说的,代理模式为现有的代码引入附加功能,比如对每个函数调用都打印耗时啊,都要做接口性能统计啊,比如要统一做接口鉴权了,我们都可以采用这种模式。

示意图如下:


代理

对于我们可以掌控的,我们可以让代理类和被代理类继承相同的接口,代理类依赖被代理类,通过外部注入方式引入,调用的时候,再被代理执行的方法前后添加需要增加的功能。

如果被代理类无法掌控,比如是依赖第三方类,我们可以新定义代理类继承被代理类,如上图。
不过如果我们需要代理的类很多,比如controller中我们很多请求都要控制权限,都要进行性能统计,每个都继承或实现相同接口的方法去写的话很麻烦,怎么办,其实也有好的办法,那就是采用动态代理的方式。

动态代理如果没接触过理解起来还是有些难度。首先要理解的是java的对象生成.

A a = new A();

上述是简单的创建java对象的语句,执行过程如下:

  1. java通过ClassLoader加载java的字节码生成Class对象(会执行静态方法),放在方法区。
  2. 在JVM堆上开辟一个空间用来存放新对象。
  3. 调用构造函数,来初始化这个空间为对象。
  4. 将堆栈上引用a指向堆上这个对象。

所以java的对象创建不一定要写个class类,我们如果可以直接得到Class对象,利用Class对象来创建对象。Class对象里面就是一些方法,构造函数和属性,我们又知道被代理类,那么是否可以通过被代理类来动态获得一个Class对象,这个我们通过这个Class对象来生成实际的代理对象。

举个例子:

package com.abc.mdpt.test;

import java.lang.reflect.*;


interface IOperator {
    int getSum(int a, int b);

    int getSub(int a, int b);
}


class Operator implements IOperator {

    @Override
    public int getSum(int a, int b) {
        return a+b;
    }

    @Override
    public int getSub(int a, int b) {
        return a-b;
    }
}


public class TestReflect {

    public static void main(String[] args) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException {
        /* @param   loader the class loader to define the proxy class
         * @param   interfaces the list of interfaces for the proxy class to implement*/
        Class sumCal = Proxy.getProxyClass(Operator.class.getClassLoader(), Operator.class.getInterfaces());
        Constructor[] cs = sumCal.getDeclaredConstructors();
        for (Constructor ca : cs) {
            System.out.println(ca);
        }
        Constructor constructor =  sumCal.getDeclaredConstructor(InvocationHandler.class);
        IOperator iOperator = (IOperator) constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("代理之前记录日志");
                long start  = System.currentTimeMillis();
                Operator operator = new Operator();
                Integer re = (Integer) method.invoke(operator, args);
                long end = System.currentTimeMillis();
                System.out.println("代理之后记录:cost :"+ String.valueOf(end-start));
                return re;
            }
        });

        System.out.println("sum:"+ iOperator.getSum(111, 333));
        System.out.println("sub:" + iOperator.getSub(11, 11333));
    }
}

上面是个简单的动态代理,在操作前后打印日志信息,计算函数调用耗时,打印结果如下:

public com.abc.mdpt.test.$Proxy0(java.lang.reflect.InvocationHandler)
代理之前记录日志
代理之后记录:cost :0
sum:444
代理之前记录日志
代理之后记录:cost :1
sub:-11322

我们利用JDK内部Proxy类来生成一个动态代理的Class对象,然后通过newInstance来创建一个动态代理的对象,参数是个InvocationHandler的对象,对象实现了invoke方法,在内部我们定义了被代理的对象,然后在执行些代理的方法。


示意图

上述代码写的不够灵活,改下:


    private static Object getProxy(final Object target) throws Exception {
        //参数1:随便找个类加载器给它, 参数2:目标对象实现的接口,让代理对象实现相同接口
        Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
        Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
        Object proxy = constructor.newInstance(new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("代理之前记录日志");
                long start  = System.currentTimeMillis();
                Object result = method.invoke(target, args);
                long end = System.currentTimeMillis();
                System.out.println("代理之后记录:cost :"+ String.valueOf(end-start));
                return result;
            }
        });
        return proxy;
    }

    public static void main(String[] args) throws Exception {
        IOperator iOperator  = (IOperator) getProxy(new Operator());
        System.out.println("sum:"+ iOperator.getSum(111, 333));
        System.out.println("sub:" + iOperator.getSub(11, 11333));
    }

二 桥接模式

桥接模式按照GoF书的含义:将抽象和实现分离,使其可以独立变化; 还有一个解释就是类有两个或多个独立变化的维度,我们可以通过组合方式,让多个维度可以独立变化。

JDBC就是好的例子,JDBC是抽象,Driver就是实现,JDBC将数据库的无关的,被抽象出来的类库。

//加载及注册JDBC驱动程序
Class.forName("com.mysql.jdbc.Driver");

加载的时候会执行静态初始化,将Mysql的数据库的Driver注册到DriverManager类中,然后:

String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
Connection con = DriverManager.getConnection(url);

后面就执行sql查询等:

Statement stmt = con.createStatement();
String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
  rs.getString(1); 
 rs.getInt(2);
}

第二种解释,我觉得更实用,通过将多个维度分离开来独立变化。举个例子:


public interface MsgSender {
  void send(String message);
}

public class TelephoneMsgSender implements MsgSender {
  private List telephones;

  public TelephoneMsgSender(List telephones) {
    this.telephones = telephones;
  }

  @Override
  public void send(String message) {
    //...
  }

}

public class EmailMsgSender implements MsgSender {
  // 与TelephoneMsgSender代码结构类似,所以省略...
}

public class WechatMsgSender implements MsgSender {
  // 与TelephoneMsgSender代码结构类似,所以省略...
}

public abstract class Notification {
  protected MsgSender msgSender;

  public Notification(MsgSender msgSender) {
    this.msgSender = msgSender;
  }

  public abstract void notify(String message);
}

public class SevereNotification extends Notification {
  public SevereNotification(MsgSender msgSender) {
    super(msgSender);
  }

  @Override
  public void notify(String message) {
    msgSender.send(message);
  }
}

public class UrgencyNotification extends Notification {
  // 与SevereNotification代码结构类似,所以省略...
}
public class NormalNotification extends Notification {
  // 与SevereNotification代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {
  // 与SevereNotification代码结构类似,所以省略...
}

如上代码,告警有不同的级别,告警的通知方式有电话通知,微信通知,邮件通知等,在使用的时候不同的告警级别采用不同的告警方式,而且要灵活配置,采用告警方式,如果不采用桥接模式,两种变化原因夹杂在一起,代码很乱,难以维护。采用桥接模式后,对于不同的告警模式,可以采用不同的告警方式,很灵活,将原来的N*M的类的组合变成了N+M的组合。

桥接模式

三 装饰器模式

装饰器故名思意是对一个类的包装,使其的功能更强,比如我们将普通的文件读取加上带缓存的文件读取,这个带缓存的读取流功能就是一种装饰,如下:


InputStream in = new FileInputStream("test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
  //...
}

装饰器从表面看来,挺麻烦,直接定义个带缓存的读文件的流不香嘛,如果从单个类的角度来看确实这样,但是如果了解java的流读取,知道除了文件读取流,还有数组流读取,还有Data的数据流读取,每个读取都耦合缓存读取功能,功能定义重复,万一读取缓存的功能我们写的代码有问题,那很多类都需要修改,我们通过装饰器模式,将缓存的功能和读取的类型分离开来,可以灵活组合;比如我们可以灵活的给数据读取加缓存,给管道读取加缓存功能。

在我看来,装饰器模式用在相似的一组类中,我们把这些类的功能进行分层,每层实现特定的功能,组合起来使用,达到复用的目的。

用继承模式来实现的话,如果需要多个功能,比如读取数据和读取缓存,那么继承关系就比较复杂。

// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
  void f();
}
public class A implements IA {
  public void f() { //... }
}
public class ADecorator implements IA {
  private IA a;
  public ADecorator(IA a) {
    this.a = a;
  }
  
  public void f() {
    // 功能增强代码
    a.f();
    // 功能增强代码
  }
}

继承相同的基类或实现相同的接口,就可以这样用:

// 包装下
new ADecorator (new A());

四 组合模式

组合模式挺简单的,将组合和个体都继承相同的类或实现相同的接口,然后再操作的时候就可以统一操作,比如计算文件夹或文件大小。


public abstract class FileSystemNode {
  protected String path;

  public FileSystemNode(String path) {
    this.path = path;
  }

  public abstract int countNumOfFiles();
  public abstract long countSizeOfFiles();

  public String getPath() {
    return path;
  }
}

public class File extends FileSystemNode {
  public File(String path) {
    super(path);
  }

  @Override
  public int countNumOfFiles() {
    return 1;
  }

  @Override
  public long countSizeOfFiles() {
    java.io.File file = new java.io.File(path);
    if (!file.exists()) return 0;
    return file.length();
  }
}

public class Directory extends FileSystemNode {
  private List subNodes = new ArrayList<>();

  public Directory(String path) {
    super(path);
  }

  @Override
  public int countNumOfFiles() {
    int numOfFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      numOfFiles += fileOrDir.countNumOfFiles();
    }
    return numOfFiles;
  }

  @Override
  public long countSizeOfFiles() {
    long sizeofFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      sizeofFiles += fileOrDir.countSizeOfFiles();
    }
    return sizeofFiles;
  }

  public void addSubNode(FileSystemNode fileOrDir) {
    subNodes.add(fileOrDir);
  }

  public void removeSubNode(FileSystemNode fileOrDir) {
    int size = subNodes.size();
    int i = 0;
    for (; i < size; ++i) {
      if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
        break;
      }
    }
    if (i < size) {
      subNodes.remove(i);
    }
  }
}

五 享元模式

享元模式,享可以理解为共享,目的是为了减少内存使用,共享对象,元即可以是对象也可以是对象一部分,这部分当然我们也抽象出来成对象,以达到共享的目的。

比如我们常见的word编辑器,每个字符都可以设置不同字体颜色和大小,如果我们每个字符都定义一个style对象,对象包含字体,颜色和大小,那么会占用大量的内存,我们可以将style对象共享了下,我们在一篇文章中涉及到的样式不会太多,这样会减少内存的使用,如下:


public class CharacterStyle {
  private Font font;
  private int size;
  private int colorRGB;

  public CharacterStyle(Font font, int size, int colorRGB) {
    this.font = font;
    this.size = size;
    this.colorRGB = colorRGB;
  }

  @Override
  public boolean equals(Object o) {
    CharacterStyle otherStyle = (CharacterStyle) o;
    return font.equals(otherStyle.font)
            && size == otherStyle.size
            && colorRGB == otherStyle.colorRGB;
  }
}

public class CharacterStyleFactory {
  private static final HashMap styles = new LinkedHashMap<>();

  public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
    String key = font.toString()+String.valueOf(size)+String.valueOf(colorRGB);
   if (styles.contains(key)) {
       return styles.get(key);
   }else {
     CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
     styles.put(key,newStyle);
     return newStyle;
  }
  }
}

public class Character {
  private char c;
  private CharacterStyle style;

  public Character(char c, CharacterStyle style) {
    this.c = c;
    this.style = style;
  }
}

public class Editor {
  private List chars = new ArrayList<>();

  public void appendCharacter(char c, Font font, int size, int colorRGB) {
    Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB));
    chars.add(character);
  }
}

上述只是示例代码,像Font都没定义,所以编译时候会报错的。

六 适配器模式

适配器模式故名思意,是将不同的东西进行适配,类似我们的转接头。使用的场景有:

  1. 为了统一接口,将使用不同类库实现同一个功能的接口进行统一,比如我们需要过滤敏感词,使用了不同的敏感词库,每种库的使用方法不同,我们将接口进行统一适配。
  2. 接口设计缺陷的弥补。
  3. 替换外部依赖,比如我们开始时候依赖外部库,现在要替换成我们自己开发的库,这个库的接口那又搞的和依赖的外部库不一样,那就用适配器来适配下吧。

比如java 开发中常用SL4j 来进行各种日志适配的打印。

适配器模式即可以采用继承的方式,用新的接口包装下老的接口调用;又可以采用注入的方式,将新接口调用,转成对注入的老对象的调用。


// 类适配器: 基于继承
public interface ITarget {
  void f1();
  void f2();
  void fc();
}

public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}

public class Adaptor extends Adaptee implements ITarget {
  public void f1() {
    super.fa();
  }
  
  public void f2() {
    //...重新实现f2()...
  }
  
  // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}

// 对象适配器:基于组合
public interface ITarget {
  void f1();
  void f2();
  void fc();
}

public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}

public class Adaptor implements ITarget {
  private Adaptee adaptee;
  
  public Adaptor(Adaptee adaptee) {
    this.adaptee = adaptee;
  }
  
  public void f1() {
    adaptee.fa(); //委托给Adaptee
  }
  
  public void f2() {
    //...重新实现f2()...
  }
  
  public void fc() {
    adaptee.fc();
  }
}

七 门面模式

在我们开发中,为了复用,将方法的粒度设置的很小,但是这个类使用起来就不太舒服,需要调用多个方法,才能实现需要的功能,门面模式就把这些小方法的调用组合起来形成大的方法,方便的调用。

这样就兼容了复用性和易用性。门面模式也可以利用在更大的方面,比如我们为了复用设计很多子系统,为了易用,我们会将多个子系统的功能结合起来。比如我们知道C语言可以直接调用系统函数也可以调用库函数,调用库函数,需要更少的参数,逻辑更简单,原因就是库函数封装了系统函数,简化了我们的操作,这可以看成一种门面模式的应用。

八 诗词欣赏

《桃花庵歌》
版本一:
桃花坞里桃花庵,桃花庵下桃花仙;
桃花仙人种桃树,又摘桃花换酒钱。
酒醒只在花前坐,酒醉还来花下眠;
半醒半醉日复日,花落花开年复年。
但愿老死花酒间,不愿鞠躬车马前;车
尘马足富者趣,酒盏花枝贫者缘。
若将富贵比贫者,一在平地一在天;
若将贫贱丨比车马,他得驱驰我得闲。
别人笑我太疯癫,我笑他人看不穿;
不见五陵豪杰墓,无花无酒锄作田.

你可能感兴趣的:(结构类设计模式)