转:http://terrylee.cnblogs.com/archive/2006/03/29/361767.html

摘要:面向对象的思想很好地解决了抽象性的问题,一般也不会出现性能上的问题。但是在某些情况下,对象的数量可能会太多,从而导致了运行时的代价。那么我们如何去避免大量细粒度的对象,同时又不影响客户程序使用面向对象的方式进行操作?

本文试图通过一个简单的字符处理的例子,运用重构的手段,一步步带你走进Flyweight模式,在这个过程中我们一同思考、探索、权衡,通过比较而得出好的实现方式,而不是给你最终的一个完美解决方案。

主要内容:

1.  Flyweight模式解说

2..NET中的Flyweight模式

3.Flyweight模式的实现要点

……

概述

面向对象的思想很好地解决了抽象性的问题,一般也不会出现性能上的问题。但是在某些情况下,对象的数量可能会太多,从而导致了运行时的代价。那么我们如何去避免大量细粒度的对象,同时又不影响客户程序使用面向对象的方式进行操作?

意图

运用共享技术有效地支持大量细粒度的对象。[GOF 《设计模式》]

结构图

图1  Flyweight模式结构图

 

Flyweight模式解说

Flyweight在拳击比赛中指最轻量级,即“蝇量级”,这里翻译为“享元”,可以理解为共享元对象(细粒度对象)的意思。提到Flyweight模式都会一般都会用编辑器例子来说明,这里也不例外,但我会尝试着通过重构来看待Flyweight模式。考虑这样一个字处理软件,它需要处理的对象可能有单个的字符,由字符组成的段落以及整篇文档,根据面向对象的设计思想和Composite模式,不管是字符还是段落,文档都应该作为单个的对象去看待,这里只考虑单个的字符,不考虑段落及文档等对象,于是可以很容易的得到下面的结构图:

图3

示意性实现代码:

// "Charactor"
public abstract class Charactor
{
    //Fields
    protected char _symbol;

protected int _width;

protected int _height;

protected int _ascent;

protected int _descent;

protected int _pointSize;

//Method
    public abstract void Display();
}

// "CharactorA"
public class CharactorA : Charactor

    // Constructor 
    public CharactorA()
    {
      this._symbol = 'A';
      this._height = 100;
      this._width = 120;
      this._ascent = 70;
      this._descent = 0;
      this._pointSize = 12;
    }

//Method
    public override void Display()
    {
        Console.WriteLine(this._symbol);
    }
}

// "CharactorB"
public class CharactorB : Charactor
{
    // Constructor 
    public CharactorB()
    {
        this._symbol = 'B';
        this._height = 100;
        this._width = 140;
        this._ascent = 72;
        this._descent = 0;
        this._pointSize = 10;
    }

//Method
    public override void Display()
    {
        Console.WriteLine(this._symbol);
    }
}

// "CharactorC"
public class CharactorC : Charactor
{
    // Constructor 
    public CharactorC()
    {
        this._symbol = 'C';
        this._height = 100;
        this._width = 160;
        this._ascent = 74;
        this._descent = 0;
        this._pointSize = 14;
    }

//Method
    public override void Display()
    {
        Console.WriteLine(this._symbol);
    }
}

好了,现在看到的这段代码可以说是很好地符合了面向对象的思想,但是同时我们也为此付出了沉重的代价,那就是性能上的开销,可以想象,在一篇文档中,字符的数量远不止几百个这么简单,可能上千上万,内存中就同时存在了上千上万个Charactor对象,这样的内存开销是可想而知的。进一步分析可以发现,虽然我们需要的Charactor实例非常多,这些实例之间只不过是状态不同而已,也就是说这些实例的状态数量是很少的。所以我们并不需要这么多的独立的Charactor实例,而只需要为每一种Charactor状态创建一个实例,让整个字符处理软件共享这些实例就可以了。看这样一幅示意图:

图4

现在我们看到的A,B,C三个字符是共享的,也就是说如果文档中任何地方需要这三个字符,只需要使用共享的这三个实例就可以了。然而我们发现单纯的这样共享也是有问题的。虽然文档中的用到了很多的A字符,虽然字符的symbol等是相同的,它可以共享;但是它们的pointSize却是不相同的,即字符在文档中中的大小是不相同的,这个状态不可以共享。为解决这个问题,首先我们将不可共享的状态从类里面剔除出去,即去掉pointSize这个状态(只是暂时的J),类结构图如下所示:

