前面我们已经讲了结构型设计模式中的,代理模式、桥接模式、装饰器模式、适配器模式、门面模式以及组合模式。今天我们来讲结构型设计模式的最后一个模式-亨元(Flyweight)模式

享元模式的定义为:采用一个共享类来避免大量拥有相同内容的“小类”的开销。这种开销中最常见、直观的影响就是增加了内存的损耗。享元模式以共享的方式高效的支持大量的细粒度对象,减少其带来的开销。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。

亨元模式的结构与实现

结构

代码实现

Flyweight.php

<?php
namespace DesignPatterns\Flyweight\example1;
/**
 * Flyweight类,它是所有具体亨元类的超类或者接口,通过这个接口Flyweight可以接受并作用于外部状态
 * Class Flyweight
 * @package DesignPatterns\Flyweight\example1
 */
abstract class Flyweight
{
    abstract public function Operation(int $extrinsicstate);
}

ConcreteFlyweight.php

<?php
namespace DesignPatterns\Flyweight\example1;
/**
 * ConcreteFlyweight继承Flyweight超类或实现Flyweight接口,并为内部状态增加储存空间
 * Class ConcreteFlyweight
 * @package DesignPatterns\Flyweight\example1
 */
class ConcreteFlyweight extends Flyweight
{

    public function Operation(int $extrinsicstate)
    {
        echo "具体Flyweight: ".$extrinsicstate."<br>";
    }
}

UnsharedConcreteFlyweight.php

<?php
namespace DesignPatterns\Flyweight\example1;
/**
 * UnsharedConcreteFlyweight 是指那些不需要共享的Flyweight子类,因为Flyweight接口并不强制共享
 * Class UnsharedConcreteFlyweight
 * @package DesignPatterns\Flyweight\example1
 */
class UnsharedConcreteFlyweight extends Flyweight
{

    public function Operation(int $extrinsicstate)
    {
        echo "不共享的具体Flyweight: ".$extrinsicstate."<br>";
    }
}

FlyweightFactory.php

<?php
namespace DesignPatterns\Flyweight\example1;
/**
 * FlyweightFactory 是一个亨元工厂用来创建并管理Flyweight对象它主要用来确保
 * 合理的共享Flyweight,当用户请求Flyweight时,FlyweightFactory对象提供一个已经创建的实例或者创建一个
 * Class FlyweightFactory
 * @package DesignPatterns\Flyweight\example1
 */
class FlyweightFactory
{
    private $flyweights = [];

    public function __construct()
    {
        //初始化是先生成三个实例
        $this->flyweights['X'] = new ConcreteFlyweight();
        $this->flyweights['Y'] = new ConcreteFlyweight();
        $this->flyweights['Z'] = new ConcreteFlyweight();
    }
    //根据客户端的请求获取已请求的实例
    public function GetFlyweight($key)
    {
        return $this->flyweights[$key];
    }
}

运行 Client.php

<?php
namespace DesignPatterns\Flyweight\example1;
require dirname(__DIR__).'/../vendor/autoload.php';
/**
 * Class Client
 * @package DesignPatterns\Flyweight\example1
 */
class Client
{

    public function run()
    {
        $extrinsicstate = 22;
        $FlyweightFactory = new FlyweightFactory();
        $fx = $FlyweightFactory->GetFlyweight('X');
        $fx->Operation(--$extrinsicstate);
        
        $fy = $FlyweightFactory->GetFlyweight('Y');
        $fy->Operation(--$extrinsicstate);
        
        $fz = $FlyweightFactory->GetFlyweight('Z');
        $fz->Operation(--$extrinsicstate);

        $UnsharedConcreteFlyweight = new UnsharedConcreteFlyweight();
        $UnsharedConcreteFlyweight->Operation(--$extrinsicstate);
    }
}
$worker = new Client();
$worker->run();

运行结果:

在本例中FlyweightFactory(亨元工厂)在初始化中直接生成好了客户需要的对象。实例开发中我们不一定需要,完全可以初始化时不操作,到需要时再去判断对象是否为null再决定是否实例化。关于UnsharedConcreteFlyweight,我们大部分的时间都需要都需要共享对象来降低内存的损耗,但个别时候也有可能不需要共享,那么此时的UnsharedConcreteFlyweight就有了存在的必要。他可以解决那些不需要共享对象的问题。

示例

假如你接手了一个项目,客户要求做一个产品展示的网站。这个不难实现,你花了一份时间就写好了并成功上线。后来又有大量的客户来找你,也希望能做这样的网站,但要求不太一样。有的客户希望是新闻发布形式的,有的希望是博客形式的。也有的希望是产品加图片说明形式的。你开始发愁了,随着这种需求的增多,每写一个网站你都要新建一份数据库,复制代码,建立空间等。如果有新的需求改动或bug,那么维护量就太大了。
我们先来看看我们现在的代码,如果每个网站是一个实例的话,代码应该是这样的

