23 Structural Through J Query Look at the Structural Design Pattern

23 Structural Through jQuery Look at the Structural Design Pattern #

Hello, I’m Ishikawa.

Today, I will take you through a few more classic structural design patterns introduced in the book “Design Patterns: Elements of Reusable Object-Oriented Software” by the Gang of Four (GoF). We will use jQuery to examine these design patterns. You might wonder what value jQuery has as it has received a lot of criticism. However, I believe that users vote with their feet. Despite the criticism, many people still use it, which proves that its advantages outweigh its drawbacks. In fact, many of its features that people can’t get enough of are backed by structural design patterns. Let’s dive into them together today.

Several classic structural patterns #

Let’s first take a look at several classic structural patterns: Flyweight, Facade, and Composite.

Flyweight pattern #

The core idea of the Flyweight pattern is to save memory by reducing the number of object creations.

The Flyweight pattern was first proposed by Paul Calder and Mark Linton in 1990. Friends who like watching boxing may know that “flyweight” is a weight class in boxing called “flyweight”, which refers to fighters weighing less than 112 pounds. Let’s take a look at a UFC match, where a heavyweight and a flyweight are compared, which gives us a more intuitive understanding. Therefore, as the name suggests, the purpose of this pattern is to help us achieve lightweight memory usage.

Image

So how does it reduce the number of object creations to save memory? The implementation here mainly optimizes the code that repeats, slowly, and inefficiently shares data, and shares as much data as possible with related objects (such as application configurations and states) to minimize memory usage in the application. In the Flyweight pattern, there are three core concepts: Flyweight factory, interface, and concrete flyweight. The relationship between them is shown in the following diagram.

Image

In the Flyweight pattern, there are two types of data: intrinsic data and extrinsic data. Intrinsic data is stored and called within the object, while external information is stored in external deletable data. Objects with the same intrinsic data can be created by the factory method as a single shared object.

The flyweight interface can receive and act on external states. The concrete flyweight is the actual implementation of the interface, which is responsible for storing shareable intrinsic states and controlling external states. The flyweight factory is responsible for managing and creating flyweight objects. When a client requests a flyweight object, if the object already exists, it returns the object; if it does not yet exist, it creates a related object.

Let’s explain this with an example of renting cars. In a car rental agency, there may be several cars of the same model. The model, maker, and VIN (Vehicle Identification Number) of these cars are the same, which can be considered as intrinsic data and stored through a single shared object called “cars”. However, the rental status and rental price of each car are different, which can be considered as extrinsic data. When a new car is added through the “addCar” function, the “createCar” function can determine whether to create a new car model or return an existing car model based on the VIN. Therefore, although we create 5 car records below, there are only 3 instances of car models.

// Store independent objects for car models
class Car {
  constructor(model, maker, vin) {
    this.model = model;
    this.maker = maker;
    this.vin = vin;
  }
}

// Store containers of concrete car models
var cars = new Map();

// If the car model is known, return the VIN; otherwise, create a new car model
var createCar = (model, maker, vin) => {
  var existingCar = cars.has(vin);

  if (existingCar) {
    return cars.get(vin);
  }

  var car = new Car(model, maker, vin);
  cars.set(vin, car);

  return car;
};

// Store containers for rented cars
var carList = [];

// Register a rented car to the list
var addCar = (model, maker, vin, availability, sales) => {
  var car = {
    ...createCar(model, maker, vin),
    sales,
    availability,
    vin
  };

  carList.push(car);
  return car;
};

addCar("911", "Porsche", "FR345", true, 2300);
addCar("911", "Porsche", "FR345", false, 2400);
addCar("Togun", "VW", "AZ567", false, 800);
addCar("C-Class", "Mercedes-Benz", "AS356", false, 1200);
addCar("C-Class", "Mercedes-Benz", "AS356", true, 1100);

With the development of hardware, today’s RAM memory sizes are mostly in the GB level, so the Flyweight pattern is no longer as important as before. However, if a huge number of objects need to be created, even tiny differences may become significant in the scaling process, so we still need to pay attention to this design pattern.

The example we mentioned above is from the perspective of data in the Flyweight pattern. Next, let’s also look at the application of the Flyweight pattern from another perspective, event handling. Because the Document Object Model (DOM) is nested, a single event, such as a click event, may be handled by multiple event handlers in different DOM levels. The DOM supports two methods of object event listening: event capturing and event bubbling. In event capturing, the event is first captured by the outermost element and propagates to the innermost element. In event bubbling, the event is captured and provided to the innermost element, and then propagates to the outer elements.

Image

In the scenario that utilizes event bubbling, based on the fact that it is a bottom-up event bubbling, the event handler at the bottom layer of elements executes. For example, suppose we have many similar elements in a document, and when users perform operations on them, such as clicking or hovering over them, these elements will react similarly. Usually, when we construct menus or other list-based widgets, we bind the click event to each link element in the container, such as $('ul li a').on(...). But what if we use the Flyweight pattern? We can add a flyweight to the outer container of the elements, and listen to events from below in a top-down manner, and then use the corresponding logic to handle these events as needed. Instead of binding the click to multiple elements like bubbling does.

