preface

Learning design patterns is to enable us to quickly find a certain pattern as a solution in the right scenario. From the code level, the role of design patterns is to enable us to write reusable and maintainable programs. Three similar and confusing patterns, decorator pattern, proxy pattern and adapter pattern, are selected to explain in detail. The key to distinguish them is the intention of the pattern.

It is also important that all design patterns follow one principle:

Identify the parts of the program that change and encapsulate the changes

Most of the content of this article refers to “JavaScript Design Patterns and Development Practices” written by Zeng Tan. If you are interested in JavaScript design patterns and practices, you are strongly advised to read the original book.

Decorator pattern

In traditional object-oriented languages, the method of inheritance is often used to add functions to objects. However, inheritance is not flexible and brings many problems 😕; On the one hand, there is strong coupling between superclass and subclass. When the superclass changes, the subclass also changes. Inheritance, on the other hand, is often referred to as “white box reuse.” “White box” is relatively visible. The inner details of a superclass are visible to subclasses, and inheritance is often thought of as breaking encapsulation. In addition, inheritance can create a large number of subclasses and consume memory. Decorator mode can dynamically add responsibilities to an object during program execution without changing the object itself. Decorator is a lighter and more flexible approach than inheritance, a “pay-as-you-go” approach, such as wearing an extra coat when it’s cold, or sticking a bamboo dragonfly in your head when you need to fly.

Decorative function

In JavaScript, it’s easy to extend properties and methods on an object, but it’s hard to add extra functionality to a function without changing its source code.

let a = function(){
    alert(1)}/ / to:
let a = function(){
    alert(1)
    alert(2)}Copy the code

Most of the time we should not touch the original function, maybe the implementation of the original function is very messy and not written 😤. Now we need a way to add functionality to a function without changing its source code.

let a = function(){
    alert(1)}let _a = a

a = function(){
    _a()
    alert(2)
}
a()
Copy the code

First use _a to save the reference to the original function, then add the new function in the new function and call the original function and finally assign the value to the variable A; This does allow you to add functionality to a function without changing the function’s source code, and it follows the open-close principle, but there are two problems with this approach

  • You have to maintain the variable _a, and if you have too many decorations that wrap around the original function you’re going to have multiple variables that you need to maintain.
  • In this example, the “this” was hijacked
var _getElementById = document.getElementById

document.getElementById = function(id){
    alert(1)
    return _getElementById(id)
}

var button = document.getElementById('btn')

Copy the code

The console raises an exception after executing this code: Uncaught TypeError: Illegal Invocation, since _getElementById is a global function, this points to the window, and the document.getElementById internal implementation needs to use this, This is expected to point to document, not window, in this method, which is why the error occurs. Here we just pass in the document as the context this

document.getElementById = function(id){
    alert(1)
    return _getElementById.apply(document,id)
}
Copy the code

While this is obviously inconvenient, here’s a perfect way to add functionality to a function dynamically — AOP😆

Decorate functions with AOP

The primary role of AOP (aspect oriented programming) is to isolate functions that are irrelevant to the core business logic, such as diary statistics, security controls, exception handling, and so on. These functions are extracted and “dynamically woven” into business logic modules. The benefits of doing this are, first, preserving the purity and high cohesion of the business logic modules and, second, making it easy to reuse functional modules such as log statistics.

Take a look at the following JavaScript implementation:

    Function.prototype.before(fn){
        let self = this
        return function(){
            fn.apply(self,arguments)}}Function.prototype.after = function(afterfn){
        let self = this
        return function(){
            let res = self.apply(this.arguments)
            afterfn.apply(this.arguments)
            return res
        }
    }
    window.onload = function () {
        console.log('window.onload');
    }

    window.onload = (window.onload || function () { })
        .before(() = > { console.log('before'); })
        .after(() = > { console.log('after'); })
Copy the code

Plug-in form validation

We’ve all written code for form validation, which often requires some validation before submitting the form to the background, such as checking that the user name and password are null when logging in

