您的位置:首页 > 编程语言 > C#

C#设计模式之12——享元模式

2012-02-27 14:32 375 查看
享元模式用于避免在大量非常相似的类方面的开销。在编程的时候可能会遇到需要生成大量的很小的用于表示数据的类实例。如果能意识到除了少数的几个参数之外,实例基本上都是相同的这一特性,就可以极大的降低需要实例化的不同类的数据。如果可以把这些参数变量移动到类的外部,并把他们作为方法调用参数传递到类实例中,就可以通过共享实例的方式来大大的减少单独实例的数目。

享元模式中,内部数据确定了实例的唯一性,外部数据则作为参数传递给实例。享元模式适用于诸如屏幕上单个字符或者图标一类的小的、细粒度的类。例如我们的例子中需要显示多个人,每个人显示一个文件夹图标,但是不同的是不同的人显示的位置不一样,而且不同的人对象的文件夹字不一样,这样我们如果存在很多的这样的对象,对将来的维护可能会带来非常的不便,也给系统带来很大的开销。

我们的文件夹可能存在两种状态,一种是选中一种是没有选中,这样如果不使用享元模式,如果公司中有上万人,每个实例维护自己的位置信息和要显示的名字信息,就需要这少维护上万个小的对象,然而文件夹的种类一共就两种,只有位置和显示的名字不同,所以可以通过享元模式来实现,这样就只需要维护两个类的实例,一个是选中的实例一个是没有选中的实例。在很大程度上减少了系统的开销和维护那么多实例的代价。

在文档中的每个字符都要使用字符类的单个实例来表示,不过字符在屏幕上的显示位置则作为外部数据进行保存。这种情况下,每个不同的字符只需要有一个实例就可以了,字符的每次显示只需要给方法传递相应的显示位置。

我们的用户界面如图:



当用户的鼠标指针移动到一个没有选中的文件夹上这个文件夹就变成深红色,取消选中的文件夹会回复到原来的颜色。

享元模式是类的可共享实例,我们需要一个工厂来完成。工厂通常是一个单件,记录了某个特定的类是否已经生成,这样就可以或者返回一个新的实例或是返回一个已经生成的实例的引用。

为了确保程序的某个部分可以使用享元模式,需要考虑程序是否可以把某些数据从类中移走,把这些数据作为类的外部数据。如果这一做法可以使得程序需要维护的不同类实例的数量大大减少,则就可以使用享元模式。

在我们的系统中,每个人都一个描述自我坐标和名字以及选中状态的对象是一种非常大的系统开销,所以可以使用享元模式减少系统开销,因为所有的对象类型只有两种:选中和没有选中。

我们创建一个FolderFactory的类,该类返回一个选中的或者未被选中的文件夹绘制类,不过只要每种类型的实例都已经被创建,就不会再创建额外的实例。这里的情况比较简单,我们的工厂可以创建一个所有可能实例的列表,然后根据需要给用户返回一个具体实例的引用。

FolderFactory的实现如下:

using System;
using System.Drawing ;
namespace Flyweight
{
/// <summary>
/// Summary description for FolderFactory.
/// </summary>
public class FolderFactory	{
private Folder selFolder, unselFolder;
//-----
public FolderFactory() 		{
//create the two folders
selFolder = new Folder(Color.Brown);
unselFolder = new Folder(Color.Bisque);
}
//-----
public Folder getFolder(bool selected) {
if(selected)
return selFolder;
else
return unselFolder;
}
}
}


在绘制文加件的时候需要把坐标、名字信息传递给调用函数,这样我们的文件夹绘制类的实现如下:

using System;
using System.Drawing ;
namespace Flyweight
{
/// <summary>
/// Summary description for Folder.
/// </summary>
public class Folder 	{
//Draws a folder at the specified coordinates
private const int w  = 50;
private const int h = 30;
private Pen blackPen, whitePen;
private Pen grayPen;
private SolidBrush backBrush, blackBrush;
private Font fnt;
//------
public Folder(Color col) 		{
backBrush = new SolidBrush(col);
blackBrush = new SolidBrush(Color.Black);
blackPen = new Pen(Color.Black);
whitePen = new Pen(Color.White);
grayPen = new Pen(Color.Gray);
fnt = new Font("Arial", 12);
}
//-----
public void draw(Graphics g, int x, int y, string title) {
g.FillRectangle(backBrush, x, y, w, h);
g.DrawRectangle(blackPen, x, y, w, h);
g.DrawLine(whitePen, x + 1, y + 1, x + w - 1, y + 1);
g.DrawLine(whitePen, x + 1, y, x + 1, y + h);

g.DrawRectangle(blackPen, x + 5, y - 5, 15, 5);
g.FillRectangle(backBrush, x + 6, y - 4, 13, 6);

g.DrawLine(grayPen, x, y + h - 1, x + w, y + h - 1);
g.DrawLine(grayPen, x + w - 1, y, x + w - 1, y + h - 1);
g.DrawString(title, fnt, blackBrush, x, y + h + 5);
}
}
}


