26.2享元模式

享元模式(Flyweight),运用共享技术有效地支持大量细粒度的对象。
享元模式(Flyweight)结构图 第二十六章 项目多也别傻做——享元模式 - 图1代码示例
Flyweight 类,它是所有具体享元类的超类或接口,通过这个接口,Flyweight 可以接受并作用于外部状态。

  1. abstract class Flyweight
  2. {
  3. abstract function Operation($extrinsic_state);
  4. }

ConcreteFlyweight 是继承 Flyweight 超类或实现 Flyweight 接口,并为内部状态增加存储空间。

  1. class ConcreteFlyweight extends Flyweight
  2. {
  3. public function Operation($extrinsic_state)
  4. {
  5. echo '具体 Flyweight:' . $extrinsic_state . PHP_EOL;
  6. }
  7. }

UnsharedConcreteFlyweight 是指那些不需要共享的 Flyweight 子类。因为 Flyweight 接口共享成为可能,但它并不强制共享。

  1. class UnsharedConcreteFlyweight extends Flyweight
  2. {
  3. public function Operation($extrinsic_state)
  4. {
  5. echo '不共享的具体 Flyweight:' . $extrinsic_state . PHP_EOL;
  6. }
  7. }

FlyweightFactory,是一个享元工厂,用来创建并管理 Flyweight 对象。它主要是用来确保合理地共享 Flyweight,当用户请求一个 Flyweight 时,FlyweightFactory 对象提供一个已创建的实例或者创建一个(如果不存在的话)。

  1. class FlyweightFactory
  2. {
  3. private $flyweights = array();
  4. //初始化工厂时,先生成三个实例
  5. public function __construct()
  6. {
  7. $this->flyweights['X'] = new ConcreteFlyweight();
  8. $this->flyweights['Y'] = new ConcreteFlyweight();
  9. $this->flyweights['Z'] = new ConcreteFlyweight();
  10. }
  11. //根据客户端请求,获得已生成的实例
  12. public function GetFlyweight($key)
  13. {
  14. return $this->flyweights[$key];
  15. }
  16. }

客户端代码

  1. public function flyweightDemo()
  2. {
  3. //代码外部状态
  4. $extrinsic_state = 22;
  5. $f = new FlyweightFactory();
  6. $fx = $f->GetFlyweight('X');
  7. $fx->Operation(--$extrinsic_state);
  8. $fy = $f->GetFlyweight('Y');
  9. $fy->Operation(--$extrinsic_state);
  10. $fz = $f->GetFlyweight('Z');
  11. $fz->Operation(--$extrinsic_state);
  12. $uf = new UnsharedConcreteFlyweight();
  13. $uf->Operation(--$extrinsic_state);
  14. }

结果表示

  1. 具体 Flyweight21
  2. 具体 Flyweight20
  3. 具体 Flyweight19
  4. 不共享的具体 Flyweight18

FlyweightFactory 根据客户需求返回早已生成好的对象,但实际上时不一定需要的,完全可以初始化时什么也不做,到需要时,再去判断对象是否为 null 来决定是否实例化。
UnsharedConcreteFlyweight 的存在,是因为尽管大部分时间都需要共享对象来降低内存的损耗,但个别时候也有可能不需要共享的,那么此时的 UnsharedConcreteFlyweight 子类就有存在的必要了,它可以解决那些不需要共享对象的问题。

26.3网站共享代码

代码实现
网站抽象类

  1. abstract class Website
  2. {
  3. abstract function Used();
  4. }

具体网站类

  1. class ConcreteWebsite extends Website
  2. {
  3. private $name = '';
  4. public function __construct($name)
  5. {
  6. $this->name = $name;
  7. }
  8. public function Used()
  9. {
  10. echo '网站分类:' . $this->name . PHP_EOL;
  11. }
  12. }

网站工厂类

  1. //网站工厂
  2. class WebSiteFactory
  3. {
  4. private $flyweights = array();
  5. //获得网站分类
  6. public function GetWebSiteCategory($key)
  7. {
  8. if (!isset($this->flyweights[$key])) {
  9. $this->flyweights[$key] = new ConcreteWebsite($key);
  10. }
  11. return $this->flyweights[$key];
  12. }
  13. //获得网站分类总数
  14. public function GetWebSiteCount()
  15. {
  16. return count($this->flyweights);
  17. }
  18. }

客户端代码

  1. public function flyweightImpFirstED()
  2. {
  3. $f = new WebSiteFactory();
  4. //实例化“产品展示”的“网站”对象
  5. $fx = $f->GetWebSiteCategory('产品展示');
  6. $fx->Used();
  7. //共享上方生成的对象,不再实例化
  8. $fy = $f->GetWebSiteCategory('产品展示');
  9. $fy->Used();
  10. $fz = $f->GetWebSiteCategory('产品展示');
  11. $fz->Used();
  12. $fl = $f->GetWebSiteCategory('博客');
  13. $fl->Used();
  14. $fm = $f->GetWebSiteCategory('博客');
  15. $fm->Used();
  16. $fn = $f->GetWebSiteCategory('博客');
  17. $fn->Used();
  18. //统计实例化个数,结果为2
  19. echo '网站分类总数为:' . $f->GetWebSiteCount();
  20. }