<html>

<body>User name:<input id="username" type="text">Password:<input id="password" type="password">
    <input id="submitBtn" value="Submit" type="button">
</body>
<script>
    let button = document.getElementById('submitBtn')
    let usernameInput = document.getElementById('username')
    let passwordInput = document.getElementById('password')
    function submit() {
        if(! usernameInput.value)return alert('User name cannot be empty')
        if(! passwordInput.value)return alert('Password cannot be empty')
        console.log(usernameInput.value, passwordInput.value);
        let parms = {
            username:usernameInput.value,
            password:passwordInput.value
        }
        ajax('http://xxx.com/login',parms)
    }
    button.onclick = function () {
        submit()
    }
</script>

</html>
Copy the code

The submit function does two things here: in addition to submitting the Ajax request, it also validates the user’s input. This code back-and-forth results in bloated functions, confusing responsibilities, and no reusability.

    function validata() {
        if (usernameInput.value === ' ') {
            return false
        }
        if (passwordInput.value === ' ') {
            return false}}function submit() {
        if (validata() === false) {
            return
        }
        console.log(usernameInput.value, passwordInput.value);
        let parms = {
            username: usernameInput.value,
            password: passwordInput.value
        }
        ajax('http://xxx.com/login', parms)
    }
    button.onclick = function () {
        submit()
    }
Copy the code

Here we have taken the validation logic out, but the return value of Validata is still computed in the Submit function, and we use decorator mode to completely separate Validata from Submit

<html>

<body>User name:<input id="username" type="text">Password:<input id="password" type="password">
    <input id="submitBtn" value="Submit" type="button">
</body>
<script>
    let button = document.getElementById('submitBtn')
    let usernameInput = document.getElementById('username')
    let passwordInput = document.getElementById('password')

    Function.prototype.before = function (beforefn) {
        let self = this
        return function () {
            if (beforefn.apply(this.arguments) = = =false) {
                return
            }
            return self.apply(this.arguments)}}function validata() {
        if (usernameInput.value === ' ') {
            alert('User name cannot be empty')
            return false
        }
        if (passwordInput.value === ' ') {
            alert('Password cannot be empty')
            return false}}function submit() {
        console.log(usernameInput.value, passwordInput.value);
        let parms = {
            username: usernameInput.value,
            password: passwordInput.value
        }
        ajax('http://xxx.com/login', parms)
    }
    submit = submit.before(validata)
    button.onclick = function () {
        submit()
    }

</script>

</html>
Copy the code

Submit = submit. Before (Validata) is a plug-and-play function until the validation rule is dynamically inserted into the submit function. It can even be written as a configuration file, which helps us maintain the two functions separately. With a little modification of the policy pattern, we can write these validation rules in the form of plug-ins 😃.

The proxy pattern

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.

Note the distinction between the proxy pattern and the decorator pattern. The proxy pattern controls access to the original object. Decorator mode dynamically adds behavior to an object. That is, the proxy pattern determines the relationship between the proxy object and the original object at the outset, whereas the decorator pattern is used to determine the full functionality of the object at the outset. What distinguishes the two patterns is their intent and design purpose.

The book introduces the proxy model with an interesting example:

On A sunny Morning in April, Xiao Ming met his 100% girl. We will call Xiao Ming’s goddess A. Two days later, Xiao Ming decided to send A A bunch of flowers to express his love. Just then, Xiaoming learned that A and he have A common friend B, who can know A’s mood. Therefore, Xiaoming decided to let B send flowers when A is in A good mood, which can greatly improve the success rate.

The following code is used to describe the process of Xiao Ming chasing goddess

var Flower = function(){}

var xiaoming = {
    sendFlower:function(target){
        var flower = new Flower()
        target.receiveFlower(flower)
    }
}

var A = {
    receiveFlower:function(flower){
        console.log('Received flowers' + flower)
    }
}

xiaoming.sendFlower(A)
Copy the code

Next, we introduce B, that is, Xiao Ming sends flowers to A through B