图5

示意性实现代码:

// "Charactor"
public abstract class Charactor
{
    //Fields
    protected char _symbol;

protected int _width;

protected int _height;

protected int _ascent;

protected int _descent;

//Method
    public abstract void Display();
}

// "CharactorA"
public class CharactorA : Charactor
{
    // Constructor 
    public CharactorA()
    {
        this._symbol = 'A';
        this._height = 100;
        this._width = 120;
        this._ascent = 70;
        this._descent = 0;
    }

//Method
    public override void Display()
    {
        Console.WriteLine(this._symbol);
    }
}

// "CharactorB"
public class CharactorB : Charactor
{
    // Constructor 
    public CharactorB()
    {
        this._symbol = 'B';
        this._height = 100;
        this._width = 140;
        this._ascent = 72;
        this._descent = 0;
    }

//Method
    public override void Display()
    {
        Console.WriteLine(this._symbol);
    }
}

// "CharactorC"
public class CharactorC : Charactor
{
    // Constructor 
    public CharactorC()
    {
        this._symbol = 'C';
        this._height = 100;
        this._width = 160;
        this._ascent = 74;
        this._descent = 0;
    }

//Method
    public override void Display()
    {
        Console.WriteLine(this._symbol);
    }
}

好,现在类里面剩下的状态都可以共享了,下面我们要做的工作就是控制Charactor类的创建过程,即如果已经存在了“A”字符这样的实例,就不需要再创建,直接返回实例;如果没有,则创建一个新的实例。如果把这项工作交给Charactor类,即Charactor类在负责它自身职责的同时也要负责管理Charactor实例的管理工作,这在一定程度上有可能违背类的单一职责原则,因此,需要一个单独的类来做这项工作,引入CharactorFactory类,结构图如下:

图6

示意性实现代码:

// "CharactorFactory"
public class CharactorFactory
{
    // Fields
    private Hashtable charactors = new Hashtable();

// Constructor 
    public CharactorFactory()
    {
        charactors.Add("A", new CharactorA());
        charactors.Add("B", new CharactorB());
        charactors.Add("C", new CharactorC());
    }
       
    // Method
    public Charactor GetCharactor(string key)
    {
        Charactor charactor = charactors[key] as Charactor;

if (charactor == null)
        {
            switch (key)
            {
                case "A": charactor = new CharactorA(); break;
                case "B": charactor = new CharactorB(); break; 
                case "C": charactor = new CharactorC(); break;
                //
            }
            charactors.Add(key, charactor);
        }
        return charactor;
    }
}

到这里已经完全解决了可以共享的状态(这里很丑陋的一个地方是出现了switch语句,但这可以通过别的办法消除,为了简单期间我们先保持这种写法)。下面的工作就是处理刚才被我们剔除出去的那些不可共享的状态,因为虽然将那些状态移除了,但是Charactor对象仍然需要这些状态,被我们剥离后这些对象根本就无法工作,所以需要将这些状态外部化。首先会想到一种比较简单的解决方案就是对于不能共享的那些状态,不需要去在Charactor类中设置,而直接在客户程序代码中进行设置,类结构图如下:

图7

示意性实现代码:

public class Program
{
    public static void Main()
    {
        Charactor ca = new CharactorA();
        Charactor cb = new CharactorB();
        Charactor cc = new CharactorC();

//显示字符

//设置字符的大小ChangeSize();
    }

public void ChangeSize()
    {
        //在这里设置字符的大小
    }
}

按照这样的实现思路,可以发现如果有多个客户端程序使用的话,会出现大量的重复性的逻辑,用重构的术语来说是出现了代码的坏味道,不利于代码的复用和维护;另外把这些状态和行为移到客户程序里面破坏了封装性的原则。再次转变我们的实现思路,可以确定的是这些状态仍然属于Charactor对象,所以它还是应该出现在Charactor类中,对于不同的状态可以采取在客户程序中通过参数化的方式传入。类结构图如下:

图8

示意性实现代码:

// "Charactor"
public abstract class Charactor
{
    //Fields
    protected char _symbol;

protected int _width;

protected int _height;

protected int _ascent;

protected int _descent;

protected int _pointSize;

//Method
    public abstract void SetPointSize(int size);
    public abstract void Display();
}

