Structural design mode

Structural patterns encapsulate combinatorial relationships between objects and describe how classes or objects can be arranged into a larger structure. Common patterns include Proxy, Composite, Flyweight, Decorator, Adapter

1. Proxy Mode

The proxy pattern provides a proxy or placeholder for an object to control access to it.

There are many real-life scenarios that use the proxy pattern. For example, to invite a star to a business performance, this time you need to contact his agent; After you negotiate with the agent, the agent will give the contract to the star.

The key to the proxy pattern is to provide a proxy object to control access to an object when it is inconvenient or not necessary for the client to directly access it. After the proxy object does some processing of the request, it passes the request to the ontology object.

The types of proxy mainly include protected proxy and virtual proxy. A protected proxy is used to control access to a target object by objects with different permissions, whereas a virtual proxy delays the creation of expensive objects until they are really needed. More commonly used in Javascript are virtual proxies.

For example, in web development, image loading is a common problem. Because the image is too large or the network is poor, the image is often not loaded and displayed as a blank. A common practice is to load the image asynchronously with a loading image, and then fill the image into the IMG node after the image is loaded. This scenario is suitable for using a virtual agent.

    var myImage = (function () {
      var imgNode = document.createElement('img');
      document.body.appendChild(imgNode);
      return {
        setSrc: function (src) { imgNode.src = src; }}}) ();var proxyImage = (function () {
      var img = new Image;
      img.onload = function () {
        myImage.setSrc(this.src);
      }
      return {
        setSrc: function (src) {
          myImage.setSrc('./loading.gif'); img.src = src; }}}) (); proxyImage.setSrc('http://xxxxx.jpg');
Copy the code

2. Composite

The composition pattern groups objects into a tree structure to represent a partial-whole hierarchy. In addition to being used to represent tree structures, another benefit of the composite pattern is that it enables consistent use of individual and composite objects through the polymorphic representation of objects.

The relationship between folders and files is best described in the composite pattern. Folders can contain files as well as other folders, eventually forming a tree. We use the composite mode to simulate the process of Folder scanning. First, we define Folder and File classes respectively.

    var Folder = function (name) {
      this.name = name;
      this.files = [];
    };
    Folder.prototype.add = function (file) {
      this.files.push(file);
    };
    Folder.prototype.scan = function () {
      console.log('Start scanning folders:' + this.name);
      for (var i = 0, file, files = this.files; file = files[i++];) { file.scan(); }};var File = function (name) {
      this.name = name;
    };
    File.prototype.add = function () {
      throw new Error('File cannot be added under file');
    };
    File.prototype.scan = function () {
      console.log('Start scanning files:' + this.name);
    };
Copy the code

Next, create some folders and file objects and combine them into a tree.

  var folder = new Folder('Learning Materials');
  var folder1 = new Folder('JavaScript');
  var folder2 = new Folder('jQuery');
  var file1 = new File('JavaScript Design Patterns and Development Practices');
  var file2 = new File('master of jQuery');
  var file3 = new File('Refactoring and Patterns')
  folder1.add(file1);
  folder2.add(file2);
  folder.add(folder1);
  folder.add(folder2);
  folder.add(file3);
Copy the code

In this example, we once again see how the client treats composite objects and leaf objects equally. In the process of adding a batch of files, the customer does not have to distinguish whether they are files or folders. Newly added files and folders can easily be added to the original tree structure to work with existing objects in the tree.

After using composite mode, scanning the entire folder is also easy, we only need to operate on the top of the tree object.

  folder.scan();
Copy the code

3. Flyweight mode

Flyweight mode is a mode used for performance optimization. “fly” stands for “fly”, meaning flyweight. The core of the share pattern is the use of sharing technology to effectively support a large number of fine-grained objects.

The share pattern is useful if the system is overloaded with memory due to the creation of a large number of similar objects. In JavaScript, browsers, especially mobile browsers, don’t allocate a lot of memory, so how to save memory becomes a very meaningful thing.

Imagine an underwear factory that currently produces 50 types of men’s underwear and 50 types of women’s underwear. In order to promote their products, the factory decides to produce plastic models to wear their underwear for advertising photos. Normally 50 male models and 50 female models would be photographed wearing separate underwear. Instead of using the free mode, a program might say something like this:

    var Model = function (sex, underwear) {
      this.sex = sex;
      this.underwear = underwear;
    };
    Model.prototype.takePhoto = function () {
      console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
    };
    for (var i = 1; i <= 50; i++) {
      var maleModel = new Model('male'.'underwear' + i);
      maleModel.takePhoto();
    };
    for (var j = 1; j <= 50; j++) {
      var femaleModel = new Model('female'.'underwear' + j);
      femaleModel.takePhoto();
    };
Copy the code

To get a photo, sex and underwear parameters are passed in each time, and as mentioned above, there are now 50 types of men’s underwear and 50 types of women’s underwear, so 100 objects will be created. If 10,000 types of underwear are produced in the future, the program will probably crash prematurely because there are so many objects.

