zoukankan      html  css  js  c++  java
  • 外观模式、组合模式、享元模式

    外观模式、组合模式、享元模式

    门面模式(外观模式)【不常用】

    1. 门面模式原理和实现都特别简单,应用场景也比较明确,主要在接口设计方面使用。
    2. 如果你平时的工作涉及接口开发,不知道你有没有遇到关于接口粒度的问题呢?
    3. 为了保证接口的可复用性(或者叫通用性),我们需要将接口尽量设计得细粒度一点,职责单一一点。但是,如果接口的粒度过小,在接口的使用者开发一个业务功能时,就会导致需要调用 n 多细粒度的接口才能完成。调用者肯定会抱怨接口不好用。
    4. 相反,如果接口粒度设计得太大,一个接口返回 n 多数据,要做 n 多事情,就会导致接口不够通用、可复用性不好。接口不可复用,那针对不同的调用者的业务需求,我们就需要开发不同的接口来满足,这就会导致系统的接口无限膨胀。
    5. 那如何来解决接口的可复用性(通用性)和易用性之间的矛盾呢?通过今天对于门面模式的学习,我想你心中会有答案。

    门面模式的原理与实现

    1. 门面模式,也叫外观模式,英文全称是 Facade Design Pattern。在 GoF 的《设计模式》一书中,门面模式是这样定义的:

    Provide a unified interface to a set of interfaces in a subsystem. Facade Pattern defines a higher-level interface that makes the subsystem easier to use.

    1. 翻译成中文就是:门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。这个定义很简洁,我再进一步解释一下。
    2. 假设有一个系统 A,提供了 a、b、c、d 四个接口。系统 B 完成某个业务功能,需要调用 A 系统的 a、b、d 接口。利用门面模式,我们提供一个包裹 a、b、d 接口调用的门面接口 x,给系统 B 直接使用。
    3. 不知道你会不会有这样的疑问,让系统 B 直接调用 a、b、d 感觉没有太大问题呀,为什么还要提供一个包裹 a、b、d 的接口 x 呢?关于这个问题,我通过一个具体的例子来解释一下。
    4. 假设我们刚刚提到的系统 A 是一个后端服务器,系统 B 是 App 客户端。App 客户端通过后端服务器提供的接口来获取数据。我们知道,App 和服务器之间是通过移动网络通信的,网络通信耗时比较多,为了提高 App 的响应速度,我们要尽量减少 App 与服务器之间的网络通信次数。
    5. 假设,完成某个业务功能(比如显示某个页面信息)需要“依次”调用 a、b、d 三个接口,因自身业务的特点,不支持并发调用这三个接口。
    6. 如果我们现在发现 App 客户端的响应速度比较慢,排查之后发现,是因为过多的接口调用过多的网络通信。针对这种情况,我们就可以利用门面模式,让后端服务器提供一个包裹 a、b、d 三个接口调用的接口 x。App 客户端调用一次接口 x,来获取到所有想要的数据,将网络通信的次数从 3 次减少到 1 次,也就提高了 App 的响应速度。
    7. 这里举的例子只是应用门面模式的其中一个意图,也就是解决性能问题。实际上,不同的应用场景下,使用门面模式的意图也不同。接下来,我们就来看一下门面模式的各种应用场景。

    Demo案例-影院管理

    • 组建一个家庭影院:
    • DVD 播放器、投影仪、自动屏幕、环绕立体声、爆米花机,要求完成使用家庭影院的功能,其过程为:
    • 直接用遥控器:统筹各设备开关
      • 开爆米花机
      • 放下屏幕
      • 开投影仪
      • 开音响
      • 开 DVD,选 dvd
      • 去拿爆米花
      • 调暗灯光
      • 播放
      • 观影结束后,关闭各种设备

    传统方案

    img

    1. 在 ClientTest 的 main 方法中,创建各个子系统的对象,并直接去调用子系统(对象)相关方法,会造成调用过程 混乱,没有清晰的过程 不利于在 ClientTest 中,去维护对子系统的操作
    2. 解决思路:定义一个高层接口,给子系统中的一组接口提供一个一致的界面(比如在高层接口提供四个方法 ready, play, pause, end ),用来访问子系统中的一群接口
    3. 也就是说 就是通过定义一个一致的接口(界面类),用以屏蔽内部子系统的细节,使得调用端只需跟这个接口发 生调用,而无需关心这个子系统的内部细节 => 外观模式
    4. 外观类(Facade): 为调用端提供统一的调用接口, 外观类知道哪些子系统负责处理请求,从而将调用端的请求代 理给适当子系统对象
    5. 调用者(Client): 外观接口的调用者
    6. 子系统的集合:指模块或者子系统,处理 Facade 对象指派的任务,他是功能的实际提供者

    门面模式代码

    TheaterLight

    public class TheaterLight {
    
      private static TheaterLight instance = new TheaterLight();
    
      public static TheaterLight getInstance() {
        return instance;
      }
    
      public void on() {
        System.out.println(" TheaterLight on ");
      }
    
      public void off() {
        System.out.println(" TheaterLight off ");
      }
    
      public void dim() {
        System.out.println(" TheaterLight dim.. ");
      }
    
      public void bright() {
        System.out.println(" TheaterLight bright.. ");
      }
    }
    

    Stereo

    public class Stereo {
    
       private static Stereo instance = new Stereo();
       
       public static Stereo getInstance() {
          return instance;
       }
       
       public void on() {
          System.out.println(" Stereo on ");
       }
       
       public void off() {
          System.out.println(" Screen off ");
       }
       
       public void up() {
          System.out.println(" Screen up.. ");
       }
       
       //...
    }
    

    Screen

    public class Screen {
    
       private static Screen instance = new Screen();
       
       public static Screen getInstance() {
          return instance;
       }
    
       public void up() {
          System.out.println(" Screen up ");
       }
       
       public void down() {
          System.out.println(" Screen down ");
       }
       
    }
    

    Projector

    public class Projector {
    
       private static Projector instance = new Projector();
       
       public static Projector getInstance() {
          return instance;
       }
       
       public void on() {
          System.out.println(" Projector on ");
       }
       
       public void off() {
          System.out.println(" Projector ff ");
       }
       
       public void focus() {
          System.out.println(" Projector is Projector  ");
       }
       
       //...
    }
    

    Popcorn

    public class Popcorn {
       
       private static Popcorn instance = new Popcorn();
       
       public static Popcorn getInstance() {
          return instance;
       }
       
       public void on() {
          System.out.println(" popcorn on ");
       }
       
       public void off() {
          System.out.println(" popcorn ff ");
       }
       
       public void pop() {
          System.out.println(" popcorn is poping  ");
       }
    }
    

    DVDPlayer

    public class DVDPlayer {
       
       //使用单例模式, 使用饿汉式
       private static DVDPlayer instance = new DVDPlayer();
       
       public static DVDPlayer getInstanc() {
          return instance;
       }
       
       public void on() {
          System.out.println(" dvd on ");
       }
       public void off() {
          System.out.println(" dvd off ");
       }
       
       public void play() {
          System.out.println(" dvd is playing ");
       }
       
       //....
       public void pause() {
          System.out.println(" dvd pause ..");
       }
    }
    

    HomeTheaterFacade

    public class HomeTheaterFacade {
    
      // 定义各个子系统对象
      private TheaterLight theaterLight;
      private Popcorn popcorn;
      private Stereo stereo;
      private Projector projector;
      private Screen screen;
      private DVDPlayer dVDPlayer;
    
      // 构造器
      public HomeTheaterFacade() {
        super();
        this.theaterLight = TheaterLight.getInstance();
        this.popcorn = Popcorn.getInstance();
        this.stereo = Stereo.getInstance();
        this.projector = Projector.getInstance();
        this.screen = Screen.getInstance();
        this.dVDPlayer = DVDPlayer.getInstanc();
      }
    
      // 操作分成 4 步
    
      public void ready() {
        popcorn.on();
        popcorn.pop();
        screen.down();
        projector.on();
        stereo.on();
        dVDPlayer.on();
        theaterLight.dim();
      }
    
      public void play() {
        dVDPlayer.play();
      }
    
      public void pause() {
        dVDPlayer.pause();
      }
    
      public void end() {
        popcorn.off();
        theaterLight.bright();
        screen.up();
        projector.off();
        stereo.off();
        dVDPlayer.off();
      }
    }
    
    public class Client {
    
      public static void main(String[] args) {
        // TODO Auto-generated method stub
        // 这里直接调用。。 很麻烦
        HomeTheaterFacade homeTheaterFacade = new HomeTheaterFacade();
        homeTheaterFacade.ready();
        homeTheaterFacade.play();
    
        homeTheaterFacade.end();
      }
    }
    

    门面模式的应用场景举例

    1. 在 GoF 给出的定义中提到,“门面模式让子系统更加易用”,实际上,它除了解决易用性问题之外,还能解决其他很多方面的问题。关于这一点,我总结罗列了 3 个常用的应用场景,你可以参考一下,举一反三地借鉴到自己的项目中。
    2. 除此之外,我还要强调一下,门面模式定义中的“子系统(subsystem)”也可以有多种理解方式。它既可以是一个完整的系统,也可以是更细粒度的类或者模块。关于这一点,在下面的讲解中也会有体现。

    解决易用性问题

    1. 门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。比如,Linux 系统调用函数就可以看作一种“门面”。它是 Linux 操作系统暴露给开发者的一组“特殊”的编程接口,它封装了底层更基础的 Linux 内核调用。再比如,Linux 的 Shell 命令,实际上也可以看作一种门面模式的应用。它继续封装系统调用,提供更加友好、简单的命令,让我们可以直接通过执行命令来跟操作系统交互。
    2. 我们前面也多次讲过,设计原则、思想、模式很多都是相通的,是同一个道理不同角度的表述。实际上,从隐藏实现复杂性,提供更易用接口这个意图来看,门面模式有点类似之前讲到的迪米特法则(最少知识原则)和接口隔离原则:两个有交互的系统,只暴露有限的必要的接口。除此之外,门面模式还有点类似之前提到封装、抽象的设计思想,提供更抽象的接口,封装底层实现细节。

    解决性能问题

    1. 关于利用门面模式解决性能问题这一点,刚刚我们已经讲过了。我们通过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高 App 客户端的响应速度。所以,关于这点,我就不再举例说明了。我们来讨论一下这样一个问题:从代码实现的角度来看,该如何组织门面接口和非门面接口?
    2. 如果门面接口不多,我们完全可以将它跟非门面接口放到一块,也不需要特殊标记,当作普通接口来用即可。如果门面接口很多,我们可以在已有的接口之上,再重新抽象出一层,专门放置门面接口,从类、包的命名上跟原来的接口层做区分。如果门面接口特别多,并且很多都是跨多个子系统的,我们可以将门面接口放到一个新的子系统中。

    解决分布式事务问题

    1. 关于利用门面模式来解决分布式事务问题,我们通过一个例子来解释一下。
    2. 在一个金融系统中,有两个业务领域模型,用户和钱包。这两个业务领域模型都对外暴露了一系列接口,比如用户的增删改查接口、钱包的增删改查接口。假设有这样一个业务场景:在用户注册的时候,我们不仅会创建用户(在数据库 User 表中),还会给用户创建一个钱包(在数据库的 Wallet 表中)。
    3. 对于这样一个简单的业务需求,我们可以通过依次调用用户的创建接口和钱包的创建接口来完成。但是,用户注册需要支持事务,也就是说,创建用户和钱包的两个操作,要么都成功,要么都失败,不能一个成功、一个失败。
    4. 要支持两个接口调用在一个事务中执行,是比较难实现的,这涉及分布式事务问题。虽然我们可以通过引入分布式事务框架或者事后补偿的机制来解决,但代码实现都比较复杂。而最简单的解决方案是,利用数据库事务或者 Spring 框架提供的事务(如果是 Java 语言的话),在一个事务中,执行创建用户和创建钱包这两个 SQL 操作。这就要求两个 SQL 操作要在一个接口中完成,所以,我们可以借鉴门面模式的思想,再设计一个包裹这两个操作的新接口,让新接口在一个事务中执行两个 SQL 操作。

    组合模式【不常用】

    1. 组合模式跟我们之前讲的面向对象设计中的“组合关系(通过组合来组装两个类)”,完全是两码事。这里讲的“组合模式”,主要是用来处理树形结构数据。这里的“数据”,你可以简单理解为一组对象集合,待会我们会详细讲解。
    2. 正因为其应用场景的特殊性,数据必须能表示成树形结构,这也导致了这种模式在实际的项目开发中并不那么常用。但是,一旦数据满足树形结构,应用这种模式就能发挥很大的作用,能让代码变得非常简洁。

    组合模式的原理与实现

    1. 在 GoF 的《设计模式》一书中,组合模式是这样定义的:

    Compose objects into tree structure to represent part-whole hierarchies.Composite lets client treat individual objects and compositions of objects uniformly.

    1. 翻译成中文就是:将一组对象组织(Compose)成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者。)可以统一单个对象和组合对象的处理逻辑。接下来,对于组合模式,我举个例子来给你解释一下。
    2. 假设我们有这样一个需求:设计一个类来表示文件系统中的目录,能方便地实现下面这些功能:
    • 动态地添加、删除某个目录下的子目录或文件;
    • 统计指定目录下的文件个数;
    • 统计指定目录下的文件总大小。
    1. 我这里给出了这个类的骨架代码,如下所示。其中的核心逻辑并未实现,你可以试着自己去补充完整,再来看我的讲解。在下面的代码实现中,我们把文件和目录统一用 FileSystemNode 类来表示,并且通过 isFile 属性来区分。
    public class FileSystemNode {
      private String path;
      private boolean isFile;
      private List<FileSystemNode> subNodes = new ArrayList<>();
    
      public FileSystemNode(String path, boolean isFile) {
        this.path = path;
        this.isFile = isFile;
      }
    
      public int countNumOfFiles() {
        // TODO:...
      }
    
      public long countSizeOfFiles() {
        // TODO:...
      }
    
      public String getPath() {
        return path;
      }
    
      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);
        }
      }
    }
    

    想要补全其中的 countNumOfFiles() 和 countSizeOfFiles() 这两个函数,并不是件难事,实际上这就是树上的递归遍历算法。对于文件,我们直接返回文件的个数(返回 1)或大小。对于目录,我们遍历目录中每个子目录或者文件,递归计算它们的个数或大小,然后求和,就是这个目录下的文件个数和文件大小。

    public int countNumOfFiles() {    if (isFile) {        return 1;    }    int numOfFiles = 0;    for (FileSystemNode fileOrDir : subNodes) {        numOfFiles += fileOrDir.countNumOfFiles();    }    return numOfFiles;}public long countSizeOfFiles() {    if (isFile) {        File file = new File(path);        if (!file.exists()) return 0;        return file.length();    }    long sizeofFiles = 0;    for (FileSystemNode fileOrDir : subNodes) {         sizeofFiles += fileOrDir.countSizeOfFiles();    }    return sizeofFiles;}
    

    单纯从功能实现角度来说,上面的代码没有问题,已经实现了我们想要的功能。但是,如果我们开发的是一个大型系统,从扩展性(文件或目录可能会对应不同的操作)、业务建模(文件和目录从业务上是两个概念)、代码的可读性(文件和目录区分对待更加符合人们对业务的认知)的角度来说,我们最好对文件和目录进行区分设计,定义为 File 和 Directory 两个类。

    按照这个设计思路,我们对代码进行重构。重构之后的代码如下所示:

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

    文件和目录类都设计好了,我们来看,如何用它们来表示一个文件系统中的目录树结构。具体的代码示例如下所示:

    public class Demo {  public static void main(String[] args) {    /**     * /     *     * <p>/wz/     *     * <p>/wz/a.txt     *     * <p>/wz/b.txt     *     * <p>/wz/movies/     *     * <p>/wz/movies/c.avi     *     * <p>/xzg/     *     * <p>/xzg/docs/     *     * <p>/xzg/docs/d.txt     */    Directory fileSystemTree = new Directory("/");    Directory node_wz = new Directory("/wz/");    Directory node_xzg = new Directory("/xzg/");    fileSystemTree.addSubNode(node_wz);    fileSystemTree.addSubNode(node_xzg);        File node_wz_a = new File("/wz/a.txt");    File node_wz_b = new File("/wz/b.txt");    Directory node_wz_movies = new Directory("/wz/movies/");    node_wz.addSubNode(node_wz_a);    node_wz.addSubNode(node_wz_b);    node_wz.addSubNode(node_wz_movies);        File node_wz_movies_c = new File("/wz/movies/c.avi");    node_wz_movies.addSubNode(node_wz_movies_c);        Directory node_xzg_docs = new Directory("/xzg/docs/");    node_xzg.addSubNode(node_xzg_docs);        File node_xzg_docs_d = new File("/xzg/docs/d.txt");    node_xzg_docs.addSubNode(node_xzg_docs_d);        System.out.println("/ files num:" + fileSystemTree.countNumOfFiles());    System.out.println("/wz/ files num:" + node_wz.countNumOfFiles());  }}
    
    1. 我们对照着这个例子,再重新看一下组合模式的定义:“将一组对象(文件和目录)组织成树形结构,以表示一种‘部分 - 整体’的层次结构(目录与子目录的嵌套结构)。组合模式让客户端可以统一单个对象(文件)和组合对象(目录)的处理逻辑(递归遍历)。”
    2. 实际上,刚才讲的这种组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。

    组合模式的应用场景举例

    1. 刚刚我们讲了文件系统的例子,对于组合模式,我这里再举一个例子。搞懂了这两个例子,你基本上就算掌握了组合模式。在实际的项目中,遇到类似的可以表示成树形结构的业务场景,你只要“照葫芦画瓢”去设计就可以了。
    2. 假设我们在开发一个 OA 系统(办公自动化系统)。公司的组织结构包含部门和员工两种数据类型。其中,部门又可以包含子部门和员工。在数据库中的表结构如下所示:

    img

    1. 我们希望在内存中构建整个公司的人员架构图(部门、子部门、员工的隶属关系),并且提供接口计算出部门的薪资成本(隶属于这个部门的所有员工的薪资和)。
    2. 部门包含子部门和员工,这是一种嵌套结构,可以表示成树这种数据结构。计算每个部门的薪资开支这样一个需求,也可以通过在树上的遍历算法来实现。所以,从这个角度来看,这个应用场景可以使用组合模式来设计和实现。
    3. 这个例子的代码结构跟上一个例子的很相似,代码实现我直接贴在了下面,你可以对比着看一下。其中,HumanResource 是部门类(Department)和员工类(Employee)抽象出来的父类,为的是能统一薪资的处理逻辑。Demo 中的代码负责从数据库中读取数据并在内存中构建组织架构图。
    public  abstract  class  HumanResource  {  protected  long id;  protected  double salary;  public  HumanResource(long id)  {    this.id = id;  }  public  long  getId()  {    return id;  }  public  abstract  double  calculateSalary();}public  class  Employee  extends  HumanResource  {  public  Employee(long id, double salary)  {    super(id);    this.salary = salary;  }  @Override  public  double  calculateSalary()  {    return salary;  }}public  class  Department  extends  HumanResource  {  private List<HumanResource> subNodes = new ArrayList<>();  public  Department(long id)  {    super(id);  }  @Override  public  double  calculateSalary()  {    double totalSalary = 0;    for (HumanResource hr : subNodes) {      totalSalary += hr.calculateSalary();    }    this.salary = totalSalary;    return totalSalary;  }  public  void  addSubNode(HumanResource hr)  {    subNodes.add(hr);  }}// 构建组织架构的代码public  class  Demo  {  private  static  final  long ORGANIZATION\_ROOT\_ID = 1001;  private DepartmentRepo departmentRepo; // 依赖注入  private EmployeeRepo employeeRepo; // 依赖注入  public  void  buildOrganization()  {    Department rootDepartment = new Department(ORGANIZATION\_ROOT\_ID);    buildOrganization(rootDepartment);  }  private  void  buildOrganization(Department department)  {    List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department.getId());    for (Long subDepartmentId : subDepartmentIds) {      Department subDepartment = new Department(subDepartmentId);      department.addSubNode(subDepartment);      buildOrganization(subDepartment);    }    List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId());    for (Long employeeId : employeeIds) {      double salary = employeeRepo.getEmployeeSalary(employeeId);      department.addSubNode(new Employee(employeeId, salary));    }  }}
    

    我们再拿组合模式的定义跟这个例子对照一下:“将一组对象(员工和部门)组织成树形结构,以表示一种‘部分 - 整体’的层次结构(部门与子部门的嵌套结构)。组合模式让客户端可以统一单个对象(员工)和组合对象(部门)的处理逻辑(递归遍历)。”

    享元模式【不常用】

    跟其他所有的设计模式类似,享元模式的原理和实现也非常简单。今天,我会通过棋牌游戏和文本编辑器两个实际的例子来讲解。除此之外,我还会讲到它跟单例、缓存、对象池的区别和联系。在后面一下享元模式在 Java Integer、String 中的应用。

    享元模式原理与实现

    1. 所谓“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
    2. 具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。
    3. 这里我稍微解释一下,定义中的“不可变对象”指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。
    4. 接下来,我们通过一个简单的例子解释一下享元模式。
    5. 假设我们在开发一个棋牌游戏(比如象棋)。一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。具体的代码如下所示。其中,ChessPiece 类表示棋子,ChessBoard 类表示一个棋局,里面保存了象棋中 30 个棋子的信息。
    public class ChessPiece { // 棋子  private int id;  private String text;  private Color color;  private int positionX;  private int positionY;  public ChessPiece(int id, String text, Color color, int positionX, int positionY) {    this.id = id;    this.text = text;    this.color = color;    this.positionX = positionX;    this.positionY = positionX;  }  public static enum Color {    RED,    BLACK  }  // ...省略其他属性和getter/setter方法...}public class ChessBoard { // 棋局  private Map<Integer, ChessPiece> chessPieces = new HashMap<>();  public ChessBoard() {    init();  }  private void init() {    chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 0));    chessPieces.put(2, new ChessPiece(2, "馬", ChessPiece.Color.BLACK, 0, 1));    // ...省略摆放其他棋子的代码...  }  public void move(int chessPieceId, int toPositionX, int toPositionY) {    // ...省略...  }}
    

    为了记录每个房间当前的棋局情况,我们需要给每个房间都创建一个 ChessBoard 棋局对象。因为游戏大厅中有成千上万的房间(实际上,百万人同时在线的游戏大厅也有很多),那保存这么多棋局对象就会消耗大量的内存。有没有什么办法来节省内存呢?

    这个时候,享元模式就可以派上用场了。像刚刚的实现方式,在内存中会有大量的相似对象。这些相似对象的 id、text、color 都是相同的,唯独 positionX、positionY 不同。实际上,我们可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。这样,棋盘只需要记录每个棋子的位置信息就可以了。具体的代码实现如下所示:

    // 享元类public class ChessPieceUnit {  private int id;  private String text;  private Color color;  public ChessPieceUnit(int id, String text, Color color) {    this.id = id;    this.text = text;    this.color = color;  }  public static enum Color {    RED,    BLACK  }  // ...省略其他属性和getter方法...}public class ChessPieceUnitFactory {  private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();  static {    pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));    pieces.put(2, new ChessPieceUnit(2, "馬", ChessPieceUnit.Color.BLACK));    // ...省略摆放其他棋子的代码...  }  public static ChessPieceUnit getChessPiece(int chessPieceId) {    return pieces.get(chessPieceId);  }}public class ChessPiece {  private ChessPieceUnit chessPieceUnit;  private int positionX;  private int positionY;  public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {    this.chessPieceUnit = unit;    this.positionX = positionX;    this.positionY = positionY;  }  // 省略getter、setter方法}public class ChessBoard {  private Map<Integer, ChessPiece> chessPieces = new HashMap<>();    public ChessBoard() {    init();  }  private void init() {    chessPieces.put(1, new ChessPiece(ChessPieceUnitFactory.getChessPiece(1), 0, 0));    chessPieces.put(1, new ChessPiece(ChessPieceUnitFactory.getChessPiece(2), 1, 0));    // ...省略摆放其他棋子的代码...  }  public void move(int chessPieceId, int toPositionX, int toPositionY) {    // ...省略...  }}
    
    1. 在上面的代码实现中,我们利用工厂类来缓存 ChessPieceUnit 信息(也就是 id、text、color)。通过工厂类获取到的 ChessPieceUnit 就是享元。所有的 ChessBoard 对象共享这 30 个 ChessPieceUnit 对象(因为象棋中只有 30 个棋子)。在使用享元模式之前,记录 1 万个棋局,我们要创建 30 万(30*1 万)个棋子的 ChessPieceUnit 对象。利用享元模式,我们只需要创建 30 个享元对象供所有棋局共享使用即可,大大节省了内存。
    2. 那享元模式的原理讲完了,我们来总结一下它的代码结构。实际上,它的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的。

    享元模式在文本编辑器中的应用

    1. 弄懂了享元模式的原理和实现之后,我们再来看另外一个例子,也就是文章标题中给出的:如何利用享元模式来优化文本编辑器的内存占用?
    2. 你可以把这里提到的文本编辑器想象成 Office 的 Word。不过,为了简化需求背景,我们假设这个文本编辑器只实现了文字编辑功能,不包含图片、表格等复杂的编辑功能。对于简化之后的文本编辑器,我们要在内存中表示一个文本文件,只需要记录文字和格式两部分信息就可以了,其中,格式又包括文字的字体、大小、颜色等信息。
    3. 尽管在实际的文档编写中,我们一般都是按照文本类型(标题、正文……)来设置文字的格式,标题是一种格式,正文是另一种格式等等。但是,从理论上讲,我们可以给文本文件中的每个文字都设置不同的格式。为了实现如此灵活的格式设置,并且代码实现又不过于太复杂,我们把每个文字都当作一个独立的对象来看待,并且在其中包含它的格式信息。具体的代码示例如下所示:
    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);  }}
    
    1. 在文本编辑器中,我们每敲一个文字,都会调用 Editor 类中的 appendCharacter() 方法,创建一个新的 Character 对象,保存到 chars 数组中。如果一个文本文件中,有上万、十几万、几十万的文字,那我们就要在内存中存储这么多 Character 对象。那有没有办法可以节省一点内存呢?
    2. 实际上,在一个文本文件中,用到的字体格式不会太多,毕竟不大可能有人把每个文字都设置成不同的格式。所以,对于字体格式,我们可以将它设计成享元,让不同的文字共享使用。按照这个设计思路,我们对上面的代码进行重构。重构后的代码如下所示:
    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 单例、缓存、对象池

    在上面的讲解中,我们多次提到“共享”“缓存”“复用”这些字眼,那它跟单例、缓存、对象池这些概念有什么区别呢?我们来简单对比一下。

    我们先来看享元模式跟单例的区别

    1. 在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于之前讲到的单例的变体:多例。
    2. 我们前面也多次提到,区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。尽管从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。

    我们再来看享元模式跟缓存的区别

    在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用。

    最后我们来看享元模式跟对象池的区别

    1. 对象池、连接池(比如数据库连接池)、线程池等也是为了复用,那它们跟享元模式有什么区别呢?
    2. 你可能对连接池、线程池比较熟悉,对对象池比较陌生,所以,这里我简单解释一下对象池。像 C++ 这样的编程语言,内存的管理是由程序员负责的。为了避免频繁地进行对象创建和释放导致内存碎片,我们可以预先申请一片连续的内存空间,也就是这里说的对象池。每次创建对象时,我们从对象池中直接取出一个空闲对象来使用,对象使用完成之后,再放回到对象池中以供后续复用,而非直接释放掉。
    3. 虽然对象池、连接池、线程池、享元模式都是为了复用,但是,如果我们再细致地抠一抠“复用”这个字眼的话,对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。
    4. 池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。

    剖析享元模式在Java Integer、String中的应用

    我们先来看下面这样一段代码。你可以先思考下,这段代码会输出什么样的结果。

    Integer i1 = 56;Integer i2 = 56;Integer i3 = 129;Integer i4 = 129;System.out.println(i1 == i2);System.out.println(i3 == i4);
    
    1. Java提供了自动拆箱与装箱机制,比如int的装箱就是Integer.valueOf(); 拆箱就是i.intValue();
    2. 前 4 行赋值语句都会触发自动装箱操作,也就是会创建 Integer 对象并且赋值给 i1、i2、i3、i4 这四个变量。根据刚刚的讲解,i1、i2 尽管存储的数值相同,都是 56,但是指向不同的 Integer 对象,所以通过“”来判定是否相同的时候,会返回 false。同理,i3i4 判定语句也会返回 false。
    3. 不过,上面的分析还是不对,答案并非是两个 false,而是一个 true,一个 false。看到这里,你可能会比较纳闷了。实际上,这正是因为 Integer 用到了享元模式来复用对象,才导致了这样的运行结果。当我们通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象的时候,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建。看代码更加清晰一些,Integer 类的 valueOf() 函数的具体代码如下所示:
    public static Integer valueOf(int i) {       if (i >= IntegerCache.low && i <= IntegerCache.high)           return IntegerCache.cache[i + (-IntegerCache.low)];       return new Integer(i);   }
    

    实际上,这里的 IntegerCache 相当于,我们上面讲的生成享元对象的工厂类,只不过名字不叫 xxxFactory 而已。我们来看它的具体代码实现。这个类是 Integer 的内部类,你也可以自行查看 JDK 源码。

    private static class IntegerCache {       static final int low = -128;       static final int high;       static final Integer cache[];       static {           // high value may be configured by property           int h = 127;           String integerCacheHighPropValue =               VM.getSavedProperty("java.lang.Integer.IntegerCache.high");           if (integerCacheHighPropValue != null) {               try {                   int i = parseInt(integerCacheHighPropValue);                   i = Math.max(i, 127);                   // Maximum array size is Integer.MAX_VALUE                   h = Math.min(i, Integer.MAX_VALUE - (-low) -1);               } catch( NumberFormatException nfe) {                   // If the property cannot be parsed into an int, ignore it.               }           }           high = h;           cache = new Integer[(high - low) + 1];           int j = low;           for(int k = 0; k < cache.length; k++)               cache[k] = new Integer(j++);           // range [-128, 127] must be interned (JLS7 5.1.7)           assert IntegerCache.high >= 127;       }       private IntegerCache() {}   }
    
    1. 为什么 IntegerCache 只缓存 -128 到 127 之间的整型值呢?
    2. 在 IntegerCache 的代码实现中,当这个类被加载的时候,缓存的享元对象会被集中一次性创建好。毕竟整型值太多了,我们不可能在 IntegerCache 类中预先创建好所有的整型值,这样既占用太多内存,也使得加载 IntegerCache 类的时间过长。所以,我们只能选择缓存对于大部分应用来说最常用的整型值,也就是一个字节的大小(-128 到 127 之间的数据)。
    3. 实际上,JDK 也提供了方法来让我们可以自定义缓存的最大值,有下面两种方式。如果你通过分析应用的 JVM 内存占用情况,发现 -128 到 255 之间的数据占用的内存比较多,你就可以用如下方式,将缓存的最大值从 127 调整到 255。不过,这里注意一下,JDK 并没有提供设置最小值的方法。
    //方法一:-Djava.lang.Integer.IntegerCache.high=255//方法二:-XX:AutoBoxCacheMax=255
    
    1. 现在,让我们再回到最开始的问题,因为 56 处于 -128 和 127 之间,i1 和 i2 会指向相同的享元对象,所以 i1i2 返回 true。而 129 大于 127,并不会被缓存,每次都会创建一个全新的对象,也就是说,i3 和 i4 指向不同的 Integer 对象,所以 i3i4 返回 false。

    2. 实际上,除了 Integer 类型之外,其他包装器类型,比如 Long、Short、Byte 等,也都利用了享元模式来缓存 -128 到 127 之间的数据。比如,Long 类型对应的 LongCache 享元工厂类及 valueOf() 函数代码如下所示:

    3. 在我们平时的开发中,对于下面这样三种创建整型对象的方式,我们优先使用后两种。

    Integer a = new Integer(123);Integer a = 123;Integer a = Integer.valueOf(123);
    
    1. 第一种创建方式并不会使用到 IntegerCache,而后面两种创建方法可以利用 IntegerCache 缓存,返回共享的对象,以达到节省内存的目的。举一个极端一点的例子,假设程序需要创建 1 万个 -128 到 127 之间的 Integer 对象。使用第一种创建方式,我们需要分配 1 万个 Integer 对象的内存空间;使用后两种创建方式,我们最多只需要分配 256 个 Integer 对象的内存空间。

    享元模式在 Java String 中的应用

    1. 刚刚我们讲了享元模式在 Java Integer 类中的应用,现在,我们再来看下,享元模式在 Java String 类中的应用。同样,我们还是先来看一段代码,你觉得这段代码输出的结果是什么呢?
    String s1 = "哈哈哈";String s2 = "哈哈哈";String s3 = new  String("哈哈哈");System.out.println(s1 == s2);System.out.println(s1 == s3);
    
    1. 上面代码的运行结果是:一个 true,一个 false。跟 Integer 类的设计思路相似,String 类利用享元模式来复用相同的字符串常量(也就是代码中的“小争哥”)。JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。上面代码对应的内存存储结构如下所示:【笔者的JVM系列有专门讲String的各种情况】

    2. 不过,String 类的享元模式的设计,跟 Integer 类稍微有些不同。Integer 类中要共享的对象,是在类加载的时候,就集中一次性创建好的。但是,对于字符串来说,我们没法事先知道要共享哪些字符串常量,所以没办法事先创建好,只能在某个字符串常量第一次被用到的时候,存储到常量池中,当之后再用到的时候,直接引用常量池中已经存在的即可,就不需要再重新创建了。

  • 相关阅读:
    Java RunTime Environment (JRE) or Java Development Kit (JDK) must be available in order to run Eclipse. ......
    UVA 1597 Searching the Web
    UVA 1596 Bug Hunt
    UVA 230 Borrowers
    UVA 221 Urban Elevations
    UVA 814 The Letter Carrier's Rounds
    UVA 207 PGA Tour Prize Money
    UVA 1592 Database
    UVA 540 Team Queue
    UVA 12096 The SetStack Computer
  • 原文地址:https://www.cnblogs.com/coderD/p/15072989.html
Copyright © 2011-2022 走看看