// "CharactorA"
public class CharactorA : Charactor
{
    // Constructor 
    public CharactorA()
    {
        this._symbol = 'A';
        this._height = 100;
        this._width = 120;
        this._ascent = 70;
        this._descent = 0;
    }

//Method
    public override void SetPointSize(int size)
    {
        this._pointSize = size;
    }

public override void Display()
    {
        Console.WriteLine(this._symbol +
          "pointsize:" + this._pointSize);
    }
}

// "CharactorB"
public class CharactorB : Charactor
{
    // Constructor 
    public CharactorB()
    {
        this._symbol = 'B';
        this._height = 100;
        this._width = 140;
        this._ascent = 72;
        this._descent = 0;
    }

//Method
    public override void SetPointSize(int size)
    {
        this._pointSize = size;
    }

public override void Display()
    {
        Console.WriteLine(this._symbol +
          "pointsize:" + this._pointSize);
    }
}

// "CharactorC"
public class CharactorC : Charactor
{
    // Constructor 
    public CharactorC()
    {
        this._symbol = 'C';
        this._height = 100;
        this._width = 160;
        this._ascent = 74;
        this._descent = 0;
    }

//Method
    public override void SetPointSize(int size)
    {
        this._pointSize = size;
    }

public override void Display()
    {
        Console.WriteLine(this._symbol +
          "pointsize:" + this._pointSize);
    }
}

// "CharactorFactory"
public class CharactorFactory
{
    // Fields
    private Hashtable charactors = new Hashtable();

// Constructor 
    public CharactorFactory()
    {
        charactors.Add("A", new CharactorA());
        charactors.Add("B", new CharactorB());
        charactors.Add("C", new CharactorC());
    }
       
    // Method
    public Charactor GetCharactor(string key)
    {
        Charactor charactor = charactors[key] as Charactor;

if (charactor == null)
        {
            switch (key)
            {
                case "A": charactor = new CharactorA(); break;
                case "B": charactor = new CharactorB(); break; 
                case "C": charactor = new CharactorC(); break;
                //
            }
            charactors.Add(key, charactor);
        }
        return charactor;
    }
}

public class Program
{
    public static void Main()
    {
        CharactorFactory factory = new CharactorFactory();

// Charactor "A"
        CharactorA ca = (CharactorA)factory.GetCharactor("A");
        ca.SetPointSize(12);
        ca.Display();
        
        // Charactor "B"
        CharactorB cb = (CharactorB)factory.GetCharactor("B");
        ca.SetPointSize(10);
        ca.Display();

// Charactor "C"
        CharactorC cc = (CharactorC)factory.GetCharactor("C");
        ca.SetPointSize(14);
        ca.Display();
    }
}

可以看到这样的实现明显优于第一种实现思路。好了,到这里我们就到到了通过Flyweight模式实现了优化资源的这样一个目的。在这个过程中,还有如下几点需要说明:

1.引入CharactorFactory是个关键,在这里创建对象已经不是new一个Charactor对象那么简单,而必须用工厂方法封装起来。

2.在这个例子中把Charactor对象作为Flyweight对象是否准确值的考虑,这里只是为了说明Flyweight模式,至于在实际应用中,哪些对象需要作为Flyweight对象是要经过很好的计算得知,而绝不是凭空臆想。

3.区分内外部状态很重要,这是享元对象能做到享元的关键所在。

到这里,其实我们的讨论还没有结束。有人可能会提出如下问题,享元对象(Charactor)在这个系统中相对于每一个内部状态而言它是唯一的,这跟单件模式有什么区别呢?这个问题已经很好回答了,那就是单件类是不能直接被实例化的,而享元类是可以被实例化的。事实上在这里面真正被设计为单件的应该是享元工厂(不是享元)类,因为如果创建很多个享元工厂的实例,那我们所做的一切努力都是白费的,并没有减少对象的个数。修改后的类结构图如下:

图9

示意性实现代码:

// "CharactorFactory"
public class CharactorFactory
{
    // Fields
    private Hashtable charactors = new Hashtable();

private CharactorFactory instance;
    // Constructor 
    private CharactorFactory()
    {
        charactors.Add("A", new CharactorA());
        charactors.Add("B", new CharactorB());
        charactors.Add("C", new CharactorC());
    }
    
