The Flyweight Pattern in Javascript

     Flyweight Pattern,中文可译作享元模式。它的核心是分离对象的:内在属性和外部属性,然后共享内在属性,组装外在属性。看一个汽车的例子: 

/*  Car class, un-optimized.  */
var Car =  function (make, model, year, owner, tag, renewDate) {
     this.make = make;
     this.model = model;
     this.year = year;
     this.owner = owner;
     this.tag = tag;
     this.renewDate = renewDate;
};
Car.prototype = {
    getMake:  function () {
         return  this.make;
    },
    getModel:  function () {
         return  this.model;
    },
    getYear:  function () {
         return  this.year;
    },
    transferOwnership:  function (newOwner, newTag, newRenewDate) {
         this.owner = newOwner;
         this.tag = newTag;
         this.renewDate = newRenewDate;
    },
    renewRegistration:  function (newRenewDate) {
         this.renewDate = newRenewDate;
    },
    isRegistrationCurrent:  function () {
         var today =  new Date();
         return today.getTime() < Date.parse( this.renewDate);
    }
};

很OOP,但是当需要很多Car的实例时,浏览器可能就慢了。 分析Car的属性,前3者终生不变,并且可以大量复制,作为内在属性。改进如下:

/*  Car class, optimized as a flyweight.  */
var Car =  function (make, model, year) {
     this.make = make;
     this.model = model;
     this.year = year;
};
Car.prototype = {
    getMake:  function () {
         return  this.make;
    },
    getModel:  function () {
         return  this.model;
    },
    getYear:  function () {
         return  this.year;
    }
};

/*  CarFactory singleton.  */
var CarFactory = ( function () {
     var createdCars = {};
     return {
        createCar:  function (make, model, year) {
             if (createdCars[make + '-' + model + '-' + year]) {
                 return createdCars[make + '-' + model + '-' + year];
            } else {
                 var car =  new Car(make, model, year);
                createdCars[make + '-' + model + '-' + year] = car;
                 return car;
            }
        }
    };
})();

/*  CarRecordManager singleton.  */
var CarRecordManager = ( function () {
     var carRecordDatabase = {};
     return {
        addCarRecord:  function (make, model, year, owner, tag, renewDate) {
             var car = CarFactory.createCar(make, model, year);
            carRecordDatabase[tag] = {
                owner: owner,
                renewDate: renewDate,
                car: car
            };
             return carRecordDatabase[tag];
        },
        getCar:  function (tag) {
             return carRecordDatabase[tag];
        },
        transferOwnership:  function (tag, newOwner, newTag, newRenewDate) {
             var record =  this.getCar(tag);
            record.owner = newOwner;
            record.tag = newTag;
            record.renewDate = newRenewDate;
        },
        renewRegistration:  function (tag, newRenewDate) {
             this.getCar(tag).renewDate = newRenewDate;
        },
        isRegistrationCurrent:  function (tag) {
             var today =  new Date();
             return today.getTime() < Date.parse( this.getCar(tag).renewDate);
        }
    };
})();


//  test
( function () {
     var car = CarRecordManager.addCarRecord("test make", "test model", "2011", "Ray", "JB001", "2012-09-29");
     var car2 = CarRecordManager.addCarRecord("test make", "test model", "2011", "Tina", "JB002", "2011-08-27");
     var car1 = CarRecordManager.getCar("JB001");
    console.log(car == car1);                //  true
    console.log(car1.car == car2.car);       //  true
    console.log(car2.owner);                 //  tina
})();   

可以看到,即便需要很多的car,有CarFactory的控制,每个make + model + year的组合的Car实例实质上分别只会存在一个。 而对于Car实例的全部操作均通过CarRecordManager来实现,它通吃汽车的内外在全部属性。 


     基于js的灵活性,将Car类的声明迁移到CarFactory类中,将CarFactory类声明迁移到CarRecordManager中: 

