我是加菲猫,如果你喜欢我的文章,欢迎点赞分享哦
某天产品同学来找我。
产品同学:有一个需求,需要对扩容操作加上权限判断
加菲猫:好的
const clusterExapnd = () => {
console.log("集群扩容操作");
}
加菲猫:改完了,看下
const clusterExapnd = () => {
if (loginUser !== "admin") {
console.log("当前用户没有操作权限!");
return;
}
console.log("集群扩容操作");
}
相信很多同学都会这样写。在上面这个简单的场景中,这样确实可行。但是后来随着需求的增加,问题开始慢慢暴露出来。
产品同学:我又来了,现在所有的操作都要加权限判断
加菲猫:好的
const clusterExapnd = () => {
console.log("集群扩容操作");
}
const clusterReboot = () => {
console.log("集群重启");
}
const clusterClean = () => {
console.log("集群清理");
}
const clusterShrink = () => {
console.log("集群减容");
}
const backUpFile = () => {
console.log("备份文件");
}
const toggleClusterWhiteList = () => {
console.log("开启/关闭集群白名单");
}
加菲猫:改完了,看下
const clusterExapnd = () => {
if (loginUser !== "admin") {
console.log("当前用户没有操作权限!");
return;
}
console.log("集群扩容操作");
}
const clusterReboot = () => {
if (loginUser !== "admin") {
console.log("当前用户没有操作权限!");
return;
}
console.log("集群重启");
}
const clusterClean = () => {
if (loginUser !== "admin") {
console.log("当前用户没有操作权限!");
return;
}
console.log("集群清理");
}
const clusterShrink = () => {
if (loginUser !== "admin") {
console.log("当前用户没有操作权限!");
return;
}
console.log("集群减容");
}
const backUpFile = () => {
if (loginUser !== "admin") {
console.log("当前用户没有操作权限!");
return;
}
console.log("备份文件");
}
const toggleClusterWhiteList = () => {
if (loginUser !== "admin") {
console.log("当前用户没有操作权限!");
return;
}
console.log("开启/关闭集群白名单");
}
如果再按照这种方法处理,代码就会变成上面这样:可以看到每个方法中都进行了 IF
判断,逻辑冗余严重。假如后期还要求增加其他权限判断,则需要在所有方法中都添加一遍,导致代码维护性大大降低。
产品同学:麻烦修改下没有权限的提示文案
产品同学:麻烦给普通管理员添加操作权限
加菲猫:晕,同样逻辑每个方法里面都得改一遍,太麻烦了
产品同学:。。。
若要实现逻辑的复用,显然不能将判断的逻辑放到每个函数里面,而是要抽到外面。在讲解方法之前,大家可以先自己考虑应该如何实现。
在上一篇 设计模式系列:策略模式 中,讲到可以通过查表的思想来选择不同策略,从而消除 IF ELSE
。但是这边并不需要选择策略,只是需要在每个方法执行前做一个前置处理。这种场景下,可以使用代理模式或者装饰器模式。
代理模式是 Java 经典设计模式之一,核心思想就是在不修改原有对象的前提下,实现了对象的扩展,实现额外附加功能。代理模式在开发和框架源码中无处不在,程序开发过程中,不能经过动别人的代码,用代理对别人的代码进行扩展。
定义
为其它对象提供一个代理对象,并由代理对象控制这个对象的访问。
特点
1)很直接的,实现同一个接口或者继承同一个抽象类。
2)代理对象控制对被代理对象的访问。
UML
先来看一个用 Java 实现的代理模式:
// 首先是抽象主题角色,很简单,单纯定义了movie方法
public interface Subject {
public void movie();
}
// 被代理角色的实现
public class Star implements Subject {
@Override
public void movie() {
System.out.println(getClass().getSimpleName() + ":经纪人接了一部电影,我负责拍就好");
}
}
// 代理角色的实现
public class Agent implements Subject {
private Subject star;
public Agent(Subject star) {
this.star = star;
}
@Override
public void movie() {
System.out.println(getClass().getSimpleName() + ":剧本很好,这部电影接下了");
star.movie();
}
}
代理角色持有被代理角色的引用,要访问被代理角色必须通过代理,负责被代理角色本职之外的职能,并且具有准入和过滤的功能。最后来看客户端的实现:
public class Client {
public static void main(String[] args) {
Subject star = new Star();
Subject proxy = new Agent(star);
proxy.movie();
}
}
表面上是调用了代理的方法,实际的执行者其实是被代理角色Star。
与代理模式类似的还有装饰器模式,先来看装饰器模式的 UML 图:
没错,装饰器模式和代理模式就是这么相似,包括UML和代码实现,甚至可以是一模一样。不信?来看装饰器模式的代码:
// 抽象构件
public interface Component {
public void movie();
}
// 具体构件,实现Component,要被装饰的
public class Star implements Component {
@Override
public void movie() {
System.out.println(getClass().getSimpleName() + ":化了妆迷倒一片妹纸,拍起来电影特别带劲");
}
}
// 装饰者,装饰具体构件
public class ConcreteDecorator implements Component {
private Component star;
public ConcreteDecorator(Component concreteComponent) {
this.star = concreteComponent;
}
@Override
public void movie() {
System.out.println(getClass().getSimpleName() + ":拍电影各种道具加身,还得化妆");
star.movie();
}
}
public class Client {
public static void main(String[] args) {
Subject star = new Star();
Subject proxy = new Agent(star);
proxy.movie();
}
}
相同点
不同点
优点
1)良好的扩展性。修改被代理角色并不影响调用者使用代理,对于调用者,被代理角色是透明的。
2)隔离,降低耦合度。代理角色协调调用者和被代理角色,被代理角色只需实现本身关心的业务,非自己本职的业务通过代理处理和隔离。
缺点
1)增加了代理类,实现需要经过代理,因此请求速度会变慢。
上面给出的示例代码是通过面向对象的方式实现代理模式,符合 Java 语言的特点。在 JavaScript 中,我们不一定需要通过类去封装,我们知道,JS 是支持函数式编程的,因此我们可以利用函数式编程的思想来实现代理模式。在实现代理模式之前,需要引入两个概念。
在函数式编程中,函数可以可以赋值给一个变量,可以作为参数传入另一个函数,函数也可以作为另一个函数的返回值。在这个意义上,就出现了高阶函数。高阶函数(Higher-Order Function)是至少满足如下条件之一的函数:
出现高阶函数之后,闭包也随之出现。
“闭包”可以改变局部变量的生命周期,并且不更改局部变量的作用范围,这一特性使得闭包的运用非常广泛。
回到最开始的问题,我们如何基于代理模式优化权限判断的功能?我们可以定义一个高阶函数 valid
作为代理方法,这个函数接收我们需要执行的函数作为参数,然后返回一个函数,在返回的函数中,我们进行权限校验,如果校验通过,就调用我们传入的函数,反之不执行,提示没有操作权限:
const valid = (cb: () => void) => {
return () => {
if (loginUser === "admin") {
cb.apply(this);
} else {
console.log("当前用户没有操作权限!");
}
}
}
然后我们对所有需要加权限校验的方法外面包裹一层 valid
:
let loginUser = "admin";
const valid = (cb: () => void) => {
return () => {
if (loginUser === "admin") {
cb.apply(this);
} else {
console.log("当前用户没有操作权限!");
}
}
}
const clusterExapnd = valid(() => {
console.log("集群扩容操作");
})
const clusterReboot = valid(() => {
console.log("集群重启");
})
const clusterClean = valid(() => {
console.log("集群清理");
})
const clusterShrink = valid(() => {
console.log("集群减容");
})
const backUpFile = valid(() => {
console.log("备份文件");
})
const toggleClusterWhiteList = valid(() => {
console.log("开启/关闭集群白名单");
})
let func = [clusterExapnd, clusterReboot, clusterClean, clusterShrink, backUpFile, toggleClusterWhiteList];
func.forEach(f => f());
这样就实现了校验逻辑和方法的分离,以后如果需要添加其他权限判断也非常方便,不需要深入到每个方法进行修改。顺便一提,基于代理模式还可以添加前置处理和后置处理逻辑。
产品同学:我又来了,现在需要对每个操作执行前进行确认,执行完毕后提示执行的结果
加菲猫:没问题
这个逻辑显然也不需要深入到每个方法里面添加,只要在代理方法内部添加即可:
import inquirer from "inquirer";
const proxy = (cb) => {
return async () => {
const { ok } = await inquirer.prompt([{
type: 'confirm',
name: 'ok',
message: '确定执行操作?',
default: true
}])
if (!ok) {
return Promise.reject("用户取消操作!");
}
const res = await cb.apply(this);
if (res.success) {
return Promise.resolve("操作成功!");
} else {
return Promise.reject("操作失败!");
}
}
}
产品同学:我又来改需求了,现在所有操作都要进行校验,有些方法执行前需要确认,有些方法执行后需要提示结果,应该不难吧
加菲猫:额,好吧
在这种情况下,我们不能把所有的逻辑都放到一个代理函数内部,而是需要根据功能拆分出不同的代理函数,然后像搭积木一样组合到一起:
import inquirer from "inquirer";
const valid = (cb) => {
return async () => {
if (loginUser === "admin") {
cb.apply(this);
} else {
return Promise.reject("当前用户没有操作权限!");
}
}
}
const inquire = (cb) => {
return async () => {
const { ok } = await inquirer.prompt([{
type: 'confirm',
name: 'ok',
message: '确定执行操作?',
default: true
}])
if (!ok) {
return Promise.reject("用户取消操作!");
}
cb.apply(this);
}
}
const result = (cb) => {
return async () => {
const res = await cb.apply(this);
if (res.success) {
return Promise.resolve("操作成功!");
} else {
return Promise.reject("操作失败!");
}
}
}
然后这样使用:
// 方法校验、操作前确认、操作后展示结果
const clusterExapnd = valid(inquire(result(() => {
console.log("集群扩容操作");
})))
// 方法校验、操作前确认
const clusterReboot = valid(inquire(() => {
console.log("集群重启");
}))
// 只有方法校验
const clusterClean = valid(() => {
console.log("集群清理");
})
// 方法校验、操作后展示结果
const toggleClusterWhiteList = valid(result(() => {
console.log("开启/关闭集群白名单");
}))
这样就实现了逻辑解耦,提升了代码的维护性。
产品同学:太强了,不愧是加菲猫
加菲猫:
但是上面这样函数嵌套函数的写法,其实还是不够优雅。后续有两种优化方向,一种是使用责任链模式,抽象为基于中间件的 compose function 形式;另一种则是使用装饰器语法实现 AOP 切面编程。由于篇幅限制,我们会在之后的文章进行讨论,欢迎持续关注!
我的Java设计模式-代理模式
从闭包和高阶函数初探JS设计模式