<?php
namespace DesignPatterns\Flyweight\example2\v1;
class WebSite
{
    private $name;
    public function __construct($name)
    {
        $this->name = $name;
    }

    public function use()
    {
        echo "网站分类: ".$this->name."<br>";
    }
}

//ClientCode
$WebSite1 = new WebSite('产品展示');
$WebSite1->use();

$WebSite2 = new WebSite('产品展示');
$WebSite2->use();

$WebSite3 = new WebSite('产品展示');
$WebSite3->use();

$WebSite4 = new WebSite('博客');
$WebSite4->use();

$WebSite5 = new WebSite('博客');
$WebSite5->use();

$WebSite6 = new WebSite('博客');
$WebSite6->use();

运行结果:

如果我们想要做三个产品展示,三个博客的网站,就需要6个网站类的实例。而其实它们的本质代码都差不多。如果网站增多,实例也就随着增多,对服务器的资源浪费也比较严重。如果有什么办法可以让网站的代码能够共用一套就好了。
我们发现虽然这些网站的模版和具体数据可能不同,但代码核心和数据库却是共享的。首先这些企业客户,他们需要的网站结构相似度很高,而且都不是那种高访问量的网站,如果分成多个虚拟空间来处理,相当于-一个相同网站的实例对象很多,这是造成服务器的大量资源浪费,当然更实际的其实就是钞票的浪费,如果整合到一个网站中,共享其相关的代码和数据,那么对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源,而对于代码,由于是一-份实例,维护和扩展都更加容易。那么如何做到共享一份实例呢?这就用到了我们今天讲的亨元模式。
下面是亨元模式的实现:
WebSite.php

<?php
namespace DesignPatterns\Flyweight\example2\v2;
/**
 * 网站抽象类
 * Class WebSite
 * @package DesignPatterns\Flyweight\example2\v2
 */
abstract class WebSite
{
    abstract public function use();
}

ConcreteWebSite.php

<?php
namespace DesignPatterns\Flyweight\example2\v2;
/**
 * 具体网站类
 * Class ConcreteWebSite
 * @package DesignPatterns\Flyweight\example2\v2
 */
class ConcreteWebSite extends WebSite
{
    private $name;
    public function __construct($name)
    {
        $this->name = $name;
    }

    public function use()
    {
        echo "网站分类: ".$this->name."<br>";
    }
}

WebSiteFactory.php

<?php
namespace DesignPatterns\Flyweight\example2\v2;
/**
 * 网站工厂类
 * Class WebSiteFactory
 * @package DesignPatterns\Flyweight\example2\v2
 */
class WebSiteFactory
{
    private $flyweights = [];

    //获得网站分类
    public function getWebSiteCategory(string $key)
    {
        //判断是否存在这个对象,如果存在则直接返回。如果不存在,则实例化后在返回
        if (empty($this->flyweights[$key])) {
            $this->flyweights[$key] = new ConcreteWebSite($key);
        }
        return $this->flyweights[$key];
    }
    //获得网站分类总数
    public function getWebSiteCount()
    {
        return count($this->flyweights);
    }
}

运行 Client.php

<?php
namespace DesignPatterns\Flyweight\example2\v2;

require dirname(dirname(__DIR__)).'/../vendor/autoload.php';

/**
 * Class Client
 * @package DesignPatterns\Adapter\example2\v2
 */
class Client
{
    public function run()
    {
        $websiteFactory = new WebSiteFactory();
        //实例化产品展示的对象
        $website1 = $websiteFactory->getWebSiteCategory('产品展示');
        $website1->use();
        //共享上方生成的对象不在实例化
        $website2 = $websiteFactory->getWebSiteCategory('产品展示');
        $website2->use();

        $website3 = $websiteFactory->getWebSiteCategory('产品展示');
        $website3->use();

        $website4 = $websiteFactory->getWebSiteCategory('博客');
        $website4->use();

        $website5 = $websiteFactory->getWebSiteCategory('博客');
        $website5->use();

        $website6 = $websiteFactory->getWebSiteCategory('博客');
        $website6->use();

        //统计实例化个数 结果为2
        echo "网站分类总数为: ".$websiteFactory->getWebSiteCount();
    }
}
$worker = new Client();
$worker->run();

运行结果:

这样写完算是基本实现了享元模式的共享对象的目的,也就是说,不管建几个网站,只要是‘产品展示’,都是一样的,只要是‘博客’,也是完全相同的。但这样是有问题的,如果我们给企业建的网站不是一家企业的,它们的数据不会相同,所以至少它们都应该有不同的账号,这样应该如何处理?这个时候我们就需要考虑亨元模式的内部状态与外部状态了。

