设计模式系列:从实际项目需求谈谈代理模式/装饰器模式

我是加菲猫,如果你喜欢我的文章,欢迎点赞分享哦

1. 问题引入

某天产品同学来找我。

产品同学:有一个需求,需要对扩容操作加上权限判断

加菲猫:好的

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 。但是这边并不需要选择策略,只是需要在每个方法执行前做一个前置处理。这种场景下,可以使用代理模式或者装饰器模式

2. 什么是代理模式/装饰器模式

代理模式是 Java 经典设计模式之一,核心思想就是在不修改原有对象的前提下,实现了对象的扩展,实现额外附加功能。代理模式在开发和框架源码中无处不在,程序开发过程中,不能经过动别人的代码,用代理对别人的代码进行扩展。

定义
为其它对象提供一个代理对象,并由代理对象控制这个对象的访问。

特点
1)很直接的,实现同一个接口或者继承同一个抽象类。

2)代理对象控制对被代理对象的访问。

UML

设计模式系列:从实际项目需求谈谈代理模式/装饰器模式_第1张图片

先来看一个用 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 图:

设计模式系列:从实际项目需求谈谈代理模式/装饰器模式_第2张图片
没错,装饰器模式和代理模式就是这么相似,包括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();
    }
}

相同点

  • 都需要实现同一个接口或者继承同一个抽象类,并且代理角色和装饰角色都持有被代理角色和构件角色的引用。
  • 两种模式都可以在被代理角色和具体构件角色的业务方法前后添加自己的方法。额…说了等于没说。

不同点

  • 代理模式重点在于控制对象的行为,而装饰模式侧重于增加对象的职能(当然也可以削弱)。看完这句话立马晕菜,到底是怎么才叫控制行为和增加职能?设计模式离不开面对对象思想,用面向对象的思想思考这个问题。代理模式是对整个对象的行为控制和限制,而非针对功能,跟装饰模式不一样,装饰模式针对的是对象职能上的加强,也就是属性或者方法。
  • 通俗的讲,它们俩根本的区别是目的性不一样,也就是使用场景。比如公司老板,为了更好的管理公司会请做行政、财务的人回来帮忙处理公司事务,把事情整理好了,有必要的事情才到达老板那,这就是代理。如果是装饰呢,就是往老板身上加职能,不仅提供财力,还要懂行政管理,财务会计,甚至要会敲代码,可以这么做,但并不符合实际使用场景。

3. 代理模式的优缺点

优点

1)良好的扩展性。修改被代理角色并不影响调用者使用代理,对于调用者,被代理角色是透明的。
2)隔离,降低耦合度。代理角色协调调用者和被代理角色,被代理角色只需实现本身关心的业务,非自己本职的业务通过代理处理和隔离。

缺点

1)增加了代理类,实现需要经过代理,因此请求速度会变慢。

4. JS 中的闭包和高阶函数

上面给出的示例代码是通过面向对象的方式实现代理模式,符合 Java 语言的特点。在 JavaScript 中,我们不一定需要通过类去封装,我们知道,JS 是支持函数式编程的,因此我们可以利用函数式编程的思想来实现代理模式。在实现代理模式之前,需要引入两个概念。

高阶函数

在函数式编程中,函数可以可以赋值给一个变量,可以作为参数传入另一个函数,函数也可以作为另一个函数的返回值。在这个意义上,就出现了高阶函数。高阶函数(Higher-Order Function)是至少满足如下条件之一的函数:

  1. 函数可以作为参数被传递;
  2. 函数可以作为返回值输出;

闭包

出现高阶函数之后,闭包也随之出现。

“闭包”可以改变局部变量的生命周期,并且不更改局部变量的作用范围,这一特性使得闭包的运用非常广泛。

5. JS 函数式编程实现代理模式

回到最开始的问题,我们如何基于代理模式优化权限判断的功能?我们可以定义一个高阶函数 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("操作失败!");
	}
  }
}

6. 更进一步

产品同学:我又来改需求了,现在所有操作都要进行校验,有些方法执行前需要确认,有些方法执行后需要提示结果,应该不难吧

加菲猫:额,好吧

在这种情况下,我们不能把所有的逻辑都放到一个代理函数内部,而是需要根据功能拆分出不同的代理函数,然后像搭积木一样组合到一起:

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设计模式

你可能感兴趣的:(Javascript,前端面经,设计模式,vue.js,java,node.js)