为了以这种方式使用享元模式,我们的主程序必须把每个文件夹的位置都计算出来,最为其绘图例程的一部分,然后把这些位置传递给文件夹实例。主程序中通过维护一个ArrayList的列表表示一共有多少个文件夹,这里每个元素只是对两个实例之中的一个的引用,所以系统开销非常小。

有两个地方需要计算文件夹的位置:在绘制文件夹的时候还有在检查鼠标悬停于文件夹之上的时候。我们把确定位置的代码抽象出来放到一个Positioner类中是非常方便的做法:

using System;

namespace Flyweight
{
/// <summary>
/// Summary description for Positioner.
/// </summary>
public class Positioner 	{
private const int pLeft = 30;
private const int pTop  = 30;
private const int HSpace = 70;
private const int VSpace = 80;
private const int rowMax = 2;
private int x, y, cnt;
//-----
public Positioner()	{
reset();
}
//-----
public void reset() {
x = pLeft;
y = pTop;
cnt = 0;
}
//-----
public int nextX() {
return x;
}
//-----
public void incr() {
cnt++;
if (cnt > rowMax) {	//reset to start new row
cnt = 0;
x = pLeft;
y += VSpace;
}
else {
x += HSpace;
}
}
//-----
public int nextY() {
return y;
}
}
}


判断一个文件夹是否被选中,我们需要在图像框里检查鼠标的动作,如果鼠标被发现位于某个矩形的内部,就把相应的名字改为被选中的名字。通过一个自己定义的类判断鼠标是否停留在某个文件夹上:

using System;

namespace csPatterns {
public class Rectangle 	{
private int x1, x2, y1, y2;
private int w, h;
public Rectangle() 		{			}
//-----
public void init(int x, int y) {
x1 = x;
y1 = y;
x2 = x1 + w;
y2 = y1 + h;
}
//-----
public void setSize(int w_, int h_) {
w = w_;
h = h_;
}
//-----
public bool contains(int xp, int yp) {
return (x1 <= xp) && (xp <= x2) &&
(y1 <= yp) && (yp <= y2);
}
}
}


当鼠标移动的时候就可以调用鼠标移动事件,进行判断是否在某个文件夹上的判断:

private void Pic_MouseMove(object sender, MouseEventArgs e) {
string oldname = selectedName;  //save old name
bool found = false;
posn.reset ();
int i = 0;
selectedName = "";
while (i < names.Count && ! found) {
rect.init (posn.nextX() , posn.nextY ());
//see if a rectangle contains the mouse
if (rect.contains(e.X, e.Y) ){
selectedName = (string)names[i];
found = true;
}
posn.incr ();
i++;
}
//only refresh if mouse in new rectangle
if(  !oldname.Equals ( selectedName)) {
Pic.Refresh();
}
}


在主程序中,通过给PictureBox的Paint事件添加新的函数进行绘制文件夹:

private ArrayList names;
private Folder fol;
private const int pLeft = 30;
private const int pTop  = 30;
private const int HSpace = 70;
private const int VSpace = 80;
private const int rowMax = 2;
private string selectedName;
private csPatterns.Rectangle rect;
private FolderFactory folFact;
private Positioner posn;
//-----
private void init() {
names = new ArrayList();
names.Add("Adam");
names.Add("Bill");
names.Add("Charlie");
names.Add("Dave");
names.Add("Edward");
names.Add("Fred");
names.Add("George");
fol = new Folder(Color.Bisque);
selectedName = (string)names[0];
Pic.Paint += new PaintEventHandler (picPaint);
rect = new csPatterns.Rectangle();
rect.setSize(50, 30);
folFact = new FolderFactory();
posn = new Positioner ();
}


 

//-----------
private void picPaint(object sender,  PaintEventArgs e ) {
Graphics g = e.Graphics;
posn.reset ();
for(int i = 0; i < names.Count; i++) {
fol = folFact.getFolder(selectedName.Equals(  (string)names[i]) );
fol.draw(g, posn.nextX() , posn.nextY (), (string)names[i]);
posn.incr();
}
}
//-----


意识到享元模式的存在,一边在需要的时候能够使用它,这对我们来说是有意义的。

享元模式使用仅有的少数几个对象实例来表示程序中的许多不同对象,所有的这些实例通常以相同的基本属性作为内部数据,以及少数的几个属性来代表外部数据,这些外部数据因类实例的每种表现而各不相同。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息