var Flower = function(){}

var xiaoming = {
    sendFlower:function(target){
        var flower = new Flower()
        target.receiveFlower(flower)
    }
}

var B = {
    receiveFlower:function(flower){
         A.listenGoodMood(function(){
             A.receiveFlower(flower)
         })
    }
}

var A = {
    receiveFlower:function(flower){
        console.log('Received flowers' + flower)
    },
    listenGoodMood:function(fn){    // Suppose the mood improves after 10 seconds
        setTimeout(function(){
            fn()
        },10000)
    }
}

xiaoming.sendFlower(B)

Copy the code

The virtual proxy implements image preloading

Virtual proxy refers to delaying the creation of some expensive objects until they are really needed. Take Xiaoming’s example above. If the new Flower is an expensive operation, we can ask B to monitor A’s mood and execute the new Flower operation when A is in A better mood, which is A virtual proxy. Another common type of proxy is called a protection proxy, which is used to control the access of objects with different permissions to the target object. Let’s still take The example of Xiao Ming above. B can help A filter some requests. If the suitor is too old or too short, B can directly reject such requests for A, with A and B acting as the good cop and the bad cop. White Face A continued to maintain her image as A goddess, and did not want to reject anyone directly, so he recruited Black Face B to control access to A. However, in JavaScript we have no way of knowing who is accessing an object, so it is not a good idea to implement a protected proxy. Let’s look at virtual proxies in more detail.

let myImage = (function(){
    let imgNode = document.createElement('img')
    document.body.appendChild(imgNode)
    return {
        setSrc:function(src){
            imgNode.src = src
        }
    }
})();

let proxyImg = (function(){
    let img = new Image()
    img.onload = function(){
        myImage.setSrc(this.src)
    }
    return function(src){
        myImage.setSrc('xxxxx')   // Add a placeholder map
        img.src = src
    }
})();

proxyImg(xxx)     // The diagram to load
Copy the code

Img. onload is triggered when the image that needs to be displayed is loaded. Note that this refers to img and the loaded image replaces the placeholder. Here’s how to implement image preloading without using virtual proxy code.

let myImage = (function(){
    let imgNode = document.createElement('img')
    document.body.appendChild(imgNode)
    let img = new Image
    img.onload = function(){
        imgNode.src = this.src
    }
    return {
        setSrc:function(src){
            imgNode.src = 'xxxxx' / / placeholder figure
            img.src = src
        }
    }
})()
myImage.setSrc(xxxx)

Copy the code

Without design mode, we use much shorter code to implement image preloading, so is proxy mode too weak? Let’s look at what the proxy pattern means

Meaning of agency

One of the design principles of object orientation is the single responsibility principle, which states that there should be only one cause of change for a class (and usually also for objects and functions). If an object takes on more than one responsibility, it means that the object becomes large and changes for more than one reason. Object-oriented design encourages the distribution of behavior among fine-grained objects, and if an object takes on too many responsibilities, those responsibilities are coupled together, which leads to fragile and low-cohesion designs. As the changes change, the design can be broken by accident. In the example above, myImage is responsible for both generating img nodes and preloading them. When we deal with one responsibility, it may affect the implementation of the other because of its strong coupling. In addition, in object-oriented programming, most of the time violating any other principle also violates the open-closed principle. The principle is defined as follows:

Software entities (classes, modules, functions), etc., should be extensible but not modifiable.

In the case of preloading images, if the images are too small, we don’t need to preload them. Instead of using design mode, we need to modify the myImage function directly, violating the open-close principle. Instead, we can use the myImage function directly, using proxyImg if necessary.

The caching proxy

Example of caching proxy – computes product

    let mult = function () {
        console.log('calculated');
        let result = 1;
        for (let i = 0; i < arguments.length; i++) {
            result = arguments[i] * result;
        }
        return result
    }


    let proxyMult = (function() {
        let cache = {}
        return function () {
            var args = Array.prototype.join.call(arguments.', ')
            if (args in cache) {
                return cache[args]
            }
            return cache[args] = mult.call(this.arguments[0])}}) ()console.log("Direct calculation",proxyMult(3.4.5));
    console.log("Use cache",proxyMult(3.4.5));
