项目多也别傻做——享元模式

项目多也别傻做——享元模式

继上一篇“世界需要和平——中介者模式”后,本文继续讲解《大话设计模式》第26章“项目多也别傻做——享元模式”。喜欢本书请到各大商城购买原书,支持正版。

大话设计模式前期读书系列:

本文正式开始


1 项目多也别傻做!

时间:7月9日21点 地点:小菜大鸟住所的客厅 人物:小菜、大鸟

“小菜,最近一直在忙些什么呢?回家就自个忙开了。”大鸟问道。

“哦,最近有朋友介绍我一些小型的外包项目,是给一些私营业主做网站,我想也不是太难的事情,对自己也是一个很好的编程锻炼,所以最近我都在开发当中。”

“哈,看来小菜有外快赚了,又能锻炼自己的技术,好事呀。”

“现实不是想象的这么简单。刚开始是为一个客户做一个产品展示的网站,我花了一个多星期的时间做好了,也帮他租用了虚拟空间,应该说都很顺利。”

“嗯,产品展示网站,这个应该不难实现。”

“而后,他另外的朋友也希望能做这样网站,我想这有何难,再租用一个空间,然后把之前的代码复制一份上传,就可以了。”

“哦,这好像有点问题,后来呢?”

“实际上却是他们的朋友都希望我来提供这样的网站,但要求就不太一样了,有的人希望是新闻发布形式的,有人希望是博客形式的,也有还是原来的产品图片加说明形式的,而且他们都希望在费用上能大大降低。可是每个网站租用一个空间,费用上降低是不太可能的。我在想如何办呢?”

“他们是不是都是类似的商家客户?要求也就是信息发布、产品展示、博客留言、论坛等功能?”

“是呀,要求差别不大。你说该如何办?”小菜问道。

“你的担心是对的,如果有100家企业来找你做网站,你难道去申请100个空间,用100个数据库,然后用类似的代码复制100遍,去实现吗?”

“啊,那如果有Bug或是新的需求改动,维护量就太可怕了。”

“先来看看你现在的做法。如果是每个网站一个实例,代码应该是这样的。”

网站类

//网站
class WebSite
{
  private string name = "";
  publicWebSite(string name)
  {
    this.name = name;
  }
  public void Use()
  {
    Console.WriteLine("网站分类:" + name);
  }
}

客户端代码

static void Main(string[] args)
{
  WebSite fx = new WebSite("产品展示");
  fx.Use();
  
  WebSite fy = new WebSite("产品展示");
  fy.Use();
  
  WebSite fz = new WebSite("产品展示");
  fz.Use();
  
  WebSite fl = new WebSite("博客");
  fl.Use();
  
  WebSite fm = new WebSite("博客");
  fm.Use();
  
  WebSite fn = new WebSite("博客");
  fn.Use();
  
  Console.Read();
}

结果显示

网站分类:产品展示
网站分类:产品展示
网站分类:产品展示
网站分类:博客
网站分类:博客
网站分类:博客

“对的,也就是说,如果要做三个产品展示,三个博客的网站,就需要六个网站类的实例,而其实它们本质上都是一样的代码,如果网站增多,实例也就随着增多,这对服务器的资源浪费得很严重。小菜,你说有什么办法解决这个问题?”

“我不知道,我想过大家的网站共用一套代码,但毕竟是不同的网站,数据都不相同的。”

“我就希望你说出共享代码这句话,为什么不可以呢﹖比如现在大型的博客网站、电子商务网站,里面每一个博客或商家也可以理解为一个小的网站,但它们是如何做的?”

“啊,我明白了,利用用户ID号的不同,来区分不同的用户,具体数据和模板可以不同,但代码核心和数据库却是共享的。”

“小菜又开窍了,项目多也别傻做呀。你想,首先你的这些企业客户,他们需要的网站结构相似度很高,而且都不是那种高访问量的网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,这是造成服务器的大量资源浪费,当然更实际的其实就是钞票的浪费,如果整合到一个网站中,共享其相关的代码和数据,那么对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源,而对于代码,由于是一份实例,维护和扩展都更加容易。”

“是的。那如何做到共享一份实例呢?”

2 享元模式

“哈,在弄明白如何共享代码之前,我们先来谈谈一个设计模式——享元模式。”

享元模式(Flyweight),运用共享技术有效地支持大量细粒度的对象。[DP]

享元模式(flyweight)结构图

项目多也别傻做——享元模式

Flyweight类,它是所有具体享元类的超类或接口,通过这个接口,Flyweight可以接受并作用于外部状态。

abstract class Flyweight
{
  public abstract void Operation(int extrinsicstate);
}

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

class ConcreteFlyweight : Flyweight
{
  public override void Operation(int extrinsicstate)
  {
    Console.WriteLine("具体Flyweight:" + extrinsicstate);
  }
}

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

class UnsharedConcreteFlyweight : Flyweight
{
  public override void Operation(int extrinsicstate)
  {
    Console.WriteLine("不共享的具体Flyweight: " + extrinsicstate);
  }
}

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

class FlyweightFactory
{
  private Hashtable flyweights = new Hashtable();
  
