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>