Copy the code

Mult is a multiplicative function. We store the used multiplier as a variable in the cache as an attribute. By judging whether there is the same multiplier involved in the calculation later, we choose whether to match the cached value in the cache or call the mult function to recalculate.

Caching proxies are used for Ajax asynchronous requests for data

We often get paging requests in projects, and the data on the same page should theoretically only need to be pulled in the background once. The pulled data is cached somewhere, and the next time the same page is requested, the data can be used directly.

Let’s implement:

<! DOCTYPEhtml>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Use cache proxy to achieve page turning effect</title>
</head>
<style>
    body {
        display: flex;
        justify-content: center;
        align-items: center;
        flex-direction: column;
        height: 100vh;
    }
</style>

<body>
    <div id="container"></div>
    <! -- Pager -->
    <div id="footContainer">
        <button id="pre">"</button>
        <button id="next">"</button>
    </div>
</body>
<script>
    let container = document.getElementById('container')
    let preBtn = document.getElementById('pre')
    let nextBtn = document.getElementById('next')
    let page = 1, cache = { '1': `${ajax(1).content}` }
    container.innerHTML = `${ajax(1).content}`
    preBtn.onclick = proxyPrePage
    nextBtn.onclick = proxyNextPage
    function ajax(page) {
        return {
            page,
            content: I was the first ` < span >${page}Page contents </span> '}}function prePage() {
        if (page <= 1) {
            alert("No turning forward!)
            return
        }
        container.innerHTML = `${ajax(--page).content}`
    }
    function nextPage() {
        container.innerHTML = `${ajax(++page).content}`
    }
    function proxyPrePage() {
        if (page - 1 in cache) {
            console.log("proxyPrePage");
            container.innerHTML = cache[--page]
            return
        }
        if (page > 1) {
            cache[String(page - 1)] = ajax(page - 1).content
        }
        prePage()
    }
    function proxyNextPage() {
        if (page + 1 in cache) {
            console.log("proxyNextPage");
            container.innerHTML = cache[String(++page)]
            return
        }
        cache[String(page + 1)] = ajax(page + 1).content
        nextPage()
    }

</script>

</html>
Copy the code

Prebtn. onclick = prePage, nextbtn. onclick = nextPage, nextbtn. onclick = nextPage This design reduces the coupling of the code without worrying about the side effects of changing the original function.

Adapter mode

The alias of an adapter is a wrapper. Imagine this scenario: when we try to call the interface of a module or an object, the interface is not formatted as expected. There are two ways to do this. The first way is to modify the original interface directly, but this is obviously not what we want, especially if the interface is not the one we coded…. The second approach is to create an adapter that converts the original interface to the desired one. There are many common examples in life: USB adapters, power adapters and so on.

The following example will help us understand the adapter pattern more fully:

let googleMap = {
    show:function(){
        console.log('Start rendering Google Maps')}}let baiduMap = {
    show:function(){
        console.log('Start rendering baidu Map')}}let renderMap = function(map){
    if(map.show instanceof function){
        map.show()
    }
}

Copy the code

In this example, googlemap. show and Baidumap. show are both third-party interfaces. This code works because they both provide an interface named show. At this time we can use a wrapper to wrap it

let baiduMap = {
    display:function(){
        console.log('Start rendering baidu Map')}}let baiduMapAdapter = {
    show:function(){
        baiduMap.display()
    }
}

Copy the code

conclusion

Finally, it is always important to note that what distinguishes design patterns is the intent of the pattern:

  • Decorator mode is used to provide more functionality to the decorator
  • The proxy pattern is used to control access to the ontology by different requests
  • The adapter pattern resolves mismatches between two existing interfaces

reference

JavaScript Design Patterns and Development Practices