    // Property
    public CharactorFactory Instance
    {
        get 
        {
            if (instance != null)
            {
                instance = new CharactorFactory();
            }
            return instance;
        }
    }

// Method
    public Charactor GetCharactor(string key)
    {
        Charactor charactor = charactors[key] as Charactor;

if (charactor == null)
        {
            switch (key)
            {
                case "A": charactor = new CharactorA(); break;
                case "B": charactor = new CharactorB(); break; 
                case "C": charactor = new CharactorC(); break;
                //
            }
            charactors.Add(key, charactor);
        }
        return charactor;
    }
}

.NET框架中的Flyweight

Flyweight更多时候的时候一种底层的设计模式,在我们的实际应用程序中使用的并不是很多。在.NET中的String类型其实就是运用了Flyweight模式。可以想象,如果每次执行string s1 = “abcd”操作,都创建一个新的字符串对象的话,内存的开销会很大。所以.NET中如果第一次创建了这样的一个字符串对象s1,下次再创建相同的字符串s2时只是把它的引用指向“abcd”,这样就实现了“abcd”在内存中的共享。可以通过下面一个简单的程序来演示s1和s2的引用是否一致:

public class Program
{
    public static void Main(string[] args)
    {
        string s1 = "abcd";
        string s2 = "abcd";

Console.WriteLine(Object.ReferenceEquals(s1,s2));

Console.ReadLine();
    }
}

可以看到,输出的结果为True。但是大家要注意的是如果再有一个字符串s3,它的初始值为“ab”,再对它进行操作s3 = s3 + “cd”,这时虽然s1和s3的值相同,但是它们的引用是不同的。关于String的详细情况大家可以参考SDK,这里不再讨论了。

效果及实现要点

1.面向对象很好的解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight设计模式主要解决面向对象的代价问题,一般不触及面向对象的抽象性问题。

2.Flyweight采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。

3.享元模式的优点在于它大幅度地降低内存中对象的数量。但是,它做到这一点所付出的代价也是很高的:享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。另外它将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。

适用性

当以下所有的条件都满足时,可以考虑使用享元模式:

1、   一个系统有大量的对象。

2、   这些对象耗费大量的内存。

3、   这些对象的状态中的大部分都可以外部化。

4、   这些对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中剔除时,每一个组都可以仅用一个对象代替。

5、   软件系统不依赖于这些对象的身份,换言之,这些对象可以是不可分辨的。

满足以上的这些条件的系统可以使用享元对象。最后,使用享元模式需要维护一个记录了系统已有的所有享元的表,而这需要耗费资源。因此,应当在有足够多的享元实例可供共享时才值得使用享元模式。

总结

Flyweight模式解决的是由于大量的细粒度对象所造成的内存开销的问题,它在实际的开发中并不常用,但是作为底层的提升性能的一种手段却很有效。

