享元模式是一种结构型模式,它摒弃了在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,让你能在有限的内存容量中载入更多对象。
假如你希望在长时间工作后放松一下,所以开发了一款简单的游戏:玩家们在地图上移动并相互射击。你决定实现一个真实的粒子系统,并将其作为游戏的特色。大量的子弹、导弹和爆炸弹片会在整个地图上穿行,为玩家提供紧张刺激的游戏体验。
开发完成后,你推送提交了最新版本的程序,并在编译游戏后将其发送给了一个朋友进行测试。尽管该游戏在你的电脑上完美运行,但是你的朋友却无法长时间进行游戏:游戏总是会在他的电脑上运行几分钟后崩溃。在研究了几个小时的调试消息记录后,你发现导致游戏崩溃的原因是内存容量不足。朋友的设备性能远比不上你的电脑,因此游戏运行在他的电脑上时很快就会出现问题。
真正的问题与粒子系统有关。每个粒子(一颗子弹、一枚导弹或一块弹片)都由包含完整数据的独立对象来表示。当玩家在游戏中鏖战进入高潮后的某一时刻,游戏将无法在剩余内存中载入新建粒子,于是程序就崩溃了。
仔细观察粒子Particle
类,你可能会注意到颜色(color)和精灵图(sprite)这两个成员变量所消耗的内存要比其他变量多得多。更糟糕的是,对于所有的粒子来说,这两个成员变量所存储的数据几乎完全一样(比如所有子弹的颜色和精灵图都一样)。
每个粒子的另一些状态(坐标、移动矢量和速度)则是不同的。因为这些成员变量的数值会不断变化。这些数据代表粒子在存续期间不断变化的情景,但每个粒子的颜色和精灵图则会保持不变。
对象的常量数据通常被称为内在状态,其位于对象中,其他对象只能读取但不能修改其数值。而对象的其他状态常常能被其他对象“从外部”改变,因此被称为外在状态。
享元模式建议不在对象中存储外在状态,而是将其传递给依赖于它的一个特殊方法。程序只在对象中保存内在状态,以方便在不同情景下重用。这些对象的区别仅在于其内在状态(与外在状态相比,内在状态的变体要少很多),因此你所需的对象数量会大大削减。
让我们回到游戏中。假如能从粒子类中抽出外在状态,那么我们只需三个不同的对象(子弹、导弹和弹片)就能表示游戏中的所有粒子。你现在很可能已经猜到了,我们将这样一个仅存储内在状态的对象称为享元。
享元:该术语来自拳击,代表50公斤级的拳击手。
那么外在状态会被移动到什么地方呢?总得有类来存储它们,对不对?在大部分情况中,它们会被移动到容器对象中,也就是我们应用享元模式前的聚合对象中。
在我们的例子中,容器对象就是主要的游戏Game
对象,其会将所有粒子存储在名为粒子particles
的成员变量中。为了能将外在状态移动到这个类中,你需要创建多个数组成员变量来存储每个粒子的坐标、方向矢量和速度。除此之外,你还需要另一个数组来存储指向代表粒子的特定享元的引用。这些数组必须保持同步,这样你才能够使用同一索引来获取关于某个粒子的所有数据。
更优雅的解决方案是创建独立的情景类来存储外在状态和对享元对象的引用。在该方法中,容器类只需包含一个数组。
稍等! 这样的话情景对象数量不是会和不采用该模式时的对象数量一样多吗?的确如此,但这些对象要比之前小很多。消耗内存最多的成员变量已经被移动到很少的几个享元对象中了。现在,一个享元大对象会被上千个情境小对象复用,因此无需再重复存储数千个大对象的数据。
由于享元对象可在不同的情景中使用,你必须确保其状态不能被修改。享元类的状态只能由构造函数的参数进行一次性初始化,它不能对其他对象公开其设置器或公有成员变量。
为了能更方便地访问各种享元,你可以创建一个工厂方法来管理已有享元对象的缓存池。工厂方法从客户端处接收目标享元对象的内在状态作为参数,如果它能在缓存池中找到所需享元,则将其返回给客户端;如果没有找到,它就会新建一个享元,并将其添加到缓存池中。
你可以选择在程序的不同地方放入该函数。最简单的选择就是将其放置在享元容器中。除此之外,你还可以新建一个工厂类,或者创建一个静态的工厂方法并将其放入实际的享元类中。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 | using System;using System.Collections.Generic;using System.Linq;// Use Json.NET library, you can download it from NuGet Package Managerusing Newtonsoft.Json;namespace RefactoringGuru.DesignPatterns.Flyweight.Conceptual{ // The Flyweight stores a common portion of the state (also called intrinsic // state) that belongs to multiple real business entities. The Flyweight // accepts the rest of the state (extrinsic state, unique for each entity) // via its method parameters. public class Flyweight { private Car _sharedState; public Flyweight(Car car) { this._sharedState = car; } public void Operation(Car uniqueState) { string s = JsonConvert.SerializeObject(this._sharedState); string u = JsonConvert.SerializeObject(uniqueState); Console.WriteLine($"Flyweight: Displaying shared {s} and unique {u} state."); } } // The Flyweight Factory creates and manages the Flyweight objects. It // ensures that flyweights are shared correctly. When the client requests a // flyweight, the factory either returns an existing instance or creates a // new one, if it doesn't exist yet. public class FlyweightFactory { private List<Tuple<Flyweight, string>> flyweights = new List<Tuple<Flyweight, string>>(); public FlyweightFactory(params Car[] args) { foreach (var elem in args) { flyweights.Add(new Tuple<Flyweight, string>(new Flyweight(elem), this.getKey(elem))); } } // Returns a Flyweight's string hash for a given state. public string getKey(Car key) { List<string> elements = new List<string>(); elements.Add(key.Model); elements.Add(key.Color); elements.Add(key.Company); if (key.Owner != null && key.Number != null) { elements.Add(key.Number); elements.Add(key.Owner); } elements.Sort(); return string.Join("_", elements); } // Returns an existing Flyweight with a given state or creates a new // one. public Flyweight GetFlyweight(Car sharedState) { string key = this.getKey(sharedState); if (flyweights.Where(t => t.Item2 == key).Count() == 0) { Console.WriteLine("FlyweightFactory: Can't find a flyweight, creating new one."); this.flyweights.Add(new Tuple<Flyweight, string>(new Flyweight(sharedState), key)); } else { Console.WriteLine("FlyweightFactory: Reusing existing flyweight."); } return this.flyweights.Where(t => t.Item2 == key).FirstOrDefault().Item1; } public void listFlyweights() { var count = flyweights.Count; Console.WriteLine($"\nFlyweightFactory: I have {count} flyweights:"); foreach (var flyweight in flyweights) { Console.WriteLine(flyweight.Item2); } } } public class Car { public string Owner { get; set; } public string Number { get; set; } public string Company { get; set; } public string Model { get; set; } public string Color { get; set; } } class Program { static void Main(string[] args) { // The client code usually creates a bunch of pre-populated // flyweights in the initialization stage of the application. var factory = new FlyweightFactory( new Car { Company = "Chevrolet", Model = "Camaro2018", Color = "pink" }, new Car { Company = "Mercedes Benz", Model = "C300", Color = "black" }, new Car { Company = "Mercedes Benz", Model = "C500", Color = "red" }, new Car { Company = "BMW", Model = "M5", Color = "red" }, new Car { Company = "BMW", Model = "X6", Color = "white" } ); factory.listFlyweights(); addCarToPoliceDatabase(factory, new Car { Number = "CL234IR", Owner = "James Doe", Company = "BMW", Model = "M5", Color = "red" }); addCarToPoliceDatabase(factory, new Car { Number = "CL234IR", Owner = "James Doe", Company = "BMW", Model = "X1", Color = "red" }); factory.listFlyweights(); } public static void addCarToPoliceDatabase(FlyweightFactory factory, Car car) { Console.WriteLine("\nClient: Adding a car to database."); var flyweight = factory.GetFlyweight(new Car { Color = car.Color, Model = car.Model, Company = car.Company }); // The client code either stores or calculates extrinsic state and // passes it to the flyweight's methods. flyweight.Operation(car); } }} |
---|
执行结果:
12345678910111213141516171819202122 | FlyweightFactory: I have 5 flyweights:Camaro2018_Chevrolet_pinkblack_C300_Mercedes BenzC500_Mercedes Benz_redBMW_M5_redBMW_white_X6Client: Adding a car to database.FlyweightFactory: Reusing existing flyweight.Flyweight: Displaying shared {"Owner":null,"Number":null,"Company":"BMW","Model":"M5","Color":"red"} and unique {"Owner":"James Doe","Number":"CL234IR","Company":"BMW","Model":"M5","Color":"red"} state.Client: Adding a car to database.FlyweightFactory: Can't find a flyweight, creating new one.Flyweight: Displaying shared {"Owner":null,"Number":null,"Company":"BMW","Model":"X1","Color":"red"} and unique {"Owner":"James Doe","Number":"CL234IR","Company":"BMW","Model":"X1","Color":"red"} state.FlyweightFactory: I have 6 flyweights:Camaro2018_Chevrolet_pinkblack_C300_Mercedes BenzC500_Mercedes Benz_redBMW_M5_redBMW_white_X6BMW_red_X1 |
---|
参考原文:享元设计模式