23种模式 - 结构型

2022-04-18  本文已影响0人  Zeppelin421

结构型模式主要总结了一些类或对象组合在一起的经典结构。

代理模式

代理模式(Proxy Design Pattern)在不改变原始类(或被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

原理与实现

一般情况下,让代理类和原始类实现相同的接口,如果原始类并没有定义接口,并且原始类代码并不在维护范围内,这种情况下,可以通过让代理类继承原始类的方法来实现代理模式

动态代理

静态代理需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模板式的“重复”代码,增加了维护成本和开发成本。对于静态代码存在的问题,可以通过动态代理来解决。不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。

代理模式的应用场景

桥接模式

桥接模式(Bridge Design Pattern)有两种理解

// JDBC 驱动就是桥接模式的经典应用
Class.forName("com.mysql.jdbc.Driver");//加载及注册JDBC驱动程序
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement();
String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
  rs.getString(1);
  rs.getInt(2);
}

// 当执行 Class.forName("com.mysql.jdbc.Driver") 这条语句时,实际做了两件事
//  1. 要求 JVM 查找并加载指定的 Driver 类
//  2. 执行该类的静态代码,也就是将 MySQL Driver 注册到 DriverManager类中
package com.mysql.jdbc;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
  static {
    try {
      java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
      throw new RuntimeException("Can't register driver!");
    }
  }

  /**
   * Construct a new driver and register it with DriverManager
   * @throws SQLException if a database error occurs.
   */
  public Driver() throws SQLException {
    // Required for Class.forName().newInstance()
  }
}
public enum NotificationEmergencyLevel {
  SEVERE, URGENCY, NORMAL, TRIVIAL
}

public class Notification {
  private List<String> emailAddresses;
  private List<String> telephones;
  private List<String> wechatIds;

  public Notification() {}

  public void setEmailAddress(List<String> emailAddress) {
    this.emailAddresses = emailAddress;
  }

  public void setTelephones(List<String> telephones) {
    this.telephones = telephones;
  }

  public void setWechatIds(List<String> wechatIds) {
    this.wechatIds = wechatIds;
  }

  public void notify(NotificationEmergencyLevel level, String message) {
    if (level.equals(NotificationEmergencyLevel.SEVERE)) {
      //...自动语音电话
    } else if (level.equals(NotificationEmergencyLevel.URGENCY)) {
      //...发微信
    } else if (level.equals(NotificationEmergencyLevel.NORMAL)) {
      //...发邮件
    } else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {
      //...发邮件
    }
  }
}

//在API监控告警的例子中,我们如下方式来使用Notification类:
public class ErrorAlertHandler extends AlertHandler {
  public ErrorAlertHandler(AlertRule rule, Notification notification){
    super(rule, notification);
  }


  @Override
  public void check(ApiStatInfo apiStatInfo) {
    if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
      notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
  }
}

// 上面代码存在两个独立变化的维度:紧急程度 和 发送渠道
// 可以将不同渠道的发送逻辑剥离出来,形成独立的消息发送类。其中 Notification 类相当于抽象,MsgSender 类相当于实现,两者可以独立开发,通过组合关系任意组合在一起。
// 所谓任意组合的意思就是,不同紧急程度的消息和发送渠道之间的对应关系,不是在代码中固定写死的,可以动态地去指定(比如:通过读取配置来获取对应关系)
public interface MsgSender {
  void send(String message);
}

public class TelephoneMsgSender implements MsgSender {
  private List<String> telephones;

  public TelephoneMsgSender(List<String> 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代码结构类似,所以省略...
}

装饰器模式

装饰器模式(Decorator Design Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。

基于继承的设计方案

如果 InputStream 只有一个子类 FileInputStream 的话,那在 FileInputStream 基础上再设计一个子类 BufferedFileInputStream 也可以接受,毕竟继承结构还算简单。但实际上继承 InputStream 的子类很多,需要给每一个 InputStream 的子类再继续派生支持缓存读取的子类。除了支持缓存读取之外,如果还需要对功能进行其他方面的增强,比如支持按照基本数据类型读取等等,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。


基于继承设计方案

基于装饰器模式的设计方案

针对继承结构过于复杂的问题,可以通过将继承关系改为组合关系来解决。相对于简单的组合关系,装饰器有两个比较特殊的地方:

InputStream in = new FileInputStream("/user/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();

适配器模式

适配器模式(Adapter Design Pattern)用来做适配的,它将不兼容的接口转化为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

实现方式

// 类适配器: 基于继承
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();
  }
}

应用场景

代理、桥接、装饰器、适配器的区别

代理、桥接、装饰器、适配器这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似,笼统来说,都可以称之为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。

门面模式

门面模式(Facade Design Pattern)为子系统提供一组统一的接口,定义一组高层接口让子系统更易用

应用场景

类、模块、系统之间的“通信”,一般都是通过接口调用来完成的。接口设计的好坏直接影响到类、模块、系统是否好用。接口粒度设计得太大,太小都不好。太大会导致接口不可复用,太小会导致接口不易用。在实际开发中,接口的复用性和易用性需要“微妙”的权衡。通常处理原则是:尽量保持接口的可复用性,但针对特殊情况,允许提供冗余的门面接口,来提供更易用的接口。

组合模式

组合模式(Composite Design Pattern)将一组对象组织(Compose)成树形结构,以表示一种“部分-整体”的层次结构。组合让客户端(使用者)可以统一单个对象和组合对象的处理逻辑。

组合模式的设计思想,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成数这种数据结构,业务需求可以通过树上的递归算法来实现。

组合模式将一组对象组织成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且利用它树形结构的特点,递归地处理每个子树,依次简化代码实现。使用组合模式的前提在于,业务场景必须能够表示成树形结构。所以组合模式的应用场景比较局限。

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<FileSystemNode> 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);
    }
  }
}

享元模式

享元模式(Flyweight Design Pattern)顾名思义是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
一个系统中存在大量重复对象的时候,就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。实际上不仅仅相同对象可以设计成享元,对于相似对象,也可以将这些对象中相同的部分(字段),提取出来设计成享元,让这些大量相似对象引用这些享元。

实现

享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或 List 来缓存已经创建好的享元对象,以达到复用的目的。

public class Character {//文字
  private char c;

  private Font font;
  private int size;
  private int colorRGB;

  public Character(char c, Font font, int size, int colorRGB) {
    this.c = c;
    this.font = font;
    this.size = size;
    this.colorRGB = colorRGB;
  }
}

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

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

// 在文本编辑器中,每敲一个文字,都会创建一个新的 Character 对象保存到 chars 数组中。如果有成千上万文字,会很耗内存
// 实际上,在一个文本文件中,用到的字体格式不会太多,所以对于字体格式,可以将它设计成享元,让不同的文字共享使用
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 List<CharacterStyle> styles = new ArrayList<>();

  public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
    CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
    for (CharacterStyle style : styles) {
      if (style.equals(newStyle)) {
        return style;
      }
    }
    styles.add(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<Character> 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);
  }
}

享元模式 VS 单例、缓存、对象池

区别两种设计模式,不能光看代码实现,而是要看设计意图:享元模式是为了实现对象复用,节省内存;单例模式是为了保证对象全局唯一;缓存是为了提高访问效率;池化技术中的“复用”理解为“重复使用”,主要是为了节省时间

上一篇 下一篇

猜你喜欢

热点阅读