I’m Garfield at 🐱. If you like my article, please feel free to like it and share it

1. Introduction of problems

One day the product student came to me.

Product student 🤵 : There is a requirement to add permission judgment to the expansion operation

Garfield 🐱 : Ok

const clusterExapnd = () = > {
  console.log("Cluster Expansion Operation");
}
Copy the code

Garfield 🐱 : Finished, have a look

const clusterExapnd = () = > {
  if(loginUser ! = ="admin") {
	console.log("Current user has no operation permission!");
	return;
  }
  console.log("Cluster Expansion Operation");
}
Copy the code

I believe many students will write this way. In the simple scenario above, this works. But then, as demand increased, problems began to emerge.

Product classmate 🤵 : I come again, now all operations must add authority judgment

Garfield 🐱 : Ok

const clusterExapnd = () = > {
  console.log("Cluster Expansion Operation");
}

const clusterReboot = () = > {
  console.log("Cluster Restart");
}

const clusterClean = () = > {
  console.log("Cluster Cleanup");
}

const clusterShrink = () = > {
  console.log("Cluster Capacity Reduction");
}

const backUpFile = () = > {
  console.log("Backup file");
}

const toggleClusterWhiteList = () = > {
  console.log(Enable/Disable the Cluster Whitelist);
}
Copy the code

Garfield 🐱 : Finished, have a look

const clusterExapnd = () = > {
  if(loginUser ! = ="admin") {
	console.log("Current user has no operation permission!");
	return;
  }
  console.log("Cluster Expansion Operation");
}

const clusterReboot = () = > {
  if(loginUser ! = ="admin") {
	console.log("Current user has no operation permission!");
	return;
  }
  console.log("Cluster Restart");
}

const clusterClean = () = > {
  if(loginUser ! = ="admin") {
	console.log("Current user has no operation permission!");
	return;
  }
  console.log("Cluster Cleanup");
}

const clusterShrink = () = > {
  if(loginUser ! = ="admin") {
	console.log("Current user has no operation permission!");
	return;
  }
  console.log("Cluster Capacity Reduction");
}

const backUpFile = () = > {
  if(loginUser ! = ="admin") {
	console.log("Current user has no operation permission!");
	return;
  }
  console.log("Backup file");
}

const toggleClusterWhiteList = () = > {
  if(loginUser ! = ="admin") {
	console.log("Current user has no operation permission!");
	return;
  }
  console.log(Enable/Disable the Cluster Whitelist);
}
Copy the code

IF you follow this approach again, the code looks like this: You can see that every method has an IF judgment, and the logic is very redundant. If additional permission judgments are required later, they need to be added to all methods, resulting in a significant decrease in code maintainability.

Product student 🤵 : Please modify the prompt copy without permission

Product student 🤵 : Please add operation permission to the general administrator

Garfield cat 🐱 : dizzy, same logic each method inside all have to change again, too troublesome

Product classmate 🤵 :…

To achieve logic reuse, you obviously can’t put the logic of judgment in every function, but pull it out. Before I go into the method, I want you to think about how to implement it.

We know that in the policy pattern, the IF ELSE can be eliminated by the idea of looking up tables to select different policies. There is no need to select a policy, just a pre-processing before each method is executed. In this scenario, either the proxy mode or the decorator mode can be used.

2. What is proxy/decorator mode

The proxy pattern is one of the classic Java design patterns. The core idea is to realize the extension of the object without modifying the original object and realize additional functions. Agent mode is everywhere in the development and framework source code, the process of program development, can not move others’ code, with the agent of others’ code for extension.

define

Provide a proxy object to other objects and control access to it.

The characteristics of

1) Very straightforward, implement the same interface or inherit the same abstract class.

2) The proxy object controls access to the proxy object.

UML

Let’s start with a proxy pattern implemented in Java:

// The first is the abstract theme role, which is very simple, just defining the movie method
public interface Subject {
    public void movie(a);
}
Copy the code
// Implementation of the agent role
public class Star implements Subject {
    @Override
    public void movie(a) {
        System.out.println(getClass().getSimpleName() + ": Agent picked up a movie, I'll make it."); }}Copy the code