Let’s consider how to optimize this scenario. Although there are 100 types of lingerie, it obviously doesn’t need 50 male models and 50 female models. In fact, one male model and one female model is enough, and they can wear different underwear for the photo shoot.

    var Model = function (sex) {
      this.sex = sex;
    };
    Model.prototype.takePhoto = function () {
      console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
    };

    var maleModel = new Model('male'),
      femaleModel = new Model('female');

    for (var i = 1; i <= 50; i++) {
      maleModel.underwear = 'underwear' + i;
      maleModel.takePhoto();
    };

    for (var j = 1; j <= 50; j++) {
      femaleModel.underwear = 'underwear' + j;
      femaleModel.takePhoto();
    };
Copy the code

The share pattern requires that the attributes of an object be divided into internal and external states. The goal of the share pattern is to minimize the number of shared objects, and the following lessons provide some guidance on how to separate internal state from external state.

  • Internal state is stored inside an object.
  • Internal state can be shared by several objects.
  • The internal state is independent of the specific scenario and usually does not change.
  • The external state depends on and changes according to the specific scenario. The external state cannot be shared.

In this way, we can designate all objects with the same internal state as the same shared object. The external state can be stripped from the object and stored externally.

4. Decorator pattern

Many times in program development, you don’t want a class to be inherently large and have many responsibilities at once. Then we can use the decorator pattern. The decorator pattern can dynamically add additional responsibilities to an object without affecting other objects derived from that class.

Let’s say we’re writing a game in which we’re fighting planes. As experience increases, the planes we’re flying can be upgraded to more powerful planes. At first, these planes can fire normal bullets, at level 2 they can fire missiles, and at level 3 they can fire atomic bombs.

Let’s look at the code implementation.

    var Plane = function () { }
    Plane.prototype.fire = function () {
      console.log('Fire ordinary bullets');
    }
    var MissileDecorator = function (plane) {
      this.plane = plane;
    }
    MissileDecorator.prototype.fire = function () {
      this.plane.fire();
      console.log('Launch a missile');
    }
    var AtomDecorator = function (plane) {
      this.plane = plane;
    }
    AtomDecorator.prototype.fire = function () {
      this.plane.fire();
      console.log('Launch an atomic bomb');
    }

    var plane = new Plane();
    plane = new MissileDecorator(plane);
    plane = new AtomDecorator(plane);
    plane.fire();
    // Separate output: firing ordinary bullets, firing missiles, firing atomic bombs
Copy the code

This way of dynamically adding responsibility to an object does not really change the object itself, but puts the object into another object, which is referred to in a chain, forming an aggregate object. These objects all have the same interface (fire method), and when a request reaches an object in the chain, that object performs its own operation and then forwards the request to the next object in the chain.

The JavaScript language makes it fairly easy to change objects on the fly. You can rewrite an object or a method of an object directly without using a “class” to implement the decorator pattern.

    var plane = {
      fire: function () {
        console.log('Fire ordinary bullets'); }}var missileDecorator = function () {
      console.log('Launch a missile');
    }
    var atomDecorator = function () {
      console.log('Launch an atomic bomb');
    }
    var fire1 = plane.fire;
    plane.fire = function () {
      fire1();
      missileDecorator();
    }
    var fire2 = plane.fire;
    plane.fire = function () {
      fire2();
      atomDecorator();
    }
    plane.fire();
  // Separate output: firing ordinary bullets, firing missiles, firing atomic bombs
Copy the code

It is possible to rewrite a function by saving the original reference. Such code is certainly in line with the open and closed principle. It is true that we did not modify the original code when adding new functions, but this method has the following two problems.

  • Intermediate variables must be maintained.
  • This has been hijacked.

This can be done with AOP:

    Function.prototype.before = function (beforefn) {
      var __self = this; // Save a reference to the original function
      return function () { // Returns a "proxy" function that contains both the original function and the new function
        beforefn.apply(this.arguments); // Execute the new function and ensure that this is not hijacked
        // The new function is executed before the original function
        return __self.apply(this.arguments); // Execute the original function and return the result of the original function.
        // And make sure this is not hijacked}}Function.prototype.after = function (afterfn) {
      var __self = this;
      return function () {
        var ret = __self.apply(this.arguments);
        afterfn.apply(this.arguments);
        returnret; }};var fire = function () {
      console.log('Fire ordinary bullets');
    }

    var missileDecorator = function () {
      console.log('Launch a missile');
    }
    var atomDecorator = function () {
      console.log('Launch an atomic bomb');
    }
    var fire = fire.after(missileDecorator).after(atomDecorator);
    fire();
Copy the code

5. Adapter Mode

The role of the adapter pattern is to solve the problem of interface incompatibilities between two software entities. Using the adapter pattern, two software entities that would otherwise not work due to incompatible interfaces can work together.

Suppose we want to use googleMap and baiduMap to present the map on the page in their own way.

    var googleMap = {
      show: function () {
        console.log('Start rendering Google Maps'); }};var baiduMap = {
      display: function () {
        console.log('Start rendering baidu Map'); }};var renderMap = function (map) {
      if (map.show instanceof Function) { map.show(); }};Copy the code

The renderMap function cannot be called directly because the two maps are presented differently, so we will provide an adapter for the Baidu map. This allows us to render two maps at once.

    var baiduMapAdapter = {
      show: function () {
        returnbaiduMap.display(); }}; renderMap(googleMap);// Output: Start rendering Google Maps
    renderMap(baiduMapAdapter); // Output: Start rendering Baidu map
Copy the code

The resources

JavaScript Design Patterns and Development Practices