更多优质内容
请关注公众号

面向对象和设计模式(二十) 节省内存的享元模式——PHP语言实现-张柏沛IT博客

正文内容

面向对象和设计模式(二十) 节省内存的享元模式——PHP语言实现

栏目:PHP 系列:面向对象与设计模式 发布时间:2023-05-03 11:11 浏览量:2074
本系列文章目录
展开/收起

一、享元模式概念


“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象节省内存,前提是享元对象是不可变对象。


我们将对象的信息分为两个部分: 内部状态和外部状态。

内部状态是对象内部共享的属性,不随环境的改变而改变。

外部状态不可以被共享的,会随环境改变而变化的属性。

享元模式具体来说就是当一个系统中存在大量相似对象的时候,可以将这些对象中相同且不变的部分(不可变的相同属性)提取出来,设计成享元类对象,让这些大量相似的对象引用这些享元对象。

这里所说的内部状态的属性是指通过构造函数初始化后,这些成员变量就不会再被修改了。


下面是享元模式的类图和角色如下(享元的英文就是FlyWeight,FlyWeight的原义是轻量的意思,可见享元模式在节省内存、轻量化对象的作用):



FlyWeight:享元类的接口,operation方法是一些操作外部状态属性的方法,当然外部状态属性需要作为参数传入operation中。

ConcreteFlyWeight:具体的享元类,实现接口FlyWeight,存储内部状态属性。

FlyWeightFactory:享元工厂,用来创建和管理享元对象,其主要思想就是用一个Map来保存已经创建的享元对象实例。它主要是用来确保合理的共享FlyWeight对象,当用户请求一个FlyWeight对象时(getFlyWeight()方法),工厂提供一个已经存在的FlyWeight实例,如果不存在则创建一个返回。享元工厂保证一个享元对象只会有一份从而节省内存。


下面我们举两个例子来看看享元模式如何使用。