  // 初始化工厂时,先生成三个实例
  public FlyweightFactory()  
  {
    flyweights.Add("x", new ConcreteFlyweight());
    flyweights.Add("Y", new ConcreteFlyweight());
    flyweights.Add("z", new ConcreteFlyweight());
  }
  
  // 根据客户端请求,获得已生成的实例
  public Flyweight GetFlyweight(string key)
  {
    return ((Flyweight)flyweights[key]);
  }
}

客户端代码

static void Main(string[] args)
{
  // 代码外部状态
  int extrinsicstate = 22;
  
  FlyweightFactory f = new FlyweightFactory();
  
  Flyweight fx = f.GetFlyweight("x");
  fx.Operation(--extrinsicstate);
  
  Flyweight fy = f.GetFlyweight("Y");
  fy.Operation(--extrinsicstate);
  
  Flyweight fz = f.GetFlyweight("Z");
  fz.Operation(--extrinsicstate);
  
  UnsharedConcreteFlyweight uf = new UnsharedConcreteFlyweight();
  
  uf.Operation(--extrinsicstate);
  
  Console.Read();
}

结果表示

具体Flyweight:21
具体Flyweight:20
具体Flyweight:19
不共享的具体Flyweight:18

“大鸟,有个问题,”小菜问道,“FlyweightFactory根据客户需求返回早已生成好的对象,但一定要事先生成对象实例吗?”

“问得好,实际上是不一定需要的,完全可以初始化时什么也不做,到需要时,再去判断对象是否为null来决定是否实例化。”

“还有个问题,为什么要有UnsharedConcreteFlyweight的存在呢?”

“这是因为尽管我们大部分时间都需要共享对象来降低内存的损耗,但个别时候也有可能不需要共享的,那么此时的UnsharedConcreteFlyweight子类就有存在的必要了,它可以解决那些不需要共享对象的问题。”

3 网站共享代码

“好了,你试着参照这个样例来改写一下帮人做网站的代码。”大鸟接着说。

“哦,好的,那这样的话,网站应该有一个抽象类和一个具体网站类才可以,然后通过网站工厂来产生对象。我马上就去写。”

半小时后,小菜的第二版代码。

网站抽象类

abstract class WebSite
{
  public abstract void Use();
}

具体网站类

class ConcreteWebSite : WebSite
{
  private string name = "";
  public ConcreteWebSite(string name)
  {
    this.name = name;
  }
  
  public override void Use()
  {
    Console.WriteLine("网站分类:" + name);
  }
}

网站工厂类

//网站工厂
class WebSiteFactory
{
  private Hashtable flyweights = new Hashtable();

  // 获得网站分类
  public WebSite GetWebSiteCategory(string key)
  {
    // 判断是否存在这个对象,如果存在,则直接返回,若不存在,则实例化它再返回
    if (!flyweights.ContainsKey(key))
    {
      flyweights.Add(key, new ConcreteWebSite(key));
    }
    
    return ((WebSite)flyweights[key]);
  }
  
  // 获得网站分类总数(得到实例的个数)
  public int GetWebSiteCount()
  {
    return flyweights.Count;
  }
}

客户端代码