The following example uses the Flyweight pattern to build a very basic accordion. Here, jQuery is used to bind the initial click to the container div, converting many independent behaviors into shared behaviors.

var stateManager = {
  // ...
};
flyweight() {
    var self = this;
    $('#container')
        .unbind()
        .on('click', 'div.toggle', ({
            target
        }) => {
            self.handleClick(target);
        });
}
};

James Padolsey from Facebook has proposed another concept of using flyweight in jQuery. He suggests that when using some utility methods in jQuery, it is better to use the internal jQuery.methodName underlying method, such as jQuery.text, rather than using the external jQuery.fn.methodName method, such as jQuery.fn.text. jQuery.methodName is the underlying method used by the jQuery library itself to support jQuery.fn.methodName. Using it means reducing one layer of abstraction or avoiding creating new jQuery objects when calling the function method. Therefore, James proposed the idea of jQuery.single, where each call to jQuery.single means that the data of multiple objects is integrated into a centralized shared data structure, making it a kind of flyweight.

```javascript
jQuery.single = (o => {
var collection = jQuery([1]);
    return 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);
});

Facade Pattern #

The facade pattern is a common structure often seen in JavaScript libraries like jQuery. Its characteristic is to hide the implementation details of solving complex compatibility issues behind the scenes and only provide abstracted interfaces to the users.

Image

To give an analogy, the search engine we use can be considered as a “facade”. Its interface and operations are simple to the utmost. But the implementation behind the scenes is very complex, involving scheduling, crawling of network information, parsing, indexing, etc., and finally presenting the search results.

Image

Similarly, the $() selector in jQuery that we often use is responsible for handling the complex functionality of receiving and parsing multiple query types on the backend using the Sizzle engine, and presenting developers with a simpler set of selector functions.

Here is an example of $(document).ready(…). Under the hood, it is implemented based on a bindReady function.

function bindReady() { 
    // ...
    if (document.addEventListener) { 
        // Use the handy event callback 
        document.addEventListener('DOMContentLoaded', DOMContentLoaded, false); 
        // A fallback to window.onload, that will always work 
        window.addEventListener('load', jQuery.ready, false); 
        // If IE event model is used 
    } else if (document.attachEvent) { 
        document.attachEvent('onreadystatechange', DOMContentLoaded); 
        // A fallback to window.onload, that will always work 
        window.attachEvent('onload', jQuery.ready); 
    } 
}

The facade pattern provides a lot of convenience for users of jQuery, but it is not without costs. Although it reduces development cost, it sacrifices performance to some extent. For some simple page development, many developers still choose to use it because the requirements of page development in these applications are far from industrial-grade, but the development cost saved by using jQuery is exponential. This also reflects, to some extent, why jQuery remains so popular. Therefore, when developing, we should not only pay attention to the benefits that design patterns can bring, but also consider the usage scenarios and strike a balance between development efficiency and performance.

Composite Pattern #

The composite pattern refers to the ability to handle individual objects or groups of objects in the same way.

Image

In jQuery, you can use a unified approach to handle individual elements and a collection of elements, because they both return a jQuery object. The following code example of selectors demonstrates this. Here, the same show class attribute is added to two selections: one for a single element (e.g., an element with a unique ID), and another for a group of elements with the same tag name or class attribute.

// Single element
$( "#specialNote" ).addClass( "show" );
$( "#mainContainer" ).addClass( "show" );
// Group of elements
$( "div" ).addClass( "show" );
$( ".item" ).addClass( "show" );

Extension: What is the Wrapper Pattern #

In design patterns, we often mention refactoring, and the counterpart is wrapping. In design patterns, decorators and adapters often serve as wrappers. The characteristic of decorators and adapters is that they both provide decoration and adaptation without modifying the original object. For example, when using a third-party library or interface, it is impossible to modify their code, otherwise, it may cause side effects. In this case, wrapping plays a role in saving the day. We say decorators and adapters are both wrappers, so what is the difference between them? Let’s take a look at how these two patterns achieve wrapping.

Decorator #

As an example, we see more handsome boys and beautiful girls on the streets now compared to a few years ago. There could be several reasons for this, one is makeup and the other is plastic surgery. Makeup is a typical example of the decorator pattern. Because our facial features are difficult to change, if we have plastic surgery, we would need to undergo risky procedures that could potentially disfigure us, so makeup can avoid this risk. If the makeup is not done well, it can be removed and redone at any time. We have also seen many skilled makeup artists online who can create various celebrity faces, which is almost like face swapping. This way, we can have a different face every day, which is very cool to think about. Makeup is a wrapper for objects. When we don’t want to directly modify a component, decorators come in handy.

Image

If we want to extract a class from a non-self-developed component that we don’t want or cannot directly manipulate, we can use decorators. Moreover, decorators can reduce a large number of subclasses in our program. Decorators can provide a convenient feature, which is customization and configuration of expected behavior. Now let’s take a look at a basic implementation of it. In this example, we decorate a car into a limited edition upgraded version.

class Car {
  constructor(model, maker, price) {
    this.model = model;
    this.maker = maker;
    this.price = price;
  }

  getDetails() {
    return `${this.model} by ${this.maker}`;
  }
}

// decorator 1
function specialEdition(car) {
  car.isSpecial = false;
  car.specialEdition = function() {
    return `special edition ${car.getDetails()}`;
  };

  return car;
}

// decorator 2
function upgrade(car) {
  car.isUpgraded = true;
  car.price += 5000;
  return car;
}

// usage
var car1 = specialEdition(new Car('Camry', 'Toyota', 10000));

console.log(car1.isSpecial); // false
console.log(car1.specialEdition()); // 'special edition Camry by Toyota'

var car2 = upgrade(new Car('Crown', 'Toyota', 15000));

console.log(car2.isUpgraded); // true
console.log(car2.price); // 20000

In jQuery, decorators can be implemented using extend(). For example, in the following example, let’s assume we have a vehicle OS system with default options and some feature options. We can use extend to add them, and in this process, the objects defaults and options themselves remain unchanged.

// define the objects we're going to use
vehicleOS = {
    defaults: {},
    options: {},
    settings: {},
};

// merge defaults and options, without modifying defaults explicitly
vehicleOS.settings = $.extend(
    {},
    decoratorApp.defaults,
    ...
decoratorApp.options
);

Adapter #

The adapter is also easy to understand, as it is something we often encounter in our daily lives. For example, if we purchase an electronic product with a British standard, it will be difficult to find a suitable socket to use it in China. This is because the standards and hole sizes are different. However, if we use an adapter, this problem can be easily solved. There are many similar examples, such as the conversion between Type-C and Type-A connectors. Now let’s take a look at the principles and implementation of adapters.

Image

The use of adapters can also be seen everywhere in jQuery. For example, when dealing with CSS opacity, we can use the following convenient methods:

// Cross browser opacity:
// opacity: 0.9; Chrome 4+, FF2+, Saf3.1+, Opera 9+, IE9, iOS 3.2+, Android 2.1+
// filter: alpha(opacity=90); IE6-IE8
// Setting opacity
$( ".container" ).css( { opacity: .5 } );
// Getting opacity
var currentOpacity = $( ".container" ).css('opacity');

However, behind the scenes, jQuery does a lot of work.

get: function( elem, computed ) {
  // IE uses filters for opacity
  return ropacity.test( (
        computed && elem.currentStyle ?
            elem.currentStyle.filter : elem.style.filter) || "" ) ?
    ( parseFloat( RegExp.$1 ) / 100 ) + "" :
    computed ? "1" : "";
},
set: function( elem, value ) {
  var style = elem.style,
    currentStyle = elem.currentStyle,
    opacity = jQuery.isNumeric( value ) ?
          "alpha(opacity=" + value * 100 + ")" : "",
    filter = currentStyle && currentStyle.filter || style.filter || "";
  // IE has trouble with opacity if it does not have layout
  // Force it by setting the zoom level
  style.zoom = 1;
  // if setting opacity to 1, and no other filters
  //exist - attempt to remove filter attribute #6652
  if ( value >= 1 && jQuery.trim( filter.replace( ralpha, "" ) ) === "" ) {
    // Setting style.filter to null, "" & " " still leave
    // "filter:" in the cssText if "filter:" is present at all,
    // clearType is disabled, we want to avoid this style.removeAttribute
    // is IE Only, but so apparently is this code path...
    style.removeAttribute( "filter" );
    // if there there is no filter style applied in a css rule, we are done
    if ( currentStyle && !currentStyle.filter ) {
      return;
    }
  }
  // otherwise, set new filter values
  style.filter = ralpha.test( filter ) ?
    filter.replace( ralpha, opacity ) :
    filter + " " + opacity;
}
};

Decorator or Adapter? #

Comparing the two, we can see that they are similar in that they both add a layer of wrapping to the original object when the main object cannot be directly modified. The difference lies in the way they wrap in different usage scenarios. Decorators are mostly used to add different features through nested wrapping, while adapters are more like interface mappings between objects.

Furthermore, when should we avoid using decorators and adapters? If we have control over the entire implementation (meaning we have our own library), we can achieve the same utility by modifying the base code rather than complicating the interface through wrapping. Also, as with any design pattern, if you must use a wrapping pattern, make sure the actual result and impact are simpler and easier to understand than the original non-pattern code.

Summary #

This lesson ends here, and let me summarize it. Today, I have introduced several classic structural design patterns using jQuery. We can see that although jQuery is often criticized by many people, its existence is reasonable. It provides a lot of convenience for development through the facade pattern, composite pattern, and adapter pattern. At the same time, it also has its unique application scenarios for the flyweight pattern, and it also has its support for decorators.

Thought question #

We mentioned decorators earlier, and they can also be implemented using Proxy mentioned in the previous lecture. Can you explain how it is implemented?

Please feel free to share your answers, exchange learning experiences, or ask questions in the comments. If you find it helpful, you’re also welcome to share today’s content with more friends. See you in the next session!