// Implement the proxy role
public class Agent implements Subject {
    private Subject star;

    public Agent(Subject star) {
        this.star = star;
    }

    @Override
    public void movie(a) {
        System.out.println(getClass().getSimpleName() + ": The script is great. The movie is here."); star.movie(); }}Copy the code

The proxy role references the proxy role. To access the proxy role, the proxy role is responsible for functions other than its own, and has access and filtering functions. Finally, the client implementation:

public class Client {
    public static void main(String[] args) {
        Subject star = new Star();
        Subject proxy = newAgent(star); proxy.movie(); }}Copy the code

While the proxied method is called, the actual actor is the proxied role Star.

Similar to the proxy pattern is the decorator pattern. Let’s start with a UML diagram of the decorator pattern:

Yes, the decorator pattern and the proxy pattern are so similar, including UML and code implementations, that they can even be identical. Don’t believe it? Look at the code for the decorator pattern:

// The abstract component
public interface Component {    
    public void movie(a);    
}

// The implementation Component is to be decorated
public class Star implements Component {
    @Override
    public void movie(a) {
        System.out.println(getClass().getSimpleName() + "It's fun to make a movie when you're wearing makeup."); }}// Decorator
public class Agent implements Component {
    private Component star;

    public Agent(Component Agent) {
        this.star = Agent;
    }

    @Override
    public void movie(a) {
        System.out.println(getClass().getSimpleName() + ": All the props and makeup for the movie."); star.movie(); }}public class Client {    
    public static void main(String[] args) {
        Subject star = new Star();
        Subject proxy = newAgent(star); proxy.movie(); }}Copy the code

The same

  • Both need to implement the same interface or inherit from the same abstract class, and both the proxy and decorator roles hold references to the proxy and component roles.
  • Both patterns can add their own methods before and after the business methods of the brokered role and the concrete component role.

The difference between

  • The proxy pattern focuses on controlling the behavior of objects, while the decorator pattern focuses on increasing (or weakening) the functions of objects.
  • In layman’s terms, the fundamental difference between them is the purpose, which is the context.

3. Advantages and disadvantages of the proxy model

advantages

1) Good scalability. Modifying the proxied role does not affect the use of the proxy by the caller; the proxied role is transparent to the caller. 2) Isolation, reduce the coupling degree. The proxy role coordinates the caller and the proxy role. The proxy role only needs to implement the services it cares about, and the services that are not its own business are processed and isolated through the proxy.

disadvantages

1) Add proxy class, implementation needs to go through proxy, so the request speed will be slower.

4. Closures and higher-order functions in JS

The sample code shown above implements the proxy pattern in an object-oriented manner, consistent with the Java language. In JavaScript, we don’t necessarily need to use classes to encapsulate. We know that JS supports functional programming, so we can use the idea of functional programming to implement the proxy pattern. Before implementing the proxy pattern, two concepts need to be introduced.

Higher-order functions

In functional programming, a function can be assigned to a variable, passed as an argument to another function, or returned as a value from another function. In this sense, higher-order functions appear. A higher-order Function is a Function that meets at least one of the following conditions:

  1. Functions can be passed as arguments;
  2. Functions can be output as return values;

closure

Higher-order functions are followed by closures.

Closures are very popular because they can change the life cycle of local variables without changing their scope.

5. JS functional programming to achieve the proxy mode

Going back to the original question, how can we optimize permission determination based on the proxy pattern? We can define a high-order function valid as a proxy method, which takes the function we need to execute as an argument and returns a function. In the returned function, we perform permission check. If the check passes, the function we passed will be called.

const valid = (cb: () => void) = > {
  return () = > {
    if (loginUser === "admin") {
      cb.apply(this);
    } else {
      console.log("Current user has no operation permission!"); }}}Copy the code

Then we wrap a valid layer around all methods that require permission checks:

let loginUser = "admin";