static void Main (string[]args)
{
  WebSiteFactory f = new WebSiteFactory();
  
  // 实例化“产品展示”的“网站”对象
  WebSite fx = f.GetWebSiteCategory("产品展示");
  fx.Use();
  
  // 共享上方生成的对象,不再实例化
  WebSite fy = f.GetWebSiteCategory("产品展示");
  fy.Use();
  
  WebSite fz = f.GetWebSiteCategory("产品展示");
  fz.Use();
  
  WebSite fl = f.GetWebSiteCategory("博客");
  fl.Use();
  
  WebSite fm = f.GetWebSiteCategory("博客");
  fm.Use();
  
  WebSite fn = f.GetWebSiteCategory(”博客");
  fn.Use();
  
  // 统计实例化个数,结果应该为2
  Console.WriteLine("网站分类总数为{0}", f.GetWebSiteCount());
  Console.Read() ;

结果显示

网站分类:产品展示
网站分类:产品展示
网站分类:产品展示
网站分类:博客
网站分类:博客
网站分类:博客
网站分类总数为 2

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

“啊,对的,实际上我这样写没有体现对象间的不同,只体现了它们共享的部分。”

4 内部状态与外部状态

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

“那你的意思是说,客户的账号就是外部状态,应该由专门的对象来处理。”

“来,你试试看。”

大约二十分钟后,小菜写出代码第三版。

代码结构图

项目多也别傻做——享元模式

用户类,用于网站的客户账号,是“网站”类的外部状态

//用户
public class User
{
  private string name;
  public User(string name)
  {
    this.name = name;
  }
  
  public string Name
  {
    get { return name; }
  }
}

网站抽象类

abstract class WebSite
{
  // “使用”方法需要传递“用户”对象
  public abstract void Use(User user);
}

具体网站类

class ConcreteWebSite : WebSite
{
  private string name = "";
  public ConcreteWebSite(string name)
  {
    this.name = name;
  }
  
  // 实现“Use”方法
  public override void Use(User user)
  {
    Console.WriteLine("网站分类: " + name + "用户:" + user.Name);
  }
}

网站工厂类

//网站工厂
class WebSiteFactory
{
  private Hashtable flyweights = new Hashtable();
  
  //获得网站分类
  public WebSite GetWebSiteCategory(string key)
  {
    if (!flyweights.ContainsKey(key))
    {
      flyweights.Add(key, new ConcreteWebSite(key));
    }
    
    return ((WebSite)flyweights[key]);
  }
  
  //获得网站分类总数
  public int GetWebSiteCount()
  {
    return flyweights.Count;
  }
}

客户端代码

static void Main(string[] args)
{
  WebSiteFactory f = new WebSiteFactory();
  
  WebSite fx = f.GetWebSiteCategory ("产品展示");
  fx.Use(new User("小菜"));
  
  WebSite fy = f.GetWebSiteCategory("产品展示");
  fy.Use(new User("大鸟"));
  
  WebSite fz = f.GetWebSiteCategory("产品展示");
  fz.Use(new User("娇娇""));
  
  WebSite fl = f.GetWebSiteCategory(“博客");
  fl.Use(new User("老顽童"));
  
  WebSite fm= f.GetWebSiteCategory("博客");
  fm.Use(new User(“桃谷六仙"));
  
  WebSite fn = f.GetWebSiteCategory(“博客");
  fn.Use(new User(“南海鳄神"));
  
  Console.WriteLine("得到网站分类总数为{0}", f.GetWebSiteCount());
  
  Console.Read();
}

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

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

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

5 享元模式应用

“大鸟,你通过这个例子来讲解享元模式虽然我是理解了,但在现实中什么时候才应该考思使用享元模式呢?”

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

“在实际使用中,享元模式到底能达到什么效果呢?”

“因为用了享元模式,所以有了共享对象,实例总数就大大减少了,如果共享的对象越多,存储节约也就越多,节约量随着共享状态的增多而增大。”

“能具体一些吗?有些什么情况是用到享元模式的?”

“哈,实际上在.NET 中,字符串 string就是运用了Flyweight模式。举个例子吧。Object.ReferenceEquals(object objA,object objB)方法是用来确定objA与objB是否是相同的实例,返回值为bool值。”

string titleA = "大话设计模式";
string titleB = "大话设计模式";
Console.WriteLine(0bject.ReferenceEquals(titleA, titleB));

“啊,返回值竟然是True,这两个字符串是相同的实例。”

“试想一下,如果每次创建字符串对象时,都需要创建一个新的字符串对象的话,内存的开销会很大。所以如果第一次创建了字符串对象titleA,下次再创建相同的字符串 titleB时只是把它的引用指向‘大话设计模式’,这样就实现了‘大话设计模式’在内存中的共享。”

“哦,原来我一直在使用享元模式呀,我以前都不知道。还有没有其他现实中的应用呢?”

“虽说享元模式更多的时候是一种底层的设计模式,但现实中也是有应用的。比如说休闲游戏开发中,像围棋、五子棋、跳棋等,它们都有大量的棋子对象,你分析一下,它们的内部状态和外部状态各是什么?”

“围棋和五子棋只有黑白两色、跳棋颜色略多一些,但也是不太变化的,所以颜色应该是棋子的内部状态,而各个棋子之间的差别主要就是位置的不同,所以方位坐标应该是棋子的外部状态。”

“对的,像围棋,一盘棋理论上有 361个空位可以放棋子,那如果用常规的面向对象方式编程,每盘棋都可能有两三百个棋子对象产生,一台服务器就很难支持更多的玩家玩围棋游戏了,毕竟内存空间还是有限的。如果用了享元模式来处理棋子,那么棋子对象可以减少到只有两个实例,结果……你应该明白的。”

“太了不起了,这的确是非常好地解决了对象的开销问题。”

“在某些情况下,对象的数量可能会太多,从而导致了运行时的资源与性能损耗。那么我们如何去避免大量细粒度的对象,同时又不影响客户程序,是一个值得去思考的问题,享元模式,可以运用共享技术有效地支持大量细粒度的对象。不过,你也别高兴得太早,使用享元模式需要维护一个记录了系统已有的所有享元的列表,而这本身需要耗费资源,另外享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。因此,应当在有足够多的对象实例可供共享时才值得使用享元模式。”

“哦,明白了,像我给人家做网站,如果就两三个人的个人博客,其实是没有必要考虑太多的。但如果是要开发一个可供多人注册的博客网站,那么用共享代码的方式是一个非常好的选择。”

“小菜,说了这么多,你网站赚到钱了是不是该报答一下呀。”

“哈,如果开发完成后客户非常满意,我一定……我一定……”

“一定什么?怎么这么不爽快。”

“我一定送你一个博客账号!”

“啊! ! !”


每天学一点,不贪多。

下一篇我们接着读“第27章 其实你不懂老板的心——解释器模式”,欢迎关注微信公众号【乐趣课堂】。

原文出处:微信公众号【乐趣课堂】

原文链接:

本文观点不代表Dotnet9立场,转载请联系原作者。

发表评论

登录后才能评论