/*  CarRecordManager singleton.  */
var CarRecordManager = ( function () {
     var carRecordDatabase = {};

     /*  CarFactory singleton.  */
     var CarFactory = ( function () {
         var createdCars = {};

         /*  Car class, optimized as a flyweight.  */
         var Car =  function (make, model, year) {
             this.make = make;
             this.model = model;
             this.year = year;
        };
        Car.prototype = {
            getMake:  function () {
                 return  this.make;
            },
            getModel:  function () {
                 return  this.model;
            },
            getYear:  function () {
                 return  this.year;
            }
        };

         return {
            createCar:  function (make, model, year) {
                 if (createdCars[make + '-' + model + '-' + year]) {
                     return createdCars[make + '-' + model + '-' + year];
                }  else {
                     var car =  new Car(make, model, year);
                    createdCars[make + '-' + model + '-' + year] = car;
                     return car;
                }
            }
        };
    })();

     return {
        addCarRecord:  function (make, model, year, owner, tag, renewDate) {
             var car = CarFactory.createCar(make, model, year);
            carRecordDatabase[tag] = {
                owner: owner,
                renewDate: renewDate,
                car: car
            };
             return carRecordDatabase[tag];
        },
        getCar:  function (tag) {
             return carRecordDatabase[tag];
        },
        transferOwnership:  function (tag, newOwner, newTag, newRenewDate) {
             var record =  this.getCar(tag);
            record.owner = newOwner;
            record.tag = newTag;
            record.renewDate = newRenewDate;
        },
        renewRegistration:  function (tag, newRenewDate) {
             this.getCar(tag).renewDate = newRenewDate;
        },
        isRegistrationCurrent:  function (tag) {
             var today =  new Date();
             return today.getTime() < Date.parse( this.getCar(tag).renewDate);
        }
    };
})();


//  test
( function () {
     var car = CarRecordManager.addCarRecord("test make", "test model", "2011", "Ray", "JB001", "2012-09-29");
     var car2 = CarRecordManager.addCarRecord("test make", "test model", "2011", "Tina", "JB002", "2011-08-27");
     var car1 = CarRecordManager.getCar("JB001");
    console.log(car == car1);                //  true
    console.log(car1.car == car2.car);       //  true
    console.log(car2.owner);                 //  tina

     // var cc = new Car("test make", "test model", "2012");  //error
})();   

这时候,还是Flyweight,但是封装性更好了。 Car的用户看不到CarFactory,更看不到Car。即便你如倒数第二行那样声明var car = new Car(...),得到的也唯有error。同时,它完全不影响原有的正常使用。

 

    关于Flyweight,它的内在属性比较好管理,因为实例对象比较少。而外在属性怎么管理? Car的Flyweight实现,是将它们存储在一个内部的类数组对象:carRecordDataBase中。 发散一下,想到没有,一棵树,它利用Composite Pattern也可以完美的存储很多对象实例。 我们用它来实现一下Flyweight。看如下Calendar的例子: 

/*  CalendarItem interface.  */
var CalendarItem =  new Interface('CalendarItem', ['display']);