内部状态与外部状态

在享元对象内部并且不会随环境改变而改变的共享部分,可以称为是享元对象的内部状态,而随环境改变而改变的、不可以共享的状态就是外部状态了。事实上,享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个参数外基本上都是相同的,有时就能够受大幅度地减少需要实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递进来,就可以通过共享大幅度地减少单个实例的数目。也就是说,享元模式Flyweight 执行时所需的状态是有内部的也可能有外部的,内部状态存储于ConcreteFlyweight对象之中,而外部对象则应该考虑由客户端对象存储或计算,当调用Flyweight 对象的操作时,将该状态传递给它。
也就是说客户的账号就是外部状态应该由专门的对象来处理。
代码示例
User.php

<?php
namespace DesignPatterns\Flyweight\example2\v3;
/**
 * 用户类,用于网站的客户账号,是“网站”类的外部状态
 * Class User
 * @package DesignPatterns\Flyweight\example2\v3
 */
class User
{
    private $name;
    public function __construct($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }
}

WebSite.php

<?php
namespace DesignPatterns\Flyweight\example2\v3;
/**
 * 网站抽象类
 * Class WebSite
 * @package DesignPatterns\Flyweight\example2\v3
 */
abstract class WebSite
{
    abstract public function use(User $user);
}

ConcreteWebSite.php

<?php
namespace DesignPatterns\Flyweight\example2\v3;
/**
 * 具体网站类
 * Class ConcreteWebSite
 * @package DesignPatterns\Flyweight\example2\v3
 */
class ConcreteWebSite extends WebSite
{
    private $name;
    public function __construct($name)
    {
        $this->name = $name;
    }

    public function use(User $user)
    {
        echo "网站分类: ".$this->name." 用户: ".$user->getName()."<br>";
    }
}

WebSiteFactory.php

<?php
namespace DesignPatterns\Flyweight\example2\v3;
/**
 * 网站工厂类
 * Class WebSiteFactory
 * @package DesignPatterns\Flyweight\example2\v3
 */
class WebSiteFactory
{
    private $flyweights = [];

    //获得网站分类
    public function getWebSiteCategory(string $key)
    {
        //判断是否存在这个对象,如果存在则直接返回。如果不存在,则实例化后在返回
        if (empty($this->flyweights[$key])) {
            $this->flyweights[$key] = new ConcreteWebSite($key);
        }
        return $this->flyweights[$key];
    }
    //获得网站分类总数
    public function getWebSiteCount()
    {
        return count($this->flyweights);
    }
}

运行 Client.php

<?php
namespace DesignPatterns\Flyweight\example2\v3;

require dirname(dirname(__DIR__)).'/../vendor/autoload.php';

/**
 * Class Client
 * @package DesignPatterns\Adapter\example2\v3
 */
class Client
{
    public function run()
    {
        $websiteFactory = new WebSiteFactory();
        //实例化产品展示的对象
        $website1 = $websiteFactory->getWebSiteCategory('产品展示');
        $website1->use(new User('张三'));
        //共享上方生成的对象不在实例化
        $website2 = $websiteFactory->getWebSiteCategory('产品展示');
        $website2->use(new User('小明'));

        $website3 = $websiteFactory->getWebSiteCategory('产品展示');
        $website3->use(new User('小花'));

        $website4 = $websiteFactory->getWebSiteCategory('博客');
        $website4->use(new User('王武'));

        $website5 = $websiteFactory->getWebSiteCategory('博客');
        $website5->use(new User('李白'));

        $website6 = $websiteFactory->getWebSiteCategory('博客');
        $website6->use(new User('杜甫'));

        //统计实例化个数 结果为2
        echo "网站分类总数为: ".$websiteFactory->getWebSiteCount();
    }
}
$worker = new Client();
$worker->run();

运行结果:

这样就可以协调内部与外部状态了。由于用了享元模式,哪怕你接手了1000 个网站的需求,只要要求相同或类似,你的实际开发代码也就是分类的那几种,对于服务器来说,占用的硬盘空间、内存、CPU资源都是非常少的。

总结

享元模式 运用共享技术有效地支持大量细粒度的对象
享元模式可以避免大量非常类似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个参数外基本上都是相同的,有时就能够受大幅度地减少需要实例化的类的数量。如果能把参数移到类实例的外面,在方法调用时将它们传递进来,就可以通过共享大幅度地减少单个实例的数目。
如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用;还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑使用享元模式。

参考书籍:《 大话设计模式》
github示例:https://github.com/yangpanyao/design-patterns/tree/master/Flyweight

Last modification:June 29th, 2020 at 10:24 pm