设计模式学习之享元模式(Flyweight,结构型模式)(20)的更多相关文章

  1. Flyweight(享元)--对象结构型模式

    1.意图 运用共享技术有效地支持大量细粒度的对象. 2.动机 Flyweight模式描述了如何共享对象,使得可以细粒度地使用它们,而无需高昂的代价.flyweight是一个共享对象,它可以同时在多个场 ...

  2. 设计模式学习之适配器模式(Adapter,结构型模式)(14)

    参考链接:http://www.cnblogs.com/zhili/p/AdapterPattern.html一.定义:将一个类的接口转换成客户希望的另一个接口.Adapter模式使得原本由于接口不兼 ...

  3. 享元模式 FlyWeight 结构型 设计模式(十五)

    享元模式(FlyWeight)  “享”取“共享”之意,“元”取“单元”之意. 意图 运用共享技术,有效的支持大量细粒度的对象. 意图解析 面向对象的程序设计中,一切皆是对象,这也就意味着系统的运行将 ...

  4. 设计模式11: Flyweight 享元模式(结构型模式)

    Flyweight 享元模式(结构型模式) 面向对象的代价 面向对象很好的解决了系统抽象性的问题,同时在大多数情况下也不会损及系统的性能.但是,在某些特殊应用中,由于对象的数量太大,采用面向对象会给系 ...

  5. 设计模式12: Proxy 代理模式(结构型模式)

    Proxy 代理模式(结构型模式) 直接与间接 人们对于复杂的软件系统常常有一种处理手法,即增加一层间接层,从而对系统获得一种更为灵活.满足特定需求的解决方案.如下图,开始时,A需要和B进行3次通信, ...

  6. 设计模式10: Facade 外观模式(结构型模式)

    Facade 外观模式(结构型模式) 系统的复杂度 假设我们要开发一个坦克模式系统用于模拟坦克车在各种作战环境中的行为,其中坦克系统由引擎.控制器.车轮.车身等各个子系统构成. internal cl ...

  7. 设计模式08: Composite 组合模式(结构型模式)

    Composite 组合模式(结构型模式) 对象容器的问题在面向对象系统中,我们常会遇到一类具有“容器”特征的对象——即他们在充当对象的同时,又是其他对象的容器. public interface I ...

  8. 设计模式07: Bridge 桥接模式(结构型模式)

    Bridge 桥接模式(结构型模式) 抽象与实现 抽象不应该依赖于实现细节,实现细节应该依赖于抽象. 抽象B稳定,实现细节b变化 问题在于如果抽象B由于固有的原因,本身并不稳定,也有可能变化,怎么办? ...

  9. 设计模式学习心得<享元模式 Flyweight>

    享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能.这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式. 享元模式尝 ...

  10. Java设计模式学习记录-享元模式

    前言 享元模式也是一种结构型模式,这篇是介绍结构型模式的最后一篇了(因为代理模式很早之前就已经写过了).享元模式采用一个共享来避免大量拥有相同内容对象的开销.这种开销最常见.最直观的就是内存损耗. 享 ...

随机推荐

  1. AtomicInteger学习

    面试时被问到了,补下 import java.util.concurrent.atomic.AtomicInteger; /** * Created by tzq on 2018/7/15. */ p ...

  2. Python——Flask框架——程序的结构

    一.项目结构 |-flasky |-app Flask程序一般都保存在这里 |-templates/ |-static/ |main/ |-__init__.py |-errors.py |-form ...

  3. [NOI2009]变换序列(二分图匹配)

    我们先不考虑字典序最小,先来求出一种可行解. 不难发现,对于每一个i值,它所对应的T值在模n意义下最多两个,于是我们可以用二分图匹配来判断. 那字典序最小呢? 回顾一下二分图匹配的算法:网络流?貌似不 ...

  4. 读zepto源码之工具函数

    读zepto源码之工具函数 Zepto 提供了丰富的工具函数,下面来一一解读. 源码版本 本文阅读的源码为 zepto1.2.0 $.extend $.extend 方法可以用来扩展目标对象的属性.目 ...

  5. Vue(小案例_vue+axios仿手机app)_实现用户评论

    一.前言 1.渲染评论列表 2.点击加载按钮,加载更多    3.提交评论 二.主要内容 1.评论列表一般是注册到一个全局的公共组件中 2.请求后台数据,渲染评论列表 (1)数据格式如下 地址 /ap ...

  6. centos7下tomcat8.5安装部署与优化

    转自:https://www.cnblogs.com/busigulang/articles/8529719.html centos 7 Tomcat 8.5 的安装及生产环境的搭建调优 一 安装to ...

  7. 使用idea搭建maven项目

    前言---2018-11-24 博主最近呀,也是一直在看书,但是呢有许多小伙伴和博主反应,在eclipse都会搭建maven项目,但是呢到了idea就不会了,于是了博主就起了个早床写一遍博客咯.希望对 ...

  8. centos中编译安装nginx+mysql +php(未完)

    参考地址:http://www.cnblogs.com/htian/p/5728599.html 去官网找到PCRE,并下载http://www.pcre.org/wget ftp://ftp.csx ...

  9. 2017-12-19python全栈9期第四天第二节之列表的增删查改之正向排序和倒向排序和反转

    #!/user/bin/python# -*- coding:utf-8 -*-li = [3,5,6546,6,8,324,2,1,34,5,6,7]# li.sort() #正向# print(l ...

  10. 【SQL】INSERT INTO SELECT语句与SELECT INTO FROM语句

    INSERT INTO SELECT语句与SELECT INTO FROM语句,都是将一个结果集插入到一个表中: #INSERT INTO SELECT语句 1.语法形式: Insert into T ...