结果显示

  1. 网站分类:产品展示
  2. 网站分类:产品展示
  3. 网站分类:产品展示
  4. 网站分类:博客
  5. 网站分类:博客
  6. 网站分类:博客
  7. 网站分类总数为:2

这样算是基本实现了享元模式的共享对象的目的,也就是说,不管建几个网站,只要是‘产品展示’,都是一样的,只要是‘博客’,也是完全相同的,但这样是有问题的,不同企业的网站数据是不同的,所以至少它们都应该有不同的账号。

26.4内部状态于外部状态

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

  1. //用户
  2. class User
  3. {
  4. private $name = '';
  5. public function __construct($name)
  6. {
  7. $this->name = $name;
  8. }
  9. public function getName()
  10. {
  11. return $this->name;
  12. }
  13. }

网站抽象类

  1. abstract class WebSite
  2. {
  3. //“使用”方法需要传递“用户”对象
  4. abstract function Used($user);
  5. }

具体网站类

  1. class ConcreteWebSite extends WebSite
  2. {
  3. private $name = '';
  4. public function __construct($name)
  5. {
  6. $this->name = $name;
  7. }
  8. //实现“User”方法
  9. public function Used($user)
  10. {
  11. echo '网站分类:' . $this->name . ' 用户:' . $user->getName() . PHP_EOL;
  12. }
  13. }

网站工厂类

  1. //网站工厂
  2. class WebSiteFactory
  3. {
  4. private $flyweights = array();
  5. //获得网站分类
  6. public function GetWebSiteCategory($key)
  7. {
  8. if (!isset($this->flyweights[$key])) {
  9. $this->flyweights[$key] = new ConcreteWebSite($key);
  10. }
  11. return $this->flyweights[$key];
  12. }
  13. //获得网站分类总数
  14. public function GetWebSiteCount()
  15. {
  16. return count($this->flyweights);
  17. }
  18. }

客户端代码

  1. public function flyweightImp()
  2. {
  3. $f = new WebSiteFactoryImp();
  4. $fx = $f->GetWebSiteCategory('产品展示');
  5. $fx->Used(new User('小菜'));
  6. $fy = $f->GetWebSiteCategory('产品展示');
  7. $fy->Used(new User('大鸟'));
  8. $fz = $f->GetWebSiteCategory('产品展示');
  9. $fz->Used(new User('娇娇'));
  10. $fl = $f->GetWebSiteCategory('博客');
  11. $fl->Used(new User('老顽童'));
  12. $fm = $f->GetWebSiteCategory('博客');
  13. $fm->Used(new User('桃谷六仙'));
  14. $fn = $f->GetWebSiteCategory('博客');
  15. $fn->Used(new User('南海鳄神'));
  16. echo '得到网站分类总数为:' . $f->GetWebSiteCount();
  17. }

显示结果 尽管给六个不同用户使用网站,但实际上只有两个网站实例。

  1. 网站分类:产品展示 用户:小菜
  2. 网站分类:产品展示 用户:大鸟
  3. 网站分类:产品展示 用户:娇娇
  4. 网站分类:博客 用户:老顽童
  5. 网站分类:博客 用户:桃谷六仙
  6. 网站分类:博客 用户:南海鳄神
  7. 得到网站分类总数为:2

26.5享元模式应用

享元模式的应用场景:

  • 如果一个应用程序使用了大量的对象,而大量的这些对象造成了很大的存储开销时就应该考虑使用;
  • 还有就是对象的大多数状态可以外部状态,如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象,此时可以考虑使用享元模式。

虽说享元模式更多的时候是一种底层的设计模式,但现实中也是有应用的。比如说休闲游戏开发中,像围棋、五子棋、跳棋等,它们都有大量的棋子对象,对于围棋和五子棋只有黑白两色、跳棋颜色略多一些,但也是不太变化的,所以颜色应该是棋子的内部状态,而各棋子之间的差别主要就是位置的不同,所以方位坐标应该是棋子的外部状态,享元模式可以将棋子对象减少到有限个实例。
在某些情况下,对象的数量可能会太多,从而导致了运行时的资源与性能损耗。如何去避免大量细粒度的对象,同时又不影响客户程序,是一个值得去思考的问题,享元模式,可以运用共享技术有效地支持大量细粒度的对象。不过,使用享元模式需要维护一个记录了系统已有的所有享元的列表,而这本身需要消耗资源,另外享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。因此,应当在有足够多的对象实例可供共享时才值得使用享元模式。