const valid = (cb: () => void) = > {
  return () = > {
    if (loginUser === "admin") {
      cb.apply(this);
    } else {
      console.log("Current user has no operation permission!"); }}}const clusterExapnd = valid(() = > {
  console.log("Cluster Expansion Operation");
})

const clusterReboot = valid(() = > {
  console.log("Cluster Restart");
})

const clusterClean = valid(() = > {
  console.log("Cluster Cleanup");
})

const clusterShrink = valid(() = > {
  console.log("Cluster Capacity Reduction");
})

const backUpFile = valid(() = > {
  console.log("Backup file");
})

const toggleClusterWhiteList = valid(() = > {
  console.log(Enable/Disable the Cluster Whitelist);
})

let func = [clusterExapnd, clusterReboot, clusterClean, clusterShrink, backUpFile, toggleClusterWhiteList];

func.forEach(f= > f());
Copy the code

In this way, the validation logic is separated from the method, making it very convenient to add additional permission judgments in the future without having to go down to each method for modification. Incidentally, pre-processing and post-processing logic can also be added based on the proxy mode.

Product classmate 🤵 : I come again, now need to each operation before the execution of confirmation, after the execution of the results of the prompt execution

Garfield 🐱 : No problem

This logic obviously doesn’t need to be added to every method, just inside the proxy method:

import inquirer from "inquirer";

const proxy = (cb) = > {
  return async() = > {const { ok } = await inquirer.prompt([{ 
	  type: 'confirm'.name: 'ok'.message: 'Are you sure to perform the operation? '.default: true 
	}])
	if(! ok) {return Promise.reject("User canceled operation!");
	}
    const res = await cb.apply(this);
    if (res.success) {
		return Promise.resolve("Operation successful!");
	} else {
		return Promise.reject("Operation failed!"); }}}Copy the code

Step 6 Take it one step further

Product student 🤵 : I have changed the requirements again. Now all operations need to be checked. Some operations need to be confirmed before execution, and some operations need to be prompted after execution

Garfield 🐱 : Uh, okay

In this case, instead of putting all the logic into a single proxy function, we need to break down the different proxy functions according to their functions and put them together like building blocks:

import inquirer from "inquirer";

const valid = (cb) = > {
  return async() = > {if (loginUser === "admin") {
      cb.apply(this);
    } else {
      return Promise.reject("Current user has no operation permission!"); }}}const inquire = (cb) = > {
  return async() = > {const { ok } = await inquirer.prompt([{ 
	  type: 'confirm'.name: 'ok'.message: 'Are you sure to perform the operation? '.default: true 
	}])
	if(! ok) {return Promise.reject("User canceled operation!");
	}
    cb.apply(this); }}const result = (cb) = > {
  return async() = > {const res = await cb.apply(this);
    if (res.success) {
		return Promise.resolve("Operation successful!");
	} else {
		return Promise.reject("Operation failed!"); }}}Copy the code

Then use it like this:

// Verify method, confirm before operation, display result after operation
const clusterExapnd = valid(inquire(result(() = > {
  console.log("Cluster Expansion Operation");
})))
// Verify method and confirm before operation
const clusterReboot = valid(inquire(() = > {
  console.log("Cluster Restart");
}))
// Only method validation
const clusterClean = valid(() = > {
  console.log("Cluster Cleanup");
})
// verify the method and display the result after operation
const toggleClusterWhiteList = valid(result(() = > {
  console.log(Enable/Disable the Cluster Whitelist);
}))
Copy the code

This enables logical decoupling and improves the maintainability of the code.

Product classmate 🤵 : too strong, worthy of being Garfield

Garfield 🐱 : 😜

But the above nested functions are not elegant. Subsequently, there are two optimization directions. One is to use the chain of responsibility mode, which is abstracted into the middleware based compose function form. The other is AOP aspect programming using the decorator syntax. Due to space limitations, we will discuss this in a later article. Please stay tuned!

reference

My Java design pattern – proxy pattern

Explore JS design patterns from closures and higher-order functions