javascript设计模式--享元模式(flyweight)

   1 <!DOCTYPE html>
   2 <html>
   3 <head>
   4     <title>享元模式(flyweight)</title>
   5     <meta charset="utf-8">
   6 </head>
   7 <body>
   8 <input value="方案1" type="button">
   9 <input value="方案2" type="button">
  10 
  11 <div id="calendar-container"></div>
  12 
  13 <script>
  14 /**
  15  * 享元模式
  16  *
  17  * 定义:
  18  * 运用共享技术有效地支持大量细粒度的对象
  19  *
  20  * 本质:
  21  * 分离与共享
  22  *
  23  * 分离的是对象状态中的变与不变的部分,共享的是对象中不变的部分。享元模式的关键之处在于分离变与不变,把不变部分作为享元对象的内部状态,而变化部分则作为外部状态,由外部来维护,这样享元对象就能够被共享。
  24  *
  25  * 如果能够有效地减少对象的数量,减少重复的数据,那么就能够节省不少内存。一个基本的思路就是缓存这些包含着重复数据的对象,让这些对象只出现一次,也就只耗费一份内存。
  26  * 如果被缓存的对象的属性值经常变动,那就不适合缓存了。
  27  * 因此,需要分离出被缓存对象实例中,哪些数据是不变且重复出现的,哪些数据是经常变化的,真正应该被缓存的数据是那些不变且重复出现的数据,把它们称为对象的内部状态,而那些变化的数据就不缓存了,把它们称为对象的外部状态。
  28  * 这样在实现的时候,把内部状态分离出来共享,称之为享元,通过共享享元对象来减少对内存的占用。把外部状态分离出来,放到外部,让应用在使用的时候进行维护,并在需要的时候传递给享元对象使用。为了控制对内部状态的共享,并且让外部能简单地使用共享数据,提供一个工厂来管理享元,把它称为享元工厂。
  29  *
  30  * 1.变与不变
  31  * 享元模式的重点就在于分离变与不变。把一个对象的状态分成内部状态和外部状态,内部状态是不变的,外部状态是可变的。然后通过共享不变的部分,达到减少对象数量并节约内存的目的。在享元对象需要的时候,可以从外部传入外部状态给共享的对象,共享对象会在功能处理的时候,使用自己内部的状态和这些外部的状态。
  32  *
  33  * 2.共享与不共享
  34  * 在享元模式中,享元对象又有共享与不共享之分,这种情况通常出现在和组合模式合用的情况,通常共享的是叶子对象,一般不共享的部分是由共享部分组合而成的,由于所有细粒度的叶子对象已经缓存了,那么缓存组合对象就没有什么意义了。
  35  *
  36  * 3.内部状态和外部状态
  37  * 享元模式的内部状态,通常指的是包含享元对象内部的,对象本身的状态,是独立于使用享元的场景的信息,一般创建后就不再变化的状态,因此可以共享。
  38  * 外部状态指的是享元对象之外的状态,取决于使用享元的场景,会根据使用场景而变化,因此不可共享。如果享元对象需要这些外部状态的话,可以从外部传递到享元对象中,比如通过方法的参数来传递。
  39  * 也就是说享元模式真正缓存和共享的数据是享元的内部状态,而外部状态是不应该被缓存共享的。
  40  * 内部状态和外部状态是独立的。外部状态的变化不应该影响到内部状态。
  41  *
  42  * 4.实例池
  43  * 在享元模式中,为了创建和管理共享的享元部分,引入了享元工厂,享元工厂中一般都包含有享元对象的实例池,享元对象就是缓存在这个实例池中的。
  44  * 所谓实例池,指的是缓存和管理对象实例的程序,通常实例池会提供对象实例的运行环境,并控制对象实例的生存周期。
  45  * 工业级的实例池在实现上有两个最基本的难点,一个是动态控制实例数量,另一个是动态分配实例来提供给外部使用。这些都是需要算法来做保证的。
  46  * 在享元模式中,享元工厂中的实例池并没有那么复杂,因为共享的享元对象基本上都是一个实例,一般不会出现同一个享元对象有多个实例的情况。这样就不用去考虑动态创建和销毁享元对象实例的功能,也不存在动态调度的麻烦。
  47  *
  48  * 5.享元模式的调用顺序
  49  * 1)通过享元工厂来获取共享的享元对象
  50  * 2)创建相应的享元对象
  51  * 3)调用共享的享元对象的方法,传入外部状态
  52  *
  53  * 6.谁来初始化共享对象
  54  * 通常是第一次向享元工厂请求获取共享对象的时候,进行共享对象的初始化,而且多半都是在享元工厂内部实现,不会从外部传入共享对象。当然可以从外部传入一些创建共享对象需要的值,享元工厂可以按照这些值去初始化需要共享的对象,然后把创建好的共享对象的实例放入享元工厂内部的缓存中,以后再请求这个共享对象的时候就不用再创建了。
  55  */
  56 
  57 (function () {
  58     // 示例代码
  59 
  60     /**
  61      * 享元对象。封装Flyweight的内部状态,提供功能方法。
  62      * @param state 享元对象的内部状态的数据
  63      */
  64     function ConcreteFlyweight(state) {
  65         var intrinsicState = state;
  66 
  67         this.operation = function (extrinsicState) {
  68             // 具体的功能处理,可能会用到享元内部,外部的状态
  69         };
  70     }
  71 
  72     /**
  73      * 不需要共享的flyweight对象
  74      * 通常是将被共享的享元对象作为子结点组合出来的对象
  75      */
  76     function UnsharedConcreteFlyweight() {
  77         // 描述对象的状态
  78         var allState;
  79 
  80         this.operation = function (extrinsicState) {
  81             // 具体的功能处理
  82         };
  83     }
  84 
  85     // 享元工厂
  86     // 客户端不能直接创建共享享元对象实例,必须通过享元工厂来创建。
  87     function FlyweightFactory() {
  88         // 缓存多个Flyweight对象
  89         var fsMap = {};
  90 
  91         this.getFlyweight = function (key) {
  92             var f = fsMap[key];
  93 
  94             if (f == null) {
  95                 f = new ConcreteFlyweight(key);
  96                 fsMap[key] = f;
  97             }
  98 
  99             return f;
 100         };
 101     }
 102 
 103     new function () {
 104         // Client
 105         // 通常会维持一个对flyweight的引用
 106         // 计算或存储一个或多个flyweight的外部状态
 107     };
 108 
 109 })();
 110 
 111 (function () {
 112     // 权限控制
 113 
 114     // 封装授权数据中重复出现部分的享元对象
 115     function AuthorizationFlyweight(state) {
 116         var parts = state.split(',');
 117         // 内部状态, 安全实体
 118         var securityEntity = parts[0];
 119         // 内部状态,权限
 120         var permit = parts[1];
 121 
 122         this.getSecurityEntity = function () {
 123             return securityEntity;
 124         };
 125         this.getPermit = function () {
 126             return permit;
 127         };
 128         this.match = function (security, perm) {
 129             return securityEntity === security &&
 130                 permit === perm;
 131         };
 132         this.toString = function () {
 133             return securityEntity + ',' + permit;
 134         };
 135     }
 136 
 137     // 享元工厂,通常实现称为单例
 138     var FlyweightFactory = {
 139         fsMap: {},
 140         getFlyweight: function (key) {
 141             var f = this.fsMap[key];
 142 
 143             if (f == null) {
 144                 f = new AuthorizationFlyweight(key);
 145                 this.fsMap[key] = f;
 146             }
 147             return f;
 148         }
 149     };
 150 
 151     function isEmptyObj(obj) {
 152         for (var i in obj) return false;
 153         return true;
 154     }
 155 
 156     // 安全管理
 157     var SecurityMgr = {
 158         map: {},
 159         // 模拟登录
 160         login: function (user) {
 161             this.map[user] = this.queryByUser(user);
 162         },
 163         // 判断用户对某个安全实体是否拥有权限
 164         hasPermit: function (user, securityEntity, permit) {
 165             var col = this.map[user];
 166 
 167             if (col == null || isEmptyObj(col)) {
 168                 console.log(user + '没有登录或是没有被分配任何权限');
 169                 return false;
 170             }
 171 
 172             for (var i in col) {
 173                 if (!col.hasOwnProperty(i)) continue;
 174 
 175                 var flyweight = col[i];
 176                 console.log('flyweight == ' + flyweight);
 177 
 178                 if (flyweight.match(securityEntity, permit)) return true;
 179             }
 180 
 181             return false;
 182         },
 183         // 从数据库中获取某人所拥有的权限
 184         queryByUser: function (user) {
 185             var col = [];
 186 
 187             for (var i in DataBase) {
 188                 var s = DataBase[i];
 189                 var ss = s.split(',');
 190 
 191                 if (ss[0] === user) {
 192                     var fm = FlyweightFactory.getFlyweight(ss[1] + ',' + ss[2]);
 193                     col.push(fm);
 194                 }
 195             }
 196 
 197             return col;
 198         }
 199     };
 200 
 201     // mock data
 202     var DataBase = [
 203         '张三,人员列表,查看',
 204         '李四,人员列表,查看',
 205         '李四,资薪数据,查看',
 206         '李四,资薪数据,修改'
 207     ];
 208 
 209     for (var i = 0; i < 3; i++) {
 210         DataBase.push('张三' + i + ',人员列表,查看');
 211     }
 212 
 213     new function () {
 214         SecurityMgr.login('张三');
 215         SecurityMgr.login('李四');
 216         var f1 = SecurityMgr.hasPermit('张三', '资薪数据', '查看');
 217         var f2 = SecurityMgr.hasPermit('李四', '资薪数据', '查看');
 218 
 219         console.log('f1 == ' + f1);
 220         console.log('f2 == ' + f2);
 221 
 222         for (var i = 0; i < 3; i++) {
 223             SecurityMgr.login('张三' + i);
 224             SecurityMgr.hasPermit('张三' + i, '资薪数据', '查看');
 225         }
 226     };
 227 
 228 })();
 229 
 230 /*
 231 不需要共享的享元
 232 
 233 对于使用已经缓存的享元组合出来的对象,就没有必要再缓存了。也就是把已经缓存的享元当作叶子节点,组合出来的组合对象就不需要
 234 再被缓存了,也把这种享元成为复合享元。
 235 */
 236 
 237 /*
 238  享元的结构
 239  享元模式用于减少应用程序所需对象的数量。这是通过将对象的内部状态划分为内在数据(instrinsic data)和外在数据(extrinsic data)两类而实现的。内在数据是指类的内部方法所需要的信息。没有这种数据的话类就不能正常运行。外在数据则是可以从类身上剥离并存储在其外部的信息。我们可以将内在状态相同的所有对象替换为同一个共享对象,用这种方法可以把对象数量减少到不同内在状态的数量。
 240  创建这种共享对象需要使用工厂,而不是普通的构造函数。这样做可以跟踪到已经实例化的各个对象,从而仅当所需对象的内在状态不同于已有对象时才创建一个新对象。对象的外在状态被曝存在一个管理器对象中。在调用对象的方法时,管理器会把这些外在状态作为参数传入。
 241  */
 242 
 243 /*
 244  示例:汽车登记
 245  假设要开发一个系统,用以代表一个城市的所有汽车。你需要保存每一辆汽车的详细情况(品牌,型号和出厂日期)及其所有权的详细情况(车主姓名,车牌号和最近登记日期)。当然,你决定把每辆汽车表示为一个对象:
 246  */
 247 // Car class, un-optimized.
 248 /**
 249  * Car类
 250  * @param make 品牌
 251  * @param model 型号
 252  * @param year 出厂日期
 253  * @param owner 车主姓名
 254  * @param tag 车牌号
 255  * @param renewDate 最近登记日期
 256  * @constructor Car
 257  */
 258 var Car = function (make, model, year, owner, tag, renewDate) {
 259     this.make = make;
 260     this.model = model;
 261     this.year = year;
 262     this.owner = owner;
 263     this.tag = tag;
 264     this.renewDate = renewDate;
 265 };
 266 Car.prototype = {
 267     getMake: function () {
 268         return this.make;
 269     },
 270     getModel: function () {
 271         return this.model;
 272     },
 273     getYear: function () {
 274         return this.year;
 275     },
 276 
 277     transferOwnership: function (newOwner, newTag, newRenewDate) {
 278         this.owner = newOwner;
 279         this.tag = newTag;
 280         this.renewDate = newRenewDate;
 281     },
 282     renewRegistration: function (newRenewDate) {
 283         this.renewDate = newRenewDate;
 284     },
 285     isRegistrationCurrent: function () {
 286         var today = new Date();
 287         return today.getTime() < Date.parse(this.renewDate);
 288     }
 289 };
 290 /*
 291  这个系统最初表现不错。但是随着城市人口的增长,你发现它一天天地变慢。数以十万计的汽车对象耗尽了可用的计算资源。要想优化这个系统,可以采用享元模式减少所需对象的数目。
 292  优化工作的第一步是把内在状态与外在状态分开。
 293  */
 294 
 295 /*
 296  内在状态和外在状态
 297  将对象数据划分为内在和外在部分的过程有一定的随意性。既要维持每个对象的模块性,又想把尽可能多的数据作为外在数据处理。划分依据的选择多少有些主观性。在本例中,车的自然数据(品牌,型号和出厂日期)属于内在数据,而所有权数据(车主姓名,车牌号和最近登记日期)则属于外在数据。这意味着对于品牌,型号和出厂日期的每一种组合,只需要一个汽车对象就行,这个数目还是不少,不过与之前相比已经少了几个数量级。每个品牌-型号=出厂日期组合对应的那个实例将被所有该类型汽车的车主共享。下面是新版Car类的代码:
 298  */
 299 // Car class, optimized as a flyweight
 300 var Car = function (make, model, year) {
 301     this.make = make;
 302     this.model = model;
 303     this.year = year;
 304 };
 305 Car.prototype = {
 306     getMake: function () {
 307         return this.make;
 308     },
 309     getModel: function () {
 310         return this.model;
 311     },
 312     getYear: function () {
 313         return this.year;
 314     }
 315 };
 316 /*
 317  上面的代码删除了所有外在数据。所有处理登记事宜的方法都被转移到一个管理其对象中(不过,也可以将这些方法留在原地,并为其增加对应于各种外在数据的参数)。因为现在对象的数据已被分为两大部分,所以必须用工厂来实例化它。
 318  */
 319 
 320 /*
 321  用工厂进行实例化
 322  这个工厂很简单。它会检查之前是否已经创建过对应于指定品牌-型号-出厂日期组合的汽车,如果存在这样的汽车那就返回它,否则创建一辆新车,并把它包村起来供以后使用。这就确保了对应于每个唯一的内在状态,只会创建一个实例:
 323  */
 324 // CarFactory singleton
 325 var CarFactory = (function () {
 326     var createdCars = {};
 327 
 328     return {
 329         /**
 330          * 工厂方法
 331          * @param make 品牌
 332          * @param model 型号
 333          * @param year 出厂日期
 334          * @return new Car()
 335          */
 336         createCar: function (make, model, year) {
 337             // Check to see if this particular combination has
 338             // been created before.
 339             if (createdCars[make + '-' + model + '-' + year]) {
 340                 return createdCars[make + '-' + model + '-' + year];
 341             } else {
 342                 // Otherwise create a new instance and save it.
 343                 var car = new Car(make, model, year);
 344                 createdCars[make + '-' + model + '-' + year] = car;
 345                 return car;
 346             }
 347         }
 348     };
 349 })();
 350 
 351 /*
 352  封装在管理器中的外在状态
 353  要完成这种优化还需要一个对象。所有那些从Car对象中删除的数据必须有个保存地点,我们用一个单体来做封装这些数据的管理器。原先的每一个Car对象现在都被分割为外在数据及其所属的共享汽车对象的引用这样两部分。Car对象与车主数据的组合称为汽车记录(car record)。管理器存储着这两方面的信息。它还包含着从原先的Car类删除的方法:
 354  */
 355 // CarRecordManager singleton
 356 var CarRecordManager = (function () {
 357     var carRecordDatabase = {};
 358 
 359     return {
 360         // Add a new car record into the city's system
 361         addCarRecord: function (make, model, year, owner, tag, renewDate) {
 362             var car = CarFactory.createCar(make, model, year);
 363             carRecordDatabase[tag] = {
 364                 owner: owner,
 365                 renewDate: renewDate,
 366                 car: car
 367             };
 368         },
 369         // Methods previously contained in the Car class.
 370         transferOwnership: function (tag, newOwner, newTag, newRenewDate) {
 371             var record = carRecordDatabase[tag];
 372             record.owner = newOwner;
 373             record.tag = newTag;
 374             record.renewDate = newRenewDate;
 375         },
 376         renewRegistration: function (tag, newRenewDate) {
 377             carRecordDatabase[tag].renewDate = newRenewDate;
 378         },
 379         isRegistrationCurrent: function (tag) {
 380             var today = new Date();
 381             return today.getTime() < Date.parse(carRecordDatabase[tag].renewDate);
 382         }
 383     };
 384 })();
 385 /*
 386  从Car类剥离的所有数据现在都保存在CarRecordManager这个单体的私用属性carRecordDatabase中。这个carRecordDatabase对象要比以前使用的一大批对象高效得多。那些处理所有权事宜的方法现在也被封装在这个单体中,因为他们处理的都是外在数据。
 387  可以看出,这种优化是以复杂为代价的。原先有的只是一个类,而现在却变成了一个类和两个单体对象。把一个对象的数据保存在两个不同的地方这种做法有点令人困惑,但与所解决的性能问题相比,这两点都只是小问题。如果运用得当,那么享元模式能够显著的提升程序的性能。
 388  */
 389 
 390 /*
 391  管理外在状态
 392  管理享元对象的外在数据有许多不同方法。使用管理器对象是一种常见做法,这种对象有一个集中管理的数据库(centralized database),用于存放外在状态及其所属的享元对象。汽车登记那个示例就采用了这种方案。其优点在于简单,容易维护。这也是一种比较轻便的方案,因为用来保存外在数据的只是一个数组或对象字面量。
 393  另一种管理外在状态的办法是使用组合模式。你可以用对象自身的层次体系来保存信息,而不需要另外使用一个集中管理的数据库。组合对象的叶节点全都可以是享元对象,这样一来这些享元对象就可以在组合对象层次体系中多个地方被共享。对于大型的对象层次体系这非常有用,因为同样的数据用这种方案来表示时所需对象的数量要少得多。
 394  */
 395 
 396 
 397 /**
 398  * 示例:Web 日历
 399  *
 400  * 为了演示用组合对象来保存外在状态的具体做法,下面我们要创建一个Web日历。首先实现的是一个未经优化的,未使用享元的版本。这是一个大型组合对象,位于最顶层的是代表年份的组合对象。它封装着代表月份的组合对象,而后者又封装着代表日期的叶对象。这是一个简单的例子,它会按顺序显示每月中的各天,还会按顺序显示一年中的各个月:
 401  */
 402 
 403 (function () {
 404 
 405     // CalendarYear class, a composite
 406     var CalendarYear = function (year, parent) {
 407         // implements CalendarItem
 408         this.year = year;
 409         this.element = document.createElement('div');
 410         this.element.className = 'year';
 411         this.element.style.display = 'none';
 412         var title = document.createElement('h4');
 413         title.appendChild(document.createTextNode(year));
 414         parent.appendChild(this.element).appendChild(title);
 415 
 416         function isLeapYear(y) {
 417             return (y > 0) && !(y % 4) && ((y % 100) || !(y % 400));
 418         }
 419 
 420         this.months = [];
 421         // The number of days in each month.
 422         this.numDays = [31, isLeapYear(this.year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
 423         for (var i = 0; i < 12; i++) {
 424             this.months[i] = new CalendarMonth(i + 1, this.numDays[i], this.element);
 425         }
 426     };
 427     CalendarYear.prototype = {
 428         display: function () {
 429             for (var i = 0, len = this.months.length; i < len; i++) {
 430                 // Pass the call down to the next level
 431                 this.months[i].display();
 432             }
 433             this.element.style.display = 'block';
 434         }
 435     };
 436 
 437 
 438     // CalendarMonth class, a composite
 439     var CalendarMonth = function (monthNum, numDays, parent) {
 440         // implements CalendarItem
 441         this.monthNum = monthNum;
 442         this.element = document.createElement('div');
 443         this.element.className = 'month';
 444         this.element.style.display = 'none';
 445         var title = document.createElement('h5');
 446         title.appendChild(document.createTextNode(monthNum));
 447         parent.appendChild(this.element).appendChild(title);
 448 
 449         this.days = [];
 450         for (var i = 0; i < numDays; i++) {
 451             this.days[i] = new CalendarDay(i + 1, this.element);
 452         }
 453     };
 454     CalendarMonth.prototype = {
 455         display: function () {
 456             for (var i = 0, len = this.days.length; i < len; i++) {
 457                 this.days[i].display();
 458             }
 459             this.element.style.display = 'block';
 460         }
 461     };
 462 
 463 
 464     // CalendarDay class, a leaf
 465     var CalendarDay = function (date, parent) {
 466         // implements CalendarItem
 467         this.date = date;
 468         this.element = document.createElement('div');
 469         this.element.className = 'day';
 470         this.element.style.display = 'none';
 471         parent.appendChild(this.element);
 472     };
 473     CalendarDay.prototype = {
 474         display: function () {
 475             this.element.style.display = 'block';
 476             this.element.innerHTML = this.date;
 477         }
 478     };
 479 
 480     var input1 = document.getElementsByTagName('input')[0];
 481     input1.year = 2012;
 482     input1.onclick = function () {
 483         var a = new CalendarYear(this.year, document.getElementById('calendar-container'));
 484         a = new MethodProfiler(a);
 485         a.display();
 486         ++this.year;
 487     };
 488 
 489 })();
 490 /*
 491  这段代码的问题在于,你不得不为每一年创建365个CalendarDay对象。要创建一个显示10年的日历,需要实力花几千个CalendarDay对象。这些对象固然不大,但是无论什么类型的对象,如果其数目如此之多的话,都会给浏览器带来资源压力。更有效的做法是无论日历要显示多少年,都只用一个CalendarDay对象来代表所有日期。
 492  */
 493 
 494 /*
 495  把日期对象转化为享元
 496  把CalendarDay对象转化为享元对象的过程很简单。首先,修改CalendarDay类本身,出去其中保存的所有数据,让这些数据(日期和父元素)成为外在数据:
 497  */
 498 (function () {
 499 
 500     // CalendarYear class, a composite
 501     var CalendarYear = function (year, parent) {
 502         this.year = year;
 503         this.element = document.createElement('div');
 504         this.element.className = 'year';
 505         var title = document.createElement('h4');
 506         title.appendChild(document.createTextNode(year));
 507         parent.appendChild(this.element).appendChild(title);
 508 
 509         function isLeapYear(y) {
 510             return (y > 0) && !(y % 4) && ((y % 100 || !(y % 400)));
 511         }
 512 
 513         this.months = [];
 514         this.numDays = [31, isLeapYear(this.year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
 515         for (var i = 0; i < 12; i++) {
 516             this.months[i] = new CalendarMonth(i + 1, this.numDays[i], this.element);
 517         }
 518     };
 519     CalendarYear.prototype = {
 520         display: function () {
 521             for (var i = 0, len = this.months.length; i < len; i++) {
 522                 this.months[i].display();
 523             }
 524             this.element.style.display = 'block';
 525         }
 526     };
 527 
 528     // CalendarMonth class, a composite
 529     var CalendarMonth = function (monthNum, numDays, parent) {
 530         this.monthNum = monthNum;
 531         this.element = document.createElement('div');
 532         this.element.className = 'month';
 533         this.element.style.display = 'none';
 534         var title = document.createElement('h5');
 535         title.appendChild(document.createTextNode(monthNum));
 536         parent.appendChild(this.element).appendChild(title);
 537 
 538         this.days = [];
 539         for (var i = 0; i < numDays; i++) {
 540             /**------------------------------------**/
 541             this.days[i] = calendarDay;
 542             /**------------------------------------**/
 543         }
 544     };
 545     CalendarMonth.prototype = {
 546         display: function () {
 547             for (var i = 0, len = this.days.length; i < len; i++) {
 548                 /**------------------------------------**/
 549                 this.days[i].display(i + 1, this.element);
 550                 /**------------------------------------**/
 551             }
 552             this.element.style.display = 'block';
 553         }
 554     };
 555 
 556     // CalendarDay class, a leaf
 557     /**------------------------------------**/
 558     var CalendarDay = function () {
 559     };
 560     CalendarDay.prototype = {
 561         display: function (date, parent) {
 562             var element = document.createElement('div');
 563             element.className = 'day';
 564             parent.appendChild(element);
 565             element.innerHTML = date;
 566         }
 567     };
 568 
 569     /*
 570      接下来,创建日期对象的单个实例。所有CalendarMonth对象中都要使用这个实例。这里本来也可以像第一个示例那样使用工厂来创建该类的实例,不过,因为这个类只需要创建一个实例,所以直接实例化它就行了:
 571      */
 572     // Single instance of CalendarDay
 573     var calendarDay = new CalendarDay();
 574     /**------------------------------------**/
 575 
 576     var input1 = document.getElementsByTagName('input')[1];
 577     input1.year = 2012;
 578     input1.onclick = function () {
 579         var a = new CalendarYear(this.year, document.getElementById('calendar-container'));
 580         a = new MethodProfiler(a);
 581         a.display();
 582         ++this.year;
 583     };
 584     /*
 585      现在外在数据成了display方法的参数,而不是类的构造函数的参数。这是享元的典型工作方式。因为在此情况下有些(或全部)数据被曝存在对象之外,要想实现与之前同样的功能就必须把他们提供给各个方法。
 586      最后,CalendarMonth类也要略作修改。原来用CalendarDay类构造函数创建该类实例的那个表达式被替换为calendarDay对象,而那些原本提供给CalendarDay类构造函数的参数现在被转而提供给display方法。
 587      */
 588 })();
 589 
 590 /*
 591  外在数据保存在哪里
 592  本例没有像前面的例子那样使用一个中心数据库来保存所有从享元对象剥离的数据。实际上,其他类基本上没做什么修改,CalendarYear根本没改,而CalendarMonth只改了两行。这都是因为组合对象的结构本身就已经包含了所有的外在数据。由于月份对象中的所有日期对象被一次存放在一个数组中,所以它知道每一个日期对象的状态,从CalendarDay构造函数中剔除的两种数据都已经存在于CalendarMonth对象中。
 593 
 594  这就是组合模式与享元模式配合得如此完美的原因。组合对象通常拥有大量叶对象,它还保存着许多可作为外在数据处理的数据。也对象通常只包含极少的内在数据,所以很容易被转化为共享资源。
 595  */
 596 
 597 /**
 598  * 示例:工具提示对象
 599  *
 600  * 在HS对象需要创建HTML内容这种情况下,享元模式特别有用。那种会生成DOM元素的对象如果数目众多的话,会占用过多内存,使网页陷入泥沼。采用享元模式后,只需创建少许这种对象即可,所有需要这种对象的地方都可以共享它们。工具提示就是一个典型的例子。
 601  */
 602 
 603 // 未经优化的Tooltip类
 604 // Tooltip class, un-optimized
 605 function test() {
 606     var Tooltip = function (targetElement, text) {
 607         this.target = targetElement;
 608         this.text = text;
 609         this.delayTimeout = null;
 610         this.delay = 500;
 611 
 612         this.element = document.createElement('div');
 613         this.element.style.display = 'none';
 614         this.element.style.position = 'absolute';
 615         this.element.className = 'tooltip';
 616         document.body.appendChild(this.element);
 617         this.element.innerHTML = this.text;
 618 
 619         var that = this;
 620         this.target.addEventListener('mouoseover', function (e) {
 621             that.startDelay(e);
 622         }, false);
 623         this.target.addEventListener('mouseout', function (e) {
 624             that.hide();
 625         }, false);
 626     };
 627     Tooltip.prototype = {
 628         startDelay: function (e) {
 629             if (this.delayTimeout === null) {
 630                 var that = this,
 631                     x = e.clientX,
 632                     y = e.clientY;
 633                 this.delayTimeout = setTimeout(function () {
 634                     that.show(x, y);
 635                 }, this.delay);
 636             }
 637         },
 638         show: function (x, y) {
 639             clearTimeout(this.delayTimeout);
 640             this.delayTimeout = null;
 641             this.element.style.left = x + 'px';
 642             this.element.style.top = (y + 20) + 'px';
 643             this.element.style.display = 'block';
 644         },
 645         hide: function () {
 646             clearTimeout(this.delayTimeout);
 647             this.delayTimeout = null;
 648             this.element.style.display = 'none';
 649         }
 650     };
 651 
 652     var link1 = $('link-id1'),
 653         link2 = $('link-id2');
 654     var tt = new Tooltip(link1, 'Lorem ipsum....');
 655 }
 656 
 657 // 作为享元的Tooltip
 658 /*
 659  把Tooltip类转化为享元需要做三件事:把外在数据从Tooltip对象中删除;创建一个用来实例化Tooltip的工厂;创建一个用来保存外在数据的管理器。在这个例子,我们可以用一个单体同时扮演工厂和管理器的角色。此外,由于外在数据可以作为事件侦听器一部分保存,因此没有必要使用一个中心数据库。
 660  */
 661 
 662 function test() {
 663     // TooltipManager singleton, a flyweight factory and manager
 664     var TooltipManager = (function () {
 665         var storedInstance = null;
 666 
 667         // Tooltip class, as aflyweight
 668         var Tooltip = function () {
 669             this.delayTimeout = null;
 670             this.delay = 500;
 671 
 672             this.element = document.createElement('div');
 673             this.element.style.display = 'none';
 674             this.element.style.position = 'absolute';
 675             this.element.className = 'tooltip';
 676             document.body.appendChild(this.element);
 677         };
 678         Tooltip.prototype = {
 679             startDelay: function (e, text) {
 680                 if (this.delayTimeout === null) {
 681                     var that = this,
 682                         x = e.clientX,
 683                         y = e.clientY;
 684                     this.delayTimeout = setTimeout(function () {
 685                         that.show(x, y, text);
 686                     }, this.delay);
 687                 }
 688             },
 689             show: function (x, y, text) {
 690                 clearTimeout(this.delayTimeout);
 691                 this.delayTimeout = null;
 692                 this.element.innerHTML = text;
 693                 this.element.style.left = x + 'px';
 694                 this.element.style.top = (y + 20) + 'px';
 695                 this.element.style.display = 'block';
 696             },
 697             hide: function () {
 698                 clearTimeout(this.delayTimeout);
 699                 this.delayTimeout = null;
 700                 this.element.style.display = 'none';
 701             }
 702         };
 703 
 704         return {
 705             addTooltip: function (targetElement, text) {
 706                 // Get the tooltip object
 707                 var tt = this.getTooltip();
 708                 // Attach the events
 709                 targetElement.addEventListener('mouseover', function(e){
 710                     tt.startDelay(e, text);
 711                 }, false);
 712                 targetElement.addEventListener('mouseover', function(e){
 713                     tt.hide();
 714                 }, false);
 715             },
 716             getTooltip: function () {
 717                 if (storedInstance === null) {
 718                     storedInstance = new Tooltip();
 719                 }
 720                 return storedInstance;
 721             }
 722         };
 723     })();
 724 
 725 // Tooltip usage
 726     TooltipManager.addTooltip($('link-id2'), 'hello world');
 727 }
 728 /*
 729  上面的Tooltip类删除了原来的构造函数的所有参数以及注册事件处理器的代码。而startDelay和show方法则各增加了一个新的参数,这样一来,要显示的文字就可以作为外在数据传给他们。
 730 
 731  这个单体有两个方法,分别体现了他的两种角色,getTooltip是工厂方法,它与你之前见到过的其他享元的生成方法差不多。addTooltip则是管理器方法,它先获取一个Tooltip对象,然后后分别把两个匿名函数注册为目标元素的mouseover和mouseout事件侦听器。这个例子用不着创建中心数据库,因为那两个匿名函数中生成的闭包已经保存了外在数据。
 732  */
 733 
 734 /*
 735  现在生成的DOM元素已减至一个。这很重要,假如你想为工具提示添加阴影或iframe垫片等特性,那么每个Tooltip对象需要生成5-10个DOM元素。要是不把它实现为享元的话,网页将被成百上千个工具提示压垮。此外,享元模式的应用还减少了对箱内部保存的数据。
 736  */
 737 
 738 /*
 739  保存实例供以后重用
 740 
 741  模式对话框是享元模式的另一个适用场合。与工具提示一样,对话框对象也封装着数据和HTML内容。不过,后者包含的DOM元素要多得多,因此尽可能地减少其实例个数更显重要。问题在于网页上可能会同时出现不止一个对话框。实际上,你无法确却知道究竟需要多少对话框。既然如此,那有怎能得知需要用到多少实例呢?
 742 
 743  因为运行期间需要用到的实例的确却数目无法在开发期间确定,所以不能对实例的个数加以限制,而只能要用多少就创建多少,然后把它们保存起来供以后使用。这样就不用再次成熟期创建过程中的开销,而且所创建的实例的数目也刚好能满足需要。
 744  在这个示例中,DialogBox对象的实现细节并不重要。你只需要知道,它是资源密集型的对象,应该尽量少实例化。该类的基本框架以及它实现的接口如下:
 745  */
 746 
 747 // DialogBox class
 748 var DialogBox = function () {
 749     // implements DisplayModule
 750     this.wrapper = document.createElement('section');
 751     this.wrapper.className = 'dialog_wrapper';
 752     this.header = document.createElement('header');
 753     this.header.className = 'dialog_header';
 754     this.content = document.createElement('div');
 755     this.content.className = 'dialog_body';
 756     this.footer = document.createElement('footer');
 757     this.footer.className = 'dialog_footer';
 758 
 759     this.wrapper.appendChild(this.header);
 760     this.wrapper.appendChild(this.footer);
 761     this.wrapper.insertBefore(this.content, this.footer);
 762     this.wrapper.style.display = 'none';
 763     document.body.appendChild(this.wrapper);
 764 };
 765 DialogBox.prototype = {
 766     show: function (obj) {
 767         // Sets the content and shows the dialog box
 768         this.header.innerHTML = obj.header;
 769         this.content.innerHTML = obj.content;
 770         this.footer.innerHTML = obj.footer;
 771 
 772         this.wrapper.style.display = 'block';
 773     },
 774     hide: function () {
 775         // Hides the dialog box
 776         this.wrapper.style.display = 'none';
 777     },
 778     status: function () {
 779         // Returns 'visible' or 'hidden'
 780         var value = this.wrapper.style.display;
 781         if (!value) {
 782             if (document.defaultView && document.defaultView.getComputedStyle) {
 783                 var css = document.defaultView.getComputedStyle(this.wrapper, null);
 784                 value = css ? css.display : null;
 785             } else if (this.wrapper.currentStyle) {
 786                 value = this.wrapper.currentStyle.display;
 787             }
 788         }
 789         return value === 'none' ? 'hidden' : 'visible';
 790     }
 791 };
 792 
 793 /*
 794  控制享元数量的管理器。改管理器需要三个部件:一个用来显示对话框的方法,一个用来检查当前网页上正在使用的对话框的数目的方法,以及一个用来保存所生成的对话框的数据结构。我们用一个单体来包装这些部件,以确保管理器的唯一性:
 795  */
 796 // DialogBox class
 797 var DialogBox = function () {
 798     // implements DisplayModule
 799     this.wrapper = document.createElement('section');
 800     this.wrapper.className = 'dialog_wrapper';
 801     this.header = document.createElement('header');
 802     this.header.className = 'dialog_header';
 803     this.content = document.createElement('div');
 804     this.content.className = 'dialog_body';
 805     this.footer = document.createElement('footer');
 806     this.footer.className = 'dialog_footer';
 807 
 808     this.wrapper.appendChild(this.header);
 809     this.wrapper.appendChild(this.footer);
 810     this.wrapper.insertBefore(this.content, this.footer);
 811     this.wrapper.style.display = 'none';
 812     document.body.appendChild(this.wrapper);
 813 };
 814 DialogBox.prototype = {
 815     show: function (obj) {
 816         // Sets the content and shows the dialog box
 817         this.header.innerHTML = obj.header;
 818         this.content.innerHTML = obj.content;
 819         this.footer.innerHTML = obj.footer;
 820 
 821         this.wrapper.style.display = 'block';
 822     },
 823     hide: function () {
 824         // Hides the dialog box
 825         this.wrapper.style.display = 'none';
 826     },
 827     state: function () {
 828         // Returns 'visible' or 'hidden'
 829         var value = this.wrapper.style.display;
 830         if (!value) {
 831             if (document.defaultView && document.defaultView.getComputedStyle) {
 832                 var css = document.defaultView.getComputedStyle(this.wrapper, null);
 833                 value = css ? css.display : null;
 834             } else if (this.wrapper.currentStyle) {
 835                 value = this.wrapper.currentStyle.display;
 836             }
 837         }
 838         return value === 'none' ? 'hidden' : 'visible';
 839     }
 840 };
 841 
 842 /*
 843  控制享元数量的管理器。改管理器需要三个部件:一个用来显示对话框的方法,一个用来检查当前网页上正在使用的对话框的数目的方法,以及一个用来保存所生成的对话框的数据结构。我们用一个单体来包装这些部件,以确保管理器的唯一性:
 844  */
 845 // DialogBoxManager singleton
 846 var DialogBoxManager = (function () {
 847     // Stroes created instances
 848     var created = [];
 849 
 850     return {
 851         displayDialogBox: function (obj) {
 852             // Find the number currently in use
 853             var inUse = this.numberInUse();
 854             if (inUse === created.length) {
 855                 // Augment it if need be
 856                 created.push(this.createDialogBox());
 857             }
 858             // show the dialog box
 859             created[inUse].show(obj);
 860         },
 861         createDialogBox: function () {
 862             // Factory method
 863             return new DialogBox();
 864         },
 865         numberInUse: function () {
 866             var inUse = 0;
 867             for (var i = 0, len = created.length; i < len; i++) {
 868                 if (created[i].state() === 'visible') {
 869                     inUse++;
 870                 }
 871             }
 872             return inUse;
 873         }
 874     };
 875 })();
 876 
 877 DialogBoxManager.displayDialogBox({
 878     header: '<h3>title1</h3>',
 879     content: '<div>this is a content</div>',
 880     footer: '<div>this is a footer</div>'
 881 });
 882 
 883 
 884 /*
 885  这个管理器把已经创建出来的对话框对象保存在数组created中,以便于重用。numberInUse方法用于获取现有DialogBox对象中当前正被使用的对象的个数,它通过检查DialogBox对象的状态判断其是否正被使用。displayDialogBox方法会先检查这个数字是否等于数组的长度,并且只有在不能重用现有实例的情况下才创建新实例。
 886 
 887  这个示例比工具提示那个要复杂一点。总结起来就是:通过把外在数据从资源密集型对象剥离以实现对这种对象的重用;用一个管理器控制对象的个数并保存外在数据,所生成的实例的个数应该刚好够用,并且在实例化开销较大的情况下,这些实例应被保存起来供以后重用。这种技术类似于服务端语言中的SQL连接池。在后一种技术中,仅当现有连接都在使用当中时才会创建新连接。
 888  */
 889 
 890 
 891 (function(){
 892 
 893     /**
 894      * 利用享元实现简单的引用计数和垃圾回收 TODO
 895      *
 896      * 实现引用计数的基本思路
 897      * 要实现引用计数,就在享元工厂中定义一个Map,它的key值与缓存享元对象的key是一样的,而value就是被引用的次数,这样当外部每次获取该享元的时候,就把对应的引用计数取出来加上1,然后再记录回去。
 898      *
 899      * 实现垃圾回收的基本思路
 900      * 1)为了确定哪些是垃圾,一个简单的方案,定义一个缓存对象的配置对象,在这个对象中描述了缓存的开始时间和最长不被使用的时间,这个时候判断是否垃圾的计算公式如下:当前的时间 - 缓存的开始时间 >= 最长不被使用的时间。当然,每次这个对象被使用的时候,就把那个缓存开始时间更新为使用时的当前时间,也就是说如果一直有人用的话,这个对象是不会被判断为垃圾。
 901      * 2)何时回收的问题,当然是判断出来是垃圾了就可以回收了。
 902      * 3)怎么回收?直接从缓存的Map对象中删除相应的对象,让这些对象没有引用的地方,那么这些对象就可以等着被回收了。
 903      */
 904 
 905     // 描述享元对象缓存的配置对象
 906     function CacheConfModel(){
 907         // 缓存开始计时的开始时间
 908         this.beginTime = null;
 909         this.durableTime = null;
 910         // 缓存对象需要被永久存储,也就是不需要从缓存中删除
 911         this.forever = false;
 912     }
 913 
 914     var flyweightFactory = {
 915         fsMap: {},
 916         cacheConfMap: {},
 917         countMap: {},
 918         DURABLE_TIME: 6 * 1000,
 919         init: function(){
 920             clearCache();
 921         },
 922         getUserTimes: function(key){
 923             var count = this.countMap[key];
 924             return count || 0;
 925         },
 926         getFlyweight: function(key){
 927             var f = this.fsMap[key], cm;
 928 
 929             if(f == null){
 930                 f = new AuthorizationFlyweight(key);
 931                 this.fsMap[key] = f;
 932                 this.countMap[key] = 1;
 933 
 934                 cm = new CacheConfModel();
 935                 cm.beginTime = Date.now();
 936                 cm.forever = false;
 937                 cm.durableTime = this.DURABLE_TIME;
 938 
 939                 this.cacheConfMap[key] = cm;
 940             } else {
 941                 cm = this.cacheConfMap[key];
 942                 cm.beginTime = Date.now();
 943                 this.cacheConfMap[key] = cm;
 944                 this.countMap[key]++;
 945             }
 946 
 947             return f;
 948         },
 949         removeFlyweight: function(key){
 950             delete this.fsMap[key];
 951             delete this.cacheConfMap[key];
 952             delete this.countMap[key];
 953         }
 954     };
 955 
 956     function UnsharedConcreteFlyweight(){
 957         this.list = [];
 958     }
 959     UnsharedConcreteFlyweight.prototype = {
 960         add: function(flyweight){
 961             this.list.push(flyweight);
 962         },
 963         match: function(securityEntity, permit){
 964             for(var i = 0; i < this.list.length; i++){
 965                 var f = this.list[i];
 966                 if(f.match(securityEntity, permit))
 967                     return true;
 968             }
 969 
 970             return false;
 971         }
 972     };
 973 
 974     function clearCache(){
 975         setInterval(function(){
 976             var tempSet = [];
 977             var set = Object.keys(flyweightFactory.cacheConfMap);
 978 
 979             for(var i = 0; i < set.length; i++){
 980                 var key = set[i];
 981                 var ccm = flyweightFactory.cacheConfMap[key];
 982 
 983                 if(Date.now() - ccm.beginTime >= ccm.durableTime){
 984                     tempSet.push(key);
 985                 }
 986             }
 987 
 988             for(i = 0; i < tempSet.length; i++){
 989                 flyweightFactory.removeFlyweight(key);
 990             }
 991 
 992             console.log('now thread = ' + Object.keys(flyweightFactory.fsMap).length + ', fsMap = ' + Object.keys(flyweightFactory.fsMap));
 993         }, 1000);
 994     }
 995 
 996     var SecurityMgr = {
 997         hasPermit: function(user, securityEntity,
 998              permit){
 999             var col = this.queryByUser(user);
1000 
1001             if(!col || col.length === 0){
1002                 console.log(user + '没有登录或是没有被分配任何权限');
1003                 return false;
1004             }
1005 
1006             for(var i = 0; i < col.length; i++){
1007                 var fm = col[i];
1008                 if(fm.match(securityEntity, permit)){
1009                     return true;
1010                 }
1011             }
1012             return false;
1013         },
1014         queryByUser: function(user){
1015             var col = [];
1016 
1017             for(var i = 0; i < DataBase.length; i++){
1018                 var s = DataBase[i];
1019                 var ss = s.split(',');
1020 
1021                 if(ss[0] === user){
1022                     var fm;
1023 
1024                     if(ss[3] == 2){
1025                         fm = new UnsharedConcreteFlyweight();
1026                         var tempSs = DataBase[ss[1]];
1027 
1028                         for(var prop in tempSs){
1029                             var tempS = tempSs[prop];
1030                             fm.add(flyweightFactory.getFlyweight(tempS));
1031                         }
1032                     } else {
1033                         fm = flyweightFactory.getFlyweight(ss[1] + ',' + ss[2])
1034                     }
1035 
1036                     col.push(fm);
1037                 }
1038             }
1039 
1040             return col;
1041         }
1042     };
1043 
1044     // mock data
1045     var DataBase = {
1046         colDB: [
1047             '张三,人员列表,查看,1',
1048             '李四,人员列表,查看,1',
1049             '李四,操作薪资数据,,2'
1050         ],
1051         mapDB: {
1052             '操作薪资数据': ''
1053         }
1054     };
1055 
1056     for (var i = 0; i < 3; i++) {
1057         DataBase.push('张三' + i + ',人员列表,查看');
1058     }
1059 
1060 
1061     new function(){
1062         var f1 = SecurityMgr.hasPermit('张三', '薪资数据', '查看');
1063         var f2 = SecurityMgr.hasPermit('李四', '薪资数据', '查看');
1064         var f3 = SecurityMgr.hasPermit('李四', '薪资数据', '修改');
1065 
1066         for(var i =0; i < 3; i++){
1067             SecurityMgr.hasPermit('张三' + i, '薪资数据', '查看');
1068         }
1069 
1070         console.log('薪资数据,查看 被引用了' + flyweightFactory.getUserTimes('薪资数据,查看' + ''));
1071         console.log('薪资数据,修改 被引用了' + flyweightFactory.getUserTimes('薪资数据,修改' + ''));
1072         console.log('人员列表,查看 被引用了' + flyweightFactory.getUserTimes('人员列表,查看' + ''));
1073     };
1074 }());
1075 
1076 /**
1077  * 享元模式的适用场合
1078  *
1079  * 1.如果一个应用程序使用了大量的细粒度对象,可以使用享元模式来减少对象数量。
1080  * 2.如果由于使用大量的对象,造成很大的存储开销,可以使用享元模式来减少对象水昂,并节约内存。
1081  * 3.如果对象的大多数状态都可以转变为外部状态,比如通过计算得到,或是从外部传入等,可以使用享元模式来实现内部状态和外部状态的分离。
1082  * 4.如果不考虑对象的外部状态,可以用相对较少的共享对象取代很多组合对象,可以使用享元模式来共享对象,然后组合对象来使用这些共享对象。
1083  */
1084 
1085 /**
1086  * 享元模式之利
1087  *
1088  * 减少对象数量,节省内存空间。
1089  *
1090  * 享元模式之弊
1091  *
1092  * 维护共享对象,需要额外开销。
1093  *
1094  * 你必须在运行效率和可维护性之间进行权衡,然而这种权衡正是工程学的精髓所在。享元模式适合的是系统资源已经用的差不多而且明显需要进行某种优化这样一类场合。这正是其利大于弊的时候。
1095  *
1096  *
1097  * 相关模式
1098  *
1099  * 享元模式与单例模式
1100  * 可以组合使用
1101  * 通常情况下,享元模式中的工厂可以实现成为单例。另外,享元工厂中缓存的享元对象,都是单例的,可以看成是单例模式的一种变形控制。
1102  *
1103  * 享元模式与组合模式
1104  * 可以组合使用
1105  * 在享元模式中,存在不需要共享的享元实现,这些不需要共享的享元通常是对共享的享元对象的组合对象。也就是说,享元模式通常会和组合模式组合使用,来实现更复杂的对象层次结构。
1106  *
1107  * 享元模式与状态模式
1108  * 可以组合使用
1109  * 可以使用享元模式来共享状态模式中的状态对象。通常在状态模式中,会存在数量很大的,细粒度的状态对象,而且它们基本上都是可以重复使用的,都是用来处理某一个固定的状态的,它们需要的数据通常都是由上下文传入,也就是变化部分都分离出去了,所以可以用享元模式来实现这些状态对象。
1110  *
1111  * 享元模式与策略模式
1112  * 可以组合使用
1113  * 同状态模式。
1114  */
1115 </script>
1116 </body>
1117 </html>

 

posted @ 2013-03-24 14:04  LukeLin  阅读(798)  评论(0编辑  收藏  举报