象棋游戏中,我们可以抽象出两个对象:棋局和棋子。棋子的属性包括:棋子类型(将、相、士、炮、车、兵等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。而棋局需要保存所有棋子对象。利用这些数据,我们就能显示一个完整的棋盘给玩家。

<?php
  class ChessPiece {//棋子
    private $id;        // 棋子id
    private $text;
    private $color;     // 棋子颜色
    private $positionX;
    private $positionY;

    const COLOR_RED = 1;
    const COLOR_BLACK = 2;
  
    public function __construct($id, $text, $color, $positionX, $positionY) {
      $this->id = $id;
      $this->text = $text;
      $this->color = $color;
      $this->positionX = $positionX;
      $this->positionY = $positionY;
    }
    // ...省略其他属性和getter/setter方法...
  }
  
  class ChessBoard {//棋局
    private $chessPieces = [];
  
    public function __construct() {
      $this->init();
    }
  
    private function init() {
      $this->chessPieces[1] = new ChessPiece(1, "車", ChessPiece::COLOR_BLACK, 0, 0);
      $this->chessPieces[2] = new ChessPiece(2,"馬", ChessPiece::COLOR_RED, 0, 1);
      //...省略摆放其他棋子的代码...
    }
  
    public function move($chessPieceId, $toPositionX, $toPositionY) {
      //...省略...
    }
  }


想想以前我们玩的QQ游戏,一个游戏厅里有成千上万个房间,一个房间有一个棋局,为了记录每个房间当前的棋局情况,我们需要给每个房间都创建一个 ChessBoard 棋局对象和几十个棋子对象,会消耗大量的内存。

此时可以用到享元模式将相似对象中相同且不变的属性提取出来,id、text、color 属性在不同的房间中是不变,唯独 positionX、positionY 不同。

因此可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用,棋盘只需记录每个棋子的位置信息就可以了。


具体的代码实现如下所示:

<?php

  // 享元类
  class ChessPieceUnit{
    const COLOR_RED = 1;
    const COLOR_BLACK = 2;
    const CHESS_STYLE_MAP  = [
        1 => ["text"=>"車", "color" => self::COLOR_BLACK],
        2 => ["text"=>"馬", "color" => self::COLOR_RED],
        // ...其他棋子的固定属性
    ];
    private $id;        // 棋子id
    private $text;      // 棋子类型
    private $color;     // 棋子颜色

    public function __construct($id, $text, $color) {
        $this->id = $id;
        $this->text = $text;
        $this->color = $color;
    }
  }

  class ChessPieceUnitFactory{
    private static $piecesUnit = [];     // 保存所有的棋子享元,并且所有棋局和棋子想获取享元对象只能通过该工厂类获取,而不能自己实例化ChessPieceUnit,否则是做不到一个享元是有一个实例的目的了

    public static function get($id){
        if(!isset(self::$piecesUnit[$id])){
            $style = ChessPieceUnit::CHESS_STYPE_MAP[$id];
            $piecesUnit[$id] = new ChessPieceUnit($id, $style['text'], $style['color']);
        }
        return $piecesUnit[$id];
    }
  }

  class ChessPiece {//棋子
    private $unit;
    private $positionX;
    private $positionY;
  
    public function __construct(ChessPieceUnit $unit, $positionX, $positionY) {
      $this->unit = $unit;
      $this->positionX = $positionX;
      $this->positionY = $positionY;
    }
    // ...省略其他属性和getter/setter方法...
  }
  
  class ChessBoard {//棋局
    private $chessPieces = [];
  
    public function __construct() {
      $this->init();
    }
  
    private function init() {
      $this->chessPieces[1] = new ChessPiece(ChessPieceUnitFactory::get(1), 0, 0);
      $this->chessPieces[2] = new ChessPiece(ChessPieceUnitFactory::get(2), 0, 1);
      //...省略摆放其他棋子的代码...
    }
  
    public function move($chessPieceId, $toPositionX, $toPositionY) {
      //...省略...
    }
  }


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


我们在看第二个例子。

文本编辑器中,要在内存中表示一个文本文件,只需要记录文字和格式两部分信息就可以了,其中,格式又包括文字的字体、大小、颜色等信息。

文本文件中最小的组成单元是文字,一个文字就会包含属于自己的文字内容、字体、大小和颜色。但是按照正常的用户使用习惯,没有人会把一篇文章的所有文字设置成不同的格式。顶多就是标题是一个样式、正文是一个样式、正文中需要强调的内容加粗一下。

此时我们也可以使用享元模式,将相同样式的文字的样式作为享元对象存储起来。具体实现如下:

<?php

// 享元类
class CharacterStyle {
    private $uniqueId;
    private $font;
    private $size;
    private $colorRGB;
  
    public function __construct(Font $font, $size, $colorRGB) {
      $this->font = $font;
      $this->size = $size;
      $this->colorRGB = $colorRGB;
      $this->uniqueId = $this->getUniqueId();
    }
  
    // 判断传入的样式和本样式是否相同
    protected function getUniqueId() {
      return md5(json_encode($this->font).$this->size.json_encode($this->colorRGB));
    }

    public function uniqueId(){
        return $this->uniqueId;
    }
  }
  
  class CharacterStyleFactory {
    private static $styles = [];
  
    public static function getStyle(Font $font, $size, $colorRGB): CharacterStyle {
      $newStyle = new CharacterStyle($font, $size, $colorRGB);
      if(!isset(self::$styles[$newStyle->uniqueId()])){
          self::$styles[$newStyle->uniqueId()] = $newStyle;
      }else{
          $newStyle = self::$styles[$newStyle->uniqueId()];
      }
      return $newStyle;
    }
  }

  class Character {
    private $cnt;
    private $style;
  
    public function __construct($cnt, CharacterStyle $style) {
      $this->cnt = $cnt;
      $this->style = $style;
    }
  }
  
  class Editor {
    private $chars = [];
  
    public function appendCharacter($cnt, Font $font, $size, $colorRGB) {
      $character = new Character($cnt, CharacterStyleFactory::getStyle($font, $size, $colorRGB));
      $this->chars[] = $character;
    }
  }



二、享元对象的优缺点

享元模式是一个非常简单的模式, 它可以大大减少应用程序创建的对象,减低程序内存的占用,增强程序的性能,但它同时也提高了系统复杂性,需要分离出外部状态和内部状态, 而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱。



三、享元对象与单例、缓存、对象池的区别


享元模式 VS 单例

在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。

实际上,享元模式有点类似于之前讲到的单例的变体——多例。

从设计意图上来看,享元模式和单例是完全不同的。应用享元模式是为了对象复用,单纯是为了节省内存;而应用单例/多例模式是因为某个实体对象在系统中只应该保存一份,顺便节省了内存。


享元模式 VS 缓存

在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。

我们平时所讲的缓存,主要是为了提高访问效率,而非复用。


享元模式 VS 对象池

对象池、连接池(比如数据库连接池)、线程池等也是为了复用。

但池化技术中的“复用”可以理解为一种“独占式的重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。但在任意时刻,每一个对象、连接、线程,并不会在多个地方同时被使用,而是被一个使用者(线程、协程)独占(此时其他使用者无法占用这个连接),当使用完成之后,放回到池中,再由其他使用者重复利用。

享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。

故池化技术中的对象同一时刻只能被独占,其复用是为了节省初始化时间;而享元模式是同一时刻可被多个地方共享,其复用是为了节省内存。




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > 面向对象和设计模式(二十) 节省内存的享元模式——PHP语言实现

热门推荐
推荐新闻