美文网首页
享元模式

享元模式

作者: pws019 | 来源:发表于2017-04-30 13:01 被阅读19次

    享元模式

    享元模式是一个经典的代码优化的解决方案,主要针对重复的,缓慢的,低效的共享数据。它的目的最小化应用中的内存占用,通过与相关对象尽可能多的共享数据。

    在实际中,轻量级的数据共享可以涉及几个相似的对象或者数据结构,被用在大量的对象,并且将这些数据放在一个外部的对象中。我们可以传递这个对象给那些依赖数据的地方,而不是在每隔需要这些数据的地方存储相同的对象。

    使用享元

    这有两种可以应用享元的方式。第一种是数据层,我们根据数据共享的概念处理内存中大量相似的对象。

    第二种是在dom处理层。享元可以被用于一个事件管理总局,在父容器中添加我们希望拥有的相似行为,避免为每个子元素添加事件处理程序。

    数据层是享元模式最常用到的地方,我们将首先看看它。

    享元和数据共享

    在这个应用中,这里有几个关于经典享元模式的概念需要我们注意一下。享元模式中有一个概念即两种状态——内部的和外部的。内部的信息可能被我们对象的内部方法需要,他们绝对不能没有。外部的信息可以删除或存储于外部。

    拥有相同内部数据的对象可以被单个的共享对象替换,通过工厂方法创建。这允许我们减少被隐性存储的数据的数量。

    这样做的好处是,我们能够对已经被实例化的对象保持关注,这样新的副本就只能被创建,因为内在的状态与我们已经拥有的对象不同。

    我们使用一个管理者来处理外在的状态,它被如何实现可能会有变化,但是有一种方法是管理对象对外部的状态包含一个数据库并且享元对象属于其中。

    实现经典的享元模式

    今几年享元模式并未在js中被大量使用。很多的实现我们可以使用Java或者C++世界中得到的灵感完成。

    在下面的实现中我们将利用三种类型的享元组件,如下:

    a.享元相当于一个接口,在外部状态中通过享元可以接受和处理。
    b.具体的享元实际上实现了享元的接口并且存储内部状态。
    c.享元工厂管理享元对象并且创建他们。它确保我们的享元被共享,且管理他们作为一个对象组以便在我们需要个别的实例时候可以查找到。如果一个对象已经在被创建在组中,则直接返回该对象,否则,添加一个新的对象到池中并且返回他。
    

    在我们的实现中符合下面的定义

    CoffeeOrder: Flyweight
    CoffeeFlavor: Concrete Flyweight
    CoffeeOrderContext: Helper
    CoffeeFlavorFactory: Flyweight Factory
    testFlyweight: Utilization of our Flyweights
    

    Duck punching "implements"

    Duck punching 允许我们扩展一个语言或者解决方案的能力,而没必要去修改运行时的代码。下面的解决方案中需要一个Java中的关键字去实现接口,但是在js中并没有提供此关键字,首先来让我们duck punch 它。

    Function.prototype.implementsFor在一个对象的构造函数中工作,它接受一个父类(函数)或者对象并且使用正常继承或者虚拟继承继承这它。

    // Simulate pure virtual inheritance/"implement" keyword for JS
    Function.prototype.implementsFor = function( parentClassOrObject ){
        if ( parentClassOrObject.constructor === Function )
        {
            // Normal Inheritance
            this.prototype = new parentClassOrObject();
            this.prototype.constructor = this;
            this.prototype.parent = parentClassOrObject.prototype;
        }
        else
        {
            // Pure Virtual Inheritance
            this.prototype = parentClassOrObject;
            this.prototype.constructor = this;
            this.prototype.parent = parentClassOrObject;
        }
        return this;
    };
    
    

    我们可以利用这个去修补缺少“implements”关键字通过一个函数显示的继承一个接口。在下面,CoffeeFlavor实现CoffeeOrder接口,并且必须包含接口中的方法,为了我们分配实现的功能到一个对象。

    // Flyweight object
    var CoffeeOrder = {
     
      // Interfaces
      serveCoffee:function(context){},
        getFlavor:function(){}
     
    };
     
     
    // ConcreteFlyweight object that creates ConcreteFlyweight
    // Implements CoffeeOrder
    function CoffeeFlavor( newFlavor ){
     
        var flavor = newFlavor;
     
        // If an interface has been defined for a feature
        // implement the feature
        if( typeof this.getFlavor === "function" ){
          this.getFlavor = function() {
              return flavor;
          };
        }
     
        if( typeof this.serveCoffee === "function" ){
          this.serveCoffee = function( context ) {
            console.log("Serving Coffee flavor "
              + flavor
              + " to table number "
              + context.getTable());
        };
        }
     
    }
     
     
    // Implement interface for CoffeeOrder
    CoffeeFlavor.implementsFor( CoffeeOrder );
     
     
    // Handle table numbers for a coffee order
    function CoffeeOrderContext( tableNumber ) {
       return{
          getTable: function() {
             return tableNumber;
         }
       };
    }
     
     
    function CoffeeFlavorFactory() {
        var flavors = {},
        length = 0;
     
        return {
            getCoffeeFlavor: function (flavorName) {
     
                var flavor = flavors[flavorName];
                if (typeof flavor === "undefined") {
                    flavor = new CoffeeFlavor(flavorName);
                    flavors[flavorName] = flavor;
                    length++;
                }
                return flavor;
            },
     
            getTotalCoffeeFlavorsMade: function () {
                return length;
            }
        };
    }
     
    // Sample usage:
    // testFlyweight()
     
    function testFlyweight(){
     
     
      // The flavors ordered.
      var flavors = new CoffeeFlavor(),
     
      // The tables for the orders.
        tables = new CoffeeOrderContext(),
     
      // Number of orders made
        ordersMade = 0,
     
      // The CoffeeFlavorFactory instance
        flavorFactory;
     
      function takeOrders( flavorIn, table) {
         flavors[ordersMade] = flavorFactory.getCoffeeFlavor( flavorIn );
         tables[ordersMade++] = new CoffeeOrderContext( table );
      }
     
       flavorFactory = new CoffeeFlavorFactory();
     
       takeOrders("Cappuccino", 2);
       takeOrders("Cappuccino", 2);
       takeOrders("Frappe", 1);
       takeOrders("Frappe", 1);
       takeOrders("Xpresso", 1);
       takeOrders("Frappe", 897);
       takeOrders("Cappuccino", 97);
       takeOrders("Cappuccino", 97);
       takeOrders("Frappe", 3);
       takeOrders("Xpresso", 3);
       takeOrders("Cappuccino", 3);
       takeOrders("Xpresso", 96);
       takeOrders("Frappe", 552);
       takeOrders("Cappuccino", 121);
       takeOrders("Xpresso", 121);
     
       for (var i = 0; i < ordersMade; ++i) {
           flavors[i].serveCoffee(tables[i]);
       }
       console.log(" ");
       console.log("total CoffeeFlavor objects made: " + flavorFactory.getTotalCoffeeFlavorsMade());
    }
    

    用享元模式修改代码

    接下来让我们来看一个图书馆的案例。每一本书的元数据可被分解为如下:

    ID
    Title
    Author
    Genre
    Page count
    Publisher ID
    ISBN
    

    我们还需要一个属性来记录成员已经借出了一本书,以及何时借的何时还的。

    checkoutDate
    checkoutMember
    dueReturnDate
    availability
    

    每本书都可以按如下的方法表示,使用享元模式优化之前如下:

    var Book = function( id, title, author, genre, pageCount,publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate,availability ){
     
       this.id = id;
       this.title = title;
       this.author = author;
       this.genre = genre;
       this.pageCount = pageCount;
       this.publisherID = publisherID;
       this.ISBN = ISBN;
       this.checkoutDate = checkoutDate;
       this.checkoutMember = checkoutMember;
       this.dueReturnDate = dueReturnDate;
       this.availability = availability;
     
    };
     
    Book.prototype = {
     
      getTitle: function () {
         return this.title;
      },
     
      getAuthor: function () {
         return this.author;
      },
     
      getISBN: function (){
         return this.ISBN;
      },
     
      // For brevity, other getters are not shown
      updateCheckoutStatus: function( bookID, newStatus, checkoutDate, checkoutMember, newReturnDate ){
     
         this.id = bookID;
         this.availability = newStatus;
         this.checkoutDate = checkoutDate;
         this.checkoutMember = checkoutMember;
         this.dueReturnDate = newReturnDate;
     
      },
     
      extendCheckoutPeriod: function( bookID, newReturnDate ){
     
          this.id = bookID;
          this.dueReturnDate = newReturnDate;
     
      },
     
      isPastDue: function(bookID){
     
         var currentDate = new Date();
         return currentDate.getTime() > Date.parse( this.dueReturnDate );
     
       }
    };
    

    当书籍量很小的时候这种方式可能是有效的,然而,虽然着图书馆的扩张,每本书将包含很多版本和副本。随着时间的流逝我们将发现系统运行的越来越慢。使用成千上万的书籍对象可能导致内存不够用,但我们可以使用享元模式来优化和改进我们的系统。

    现在我们可以把我们的数据分为内部的和外部的规定如下:本书的对象相关的数据(标题、作者等)是内部的而检验数据(checkoutmember,duereturndate等)被认为是外部的。实际上着意味着,一个书籍对象需要书籍每个属性的集合。他仍然是需要很多对象,但是比之前来说已经少多了。

    接下来单个的图书元数据实例组合将被分享在所有的书籍副本中通过特定的标题。

    // Flyweight optimized version
    var Book = function ( title, author, genre, pageCount, publisherID, ISBN ) {
     
        this.title = title;
        this.author = author;
        this.genre = genre;
        this.pageCount = pageCount;
        this.publisherID = publisherID;
        this.ISBN = ISBN;
     
    };
    
    

    正如我们所看到的,外部状态已被删除。与库检查有关的一切都将被移动到一个管理器中,且对象现在已经被分割,可以通过工厂模式实例化他们。

    一个基本的工厂

    现在让我们定义一个非常基础的工厂。我们要做的是检查一个含有特定标题的书在我们的系统之中是否已经被创建。如果已经被创建,则直接返回他,否则,一个新的书籍将被创建并且存储以便之后可以访问他。这确保我们对于每个独特的内在数据块只创建一个单独的副本。

    // Book Factory singleton
    var BookFactory = (function () {
      var existingBooks = {}, existingBook;
     
      return {
        createBook: function ( title, author, genre, pageCount, publisherID, ISBN ) {
     
          // Find out if a particular book meta-data combination has been created before
          // !! or (bang bang) forces a boolean to be returned
          existingBook = existingBooks[ISBN];
          if ( !!existingBook ) {
            return existingBook;
          } else {
     
            // if not, let's create a new instance of the book and store it
            var book = new Book( title, author, genre, pageCount, publisherID, ISBN );
            existingBooks[ISBN] = book;
            return book;
     
          }
        }
      };
     
    })();
    

    管理外部的状态

    接下来,我们需要存储那些从书对象中移除的状态--幸运的是一个管理器(我们将定义为一个单独的)可以用来封装它们。一个书对象和库成员的组合,将它们检查出来将被称为图书记录。我们的管理者将存储并且包括检测相关的我们剥离出来的逻辑,在享元优化我们的图书类期间。

    // BookRecordManager singleton
    var BookRecordManager = (function () {
     
      var bookRecordDatabase = {};
     
      return {
        // add a new book into the library system
        addBookRecord: function ( id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability ) {
     
          var book = bookFactory.createBook( title, author, genre, pageCount, publisherID, ISBN );
     
          bookRecordDatabase[id] = {
            checkoutMember: checkoutMember,
            checkoutDate: checkoutDate,
            dueReturnDate: dueReturnDate,
            availability: availability,
            book: book
          };
        },
        updateCheckoutStatus: function ( bookID, newStatus, checkoutDate, checkoutMember, newReturnDate ) {
     
          var record = bookRecordDatabase[bookID];
          record.availability = newStatus;
          record.checkoutDate = checkoutDate;
          record.checkoutMember = checkoutMember;
          record.dueReturnDate = newReturnDate;
        },
     
        extendCheckoutPeriod: function ( bookID, newReturnDate ) {
          bookRecordDatabase[bookID].dueReturnDate = newReturnDate;
        },
     
        isPastDue: function ( bookID ) {
          var currentDate = new Date();
          return currentDate.getTime() > Date.parse( bookRecordDatabase[bookID].dueReturnDate );
        }
      };
     
    })();
    

    结果是,原来从Book类中提取的数据,被单独的存储在BookManager类的一个属性之中。这种方式相比以前我们使用的大量对象是非常有效的。有关书籍校验的方法在这里也是非常基础的,常用来处理外部的书籍而不是内部的书籍。

    这个过程确实为我们的最终解决方案加入了一点复杂度,然而这相比我们已经解决的性能问题是不值一提的。用数据说的话,如果我们有30个相同的书籍副本,我们现在仅仅需要存储它一次。当然,每个函数都会占用内存。当我们使用享元模式的时候,这些方法仅存在一个地方(管理对象中)而不是存在于每个对象中,因此节省了内存的使用。关于以上提到的未优化的享元版本,我们仅仅是将链接到对函数对象的方法存储到了它的构造函数的原型上,但是换一种实现方式,方法将被每个对象实例所创建。

    享元模式与DOM

    DOM(文档对象模型)允许对象使用两种方式触发事件——事件冒泡或者事件捕获(向上传播或者向下传播)。

    在事件捕获中,事件首先被最外层的元素捕获并逐渐的向内层传播。在事件冒泡中,事件由内层元素向外层传播。

    在这种上下文之中最好的描述享元的隐喻,是由 Gary Chisholm 撰写的,并且有点像这些:

    把享元想象成一个池塘,一个鱼张开它的嘴(此处类比事件发生),气泡上升到水面(冒泡),当气泡上升到水面时(行为),一个苍蝇飞走了。在这个例子中,我们可以轻易的移动这个鱼,让它张开嘴到一个被点击的按钮上,这个气泡开始逐渐飘起,苍蝇最终飞离到被运行的函数上。

    冒泡被用来处理一种场景,即单个的事件可能需要被多个定义在DOM树中不同层级的处理程序处理。当它发生时候,事件冒泡首先从离它指定的最近的层级元素开始执行,从那里开始,事件开始传播到它的包含元素中,直到最高的一层。

    享元可以调整事件冒泡的深入,这将在稍后被我们所看到。

    案例1:集中事件处理

    对于我们实际的第一个案例,想象我们有一堆类似的元素在我们的文档中,并且含有相似的执行行为,当我们的用户行为(例如鼠标点击或者移入)被执行时。

    通常我们构造我们的手风琴组件时,菜单或者其他基于列表的控件中的每个项目都被绑定一个点击事件,被包含在他们的父容器中。我们可以轻易的附加一个享元在我们的顶级容器中以监听来自内部元素传来的事件,而不是为多个字元素绑定点击事件。然后这些逻辑可以被简单的或者根据需要复杂的处理。

    由于所提到的组件每个部分大都有相同的标记。这里有一个很好的机会,每个可能被点击的元素都是十分相似的并且其周围有着相似的样式类。我们将在下面利用这些信息通过享元构建一个基本的手风琴。

    一个StateManager命名空间用来封装我们的享元逻辑,同时jQuery被用来对容易进行事件绑定初始化。为了确保这个页面没有其他的类似处理容器的绑定逻辑,一个解除绑定的事件首先被应用。

    为了准确的确定在容器中的子元素是如何被点击的,我们利用目标检查法,即提供的一个被元素被点击元素的引用,而不管它的父元素。我们使用这些信息处理点击事件,而不是为页面上每一个指定的子元素绑定事件。

    <div id="container">
       <div class="toggle" href="#">More Info (Address)
           <span class="info">
               This is more information
           </span></div>
       <div class="toggle" href="#">Even More Info (Map)
           <span class="info">
              <iframe src="http://www.map-generator.net/extmap.php?name=London&amp;address=london%2C%20england&amp;width=500...gt;"</iframe>
           </span>
       </div>
    </div>
    
    var stateManager = {
     
      fly: function () {
     
        var self = this;
     
        $( "#container" )
              .unbind()
              .on( "click", "div.toggle", function ( e ) {
                self.handleClick( e.target );
              });
      },
     
      handleClick: function ( elem ) {
        elem.find( "span" ).toggle( "slow" );
      }
    };
    

    这里的好处是,我们把许多独立的行动变成一个共享的行动(可能节省内存)。

    例2:使用享元模式进行性能优化

    在我们的第二个案例中,我们将涉及一些更深层次的性能优化,可以通过使用享元配合jQuery完成。

    James Padolsey曾写过一篇文章叫76字节更快的jQuery,他提醒我们,每一次的jQuery触发回调,不管类型(过滤器,每个事件处理程序),我们能够访问函数的上下文(相关的DOM元素)通过this关键词。

    不幸的是,我们中的许多人已经习惯于包装this通过$()或jquery(),这意味着每次都会构建一个新的不必要实例,而不是仅仅是表面上做的这么简单:

    $("div").on( "click", function () {
      console.log( "You clicked: " + $( this ).attr( "id" ));
    });
     
    // we should avoid using the DOM element to create a
    // jQuery object (with the overhead that comes with it)
    // and just use the DOM element itself like this:
     
    $( "div" ).on( "click", function () {
      console.log( "You clicked:"  + this.id );
    });
    

    杰姆斯曾想在以下情况下使用jQuery的jquery.text,然而他不同意的是,一个新的jquery对象必须在每次迭代中创建:

    $( "a" ).map( function () {
      return $( this ).text();
    });
    

    jQuery的工具方法在某些方面封装是多余的,使用jQuery.methodName就好过使用jQuery.fn.methodName(例如jQuery.text与jQuery.fn.text) ,此处methodName代表一个工具方法,例如each()或者text。这避免调用更高层次抽象方法的需要,或者每次调用方法的时候构造一个新的jQuery对象,就像jQuery.methodName是类库在使用一个更低层次的方法jQuery.fn.methodName的力量。

    然而不是所有的jQuery方法都有相应的单节点功能,Padolsey构想并设计了一个工具方法jquery.single。

    这里的想法是一个jQuery对象的创建和使用通过每次调用jquery.single(有效的意思是有史以来只有一个jQuery对象)。该方法的实现可以在下面找到和我们合并多个可能的对象数据到一个更重要的奇异结构,这在技术上也是一个享元。

    jQuery.single = (function( o ){
     
       var collection = jQuery([1]);
       return function( element ) {
     
           // Give collection the element:
           collection[0] = element;
     
            // Return the collection:
           return collection;
     
       };
    })();
    

    一个与实际使用相关的是:

    $( "div" ).on( "click", function () {
     
       var html = jQuery.single( this ).next().html();
       console.log( html );
     
    });
    

    备注:虽然我们可能相信简单的缓存我们的jq代码就可以获得相等的性能提升,Padolsey声称 $.single()仍然是值得使用的并且可以获得更好的性能。这不是说,根本不要使用缓存,而是留意这种方法是有用的。

    相关文章

      网友评论

          本文标题:享元模式

          本文链接:https://www.haomeiwen.com/subject/iuqxtxtx.html