/*  CalendarYear class, a composite.  */
var CalendarYear =  function (year, parent) {
     this.year = year;
     this.element = document.createElement('div');
     this.element.style.display = 'none';
    parent.appendChild( this.element);
     function isLeapYear(y) {
         return (y > 0) && !(y % 4) && ((y % 100) || !(y % 400));
    }
     this.months = [];
     this.numDays = [31, isLeapYear( this.year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
     for ( var i = 0, len = 12; i < len; i++) {
         this.months[i] =  new CalendarMonth(i,  this.numDays[i],  this.element);
    }
};
CalendarYear.prototype = {
    display:  function () {
         for ( var i = 0, len =  this.months.length; i < len; i++) {
             this.months[i].display(); 
        }
         this.element.style.display = 'block';
    }
};

/*  CalendarMonth class, a composite.  */
var CalendarMonth =  function (monthNum, numDays, parent) {
     this.monthNum = monthNum;
     this.element = document.createElement('div');
     this.element.style.display = 'none';
    parent.appendChild( this.element);
     this.days = [];
     for ( var i = 0, len = numDays; i < len; i++) {
         this.days[i] =  new CalendarDay(i,  this.element);
    }
};
CalendarMonth.prototype = {
    display:  function () {
         for ( var i = 0, len =  this.days.length; i < len; i++) {
             this.days[i].display();
        }
         this.element.style.display = 'block';
    }
};

/*  CalendarDay class, a leaf.  */
var CalendarDay =  function (date, parent) {
     this.date = date;
     this.element = document.createElement('div');
     this.element.style.display = 'none';
    parent.appendChild( this.element);
};
CalendarDay.prototype = {
    display:  function () {
         this.element.style.display = 'block';
         this.element.innerHTML =  this.date;
    }
};

CalendarYear、CalendarMonth、CalendarDay均实现了CalendarItem接口,CalendarMonth中引用和组装CalendarDay,CalendarYear引用和组装CalendarMonth。初看起来很不多,但是想一想一年得有365天,我连续加载个10年,页面估计就得挂了。 

 

    利用Singleton Pattern将CalendarDay一步到位改进如下: 

/*  CalendarDay class, a leaf.  */
var CalendarDay = ( function () {
     var innerDay =  null;
     var InnerCalendarDay =  function () { };
    InnerCalendarDay.prototype = {
        display:  function (date, parent) {
             var element = document.createElement('div');
            element.innerHTML = date;
            element.style.display = "block";
            parent.appendChild(element);
        }
    };

     return {
        getInstance:  function () {
             if (innerDay ==  null) {
                innerDay =  new InnerCalendarDay();
            }

             return innerDay;
        }
    };
})();

可以看到,我们将它的date和parent作为外在属性,在调用display方法时通过参数传入。这时候CalendarDay内部实例仅有1份。基于这个变化,修改CalendarMonth如下: 

/*  CalendarMonth class, a composite.  */
var CalendarMonth =  function (monthNum, numDays, parent) {
     this.monthNum = monthNum;
     this.element = document.createElement('div');
     this.element.style.display = 'none';
    parent.appendChild( this.element);
     this.days = [];
     for ( var i = 0, len = numDays; i < len; i++) {
         this .days[i] = CalendarDay.getInstance();
    }
};
CalendarMonth.prototype = {
    display:  function () {
         for ( var i = 0, len =  this.days.length; i < len; i++) {
             this .days[i].display(i,  this .element);
        }
         this.element.style.display = 'block';
    }
};

至此,CalendarDay级别已经优化。 你可以参考这个思路继续。 另外,创建的那些DOM元素其实也可以根据需要来进行共享。 这里暂不进行。

 

    这里再列举Tooltip的例子。一般性的代码:


var Tooltip =  function(targetElement, text) {
     this.target = targetElement;
     this.text = text;
     this.delayTimeout =  null;
     this.delay = 1500;

     this.element = document.createElement('div');
     this.element.style.display = 'none';
     this.element.style.position = 'absolute';
     this.element.className = 'tooltip';
    document.getElementsByTagName('body')[0].appendChild( this.element);
     this.element.innerHTML =  this.text;

     var that =  this
    addEvent( this.target, 'mouseover',  function(e) { that.startDelay(e); });
    addEvent( this.target, 'mouseout',  function(e) { that.hide(); });
};
Tooltip.prototype = {
    startDelay:  function (e) {
         if ( this.delayTimeout ==  null) {
             var that =  this;
             var x = e.clientX;
             var y = e.clientY;
             this.delayTimeout = setTimeout( function () {
                that.show(x, y);
            },  this.delay);
        }
    },
    show:  function (x, y) {
        clearTimeout( this.delayTimeout);
         this.delayTimeout =  null;
         this.element.style.left = x + 'px';
         this.element.style.top = (y + 20) + 'px';
         this.element.style.display = 'block';
    },
    hide:  function () {
        clearTimeout( this.delayTimeout);
         this.delayTimeout =  null;
         this.element.style.display = 'none';
    }
};

// test
var linkElement = $('link-id');
var tt =  new Tooltip(linkElement, 'Lorem ipsum...');

 同样可能会导致N对类似对象被大量创建。改进如下: 

/*  TooltipManager singleton, a flyweight factory and manager.  */
var TooltipManager = ( function () {
     var storedInstance =  null;

     /*  Tooltip class, as a flyweight.  */
     var Tooltip =  function () {
         this.delayTimeout =  null;
         this.delay = 1500;
         this.element = document.createElement('div');
         this.element.style.display = 'none';
         this.element.style.position = 'absolute';
         this.element.className = 'tooltip';
        document.getElementsByTagName('body')[0].appendChild( this.element);
    };
    Tooltip.prototype = {
        startDelay:  function (e, text) {
             if ( this.delayTimeout ==  null) {
                 var that =  this;
                 var x = e.clientX;
                 var y = e.clientY;
                 this.delayTimeout = setTimeout( function () {
                    that.show(x, y, text);
                },  this.delay);
            }
        },
        show:  function (x, y, text) {
            clearTimeout( this.delayTimeout);
             this.delayTimeout =  null;
             this.element.innerHTML = text;
             this.element.style.left = x + 'px';
             this.element.style.top = (y + 20) + 'px';
             this.element.style.display = 'block';
        },
        hide:  function () {
            clearTimeout( this.delayTimeout);
             this.delayTimeout =  null;
             this.element.style.display = 'none';
        }
    };

     return {
        addTooltip:  function (targetElement, text) {
             //  Get the tooltip object.
             var tt =  this.getTooltip();
             //  Attach the events.
            addEvent(targetElement, 'mouseover',  function (e) { tt.startDelay(e, text); });
            addEvent(targetElement, 'mouseout',  function (e) { tt.hide(); });
        },
        getTooltip:  function () {
             if (storedInstance ==  null) {
                storedInstance =  new Tooltip();
            }
             return storedInstance;
        }
    };
})();

/*  Tooltip usage.  */
TooltipManager.addTooltip($('link-id'), 'Lorem ipsum...');

全部源码download

 

 

 

 

 

 

 

 

 

 

 

 

 

 

      

 

你可能感兴趣的:(JavaScript)