zoukankan      html  css  js  c++  java
  • 浅析什么是设计模式(套路)、为什么需要设计模式(最优解决方案)、前端常见设计模式(策略模式、发布订阅模式、装饰器模式、适配器模式、职责链模式、代理模式)

    一、什么是设计模式

      官方解释一点就是:模式是一种可复用的解决方案,用于解决软件设计中遇到的常见问题。

      说白了,就是“套路”,举个例子:我们玩游戏,第一关用了半小时,第二关用了一小时,第三关用了两小时,......,然后,你花了一个月练到了满级;

      于是你开始练第二个号,这时候呢,其实你已经知道,每一关的捷径、好的装备在哪里,所以你按照这个套路,很快的,20 天又练满了一个号;

      身边有好友问你怎么这么快的又练了一个号,于是你为了造福大众,你写了一本 闯关攻略 ~。

      通过这个例子,你应该知道,什么是设计模式了吧?烹饪有菜谱,游戏有攻略,干啥都有一些能够让我们达到目标的“套路”,在程序世界,编程的“套路”就是设计模式。当然,如果真要给你个定义,我认为,设计模式就是在软件设计、开发过程中,针对特定问题、场景的更优解决方案。

    二、为什么会有设计模式

      就还是上边的例子,鲁迅先生说过 : “希望是本无所谓有,无所谓无的。这正如地上的路;其实地上本没有路,走的人多了,也便成了路”,设计模式是前辈们针对开发中遇到的问题,提出大家公认且有效的解决方案

    1、为什么需要设计模式?

      可能有小伙伴确实没有用过,或者说用了但不知道这就是设计模式。那么为什么我们需要呢?是因为在我们遇到相似的问题、场景时,能快速找到更优的方式解决。

    2、如何使用

      在 JS 设计模式中,最核心的思想 - 封装变化。怎么理解,比如我们写一个东西,这个东西在初始 v1.0 的时候是这样,到了 v5.0v10.0 甚至 v99.0v100.0 还是这样,你爱怎么写就怎么写,你只要实现就可以了。

    设计模式的核心操作是去观察你整个逻辑里面的变与不变,然后将变与不变分离,达到使变化的部分灵活、不变的地方稳定的目的。

    3、都有啥设计模式 ?

      相信了解的,都知道有 20 多种。挺多的,我不扯那么多,其它的我没用过:什么享元模式、外观模式、生成器模式啥的,今天就主要聊下前端常用的 6 种设计模式。

    三、前端常用的 -  策略模式

    1、问题背景

      我们先来做一个题,很简单的,大家肯定都做过 权限逻辑 判断吧?需求:只用当用户满足以下条件,才能看阿宽的这篇文章。给大家 3min,代码怎么写? “ 呵,你这不是看不起老夫吗?老夫拿起键盘,就是 if-else 梭哈,直接带走,下一个 ! ”

    function checkAuth(data) {
      if (data.role !== 'juejin') {
        console.log('不是掘金用户');
        return false;
      }
      if (data.grade < 1) {
        console.log('掘金等级小于 1 级');
        return false;
      }
      if (data.job !== 'FE') {
        console.log('不是前端开发');
        return false;
      }
      if (data.type !== 'eat melons') {
        console.log('不是吃瓜群众');
        return false;
      }
    }

      相信这段代码,大家都会写,那么这么写,有什么问题 ?

    • checkAuth 函数会爆炸
    • 策略项无法复用
    • 违反开闭原则

      聪明的小伙伴已经知道这里要讲的是什么模式了,对头!这里讲的就是 策略模式。那么什么是策略模式呢 ?

    2、什么是策略模式

      定义 : 要实现某一个功能,有多种方案可以选择。我们定义策略,把它们一个个封装起来,并且使它们可以相互转换。

    策略 + 组合 = 绝配

      我们用策略模式来改造以下这段逻辑

    // 维护权限列表
    const jobList = ['FE', 'BE'];
    
    // 策略
    var strategies = {
      checkRole: function(value) {
        return value === 'juejin';
      },
      checkGrade: function(value) {
        return value >= 1;
      },
      checkJob: function(value) {
        return jobList.indexOf(value) > 1;
      },
      checkEatType: function(value) {
        return value === 'eat melons';
      }
    };

      我们已经写完了策略,接下来要做的就是验证了

    // 校验规则
    var Validator = function() {
      this.cache = [];
    
      // 添加策略事件
      this.add = function(value, method) {
        this.cache.push(function() {
          return strategies[method](value);
        });
      };
    
      // 检查
      this.check = function() {
        for (let i = 0; i < this.cache.length; i++) {
          let valiFn = this.cache[i];
          var data = valiFn(); // 开始检查
          if (!data) {
            return false;
          }
        }
        return true;
      };
    };

      此时,小彭同学需要进行权限验证的条件为:掘金用户、掘金等级 1 级以上,那么代码就可以这么写 :

    // 小彭使用策略模式进行操作
    var compose1 = function() {
      var validator = new Validator();
      const data1 = {
        role: 'juejin',
        grade: 3
      };
      validator.add(data1.role, 'checkRole');
      validator.add(data1.grade, 'checkGrade');
      const result = validator.check();
      return result;
    };

      然后另一个小伙伴阿宽,他可能需要进行权限验证的条件为:掘金用户、前端工程师,那么代码就可以这么写:

    // 阿宽使用策略模式进行操作
    var compose2 = function() {
      var validator = new Validator();
      const data2 = {
        role: 'juejin',
        job: 'FE'
      };
      validator.add(data2.role, 'checkRole');
      validator.add(data2.job, 'checkJob');
      const result = validator.check();
      return result;
    };

      这是不是比一直疯狂写 if-else 好太多了呢?还有什么例子?最常见的表单验证啊 ~ 对于表单字段(名称、密码、邮箱、....)我们可以使用策略模式去设计优化它。

    总结一下:

    1、策略规则:先定义各个策略是怎么样的  ——   策略

    2、校验规则:将所需校验的策略 add 至缓存起来,需要多少个策略就 add 多少策略   ——  组合

    3、校验规则:最后统一 check ,将所有策略均检查,得到一个 统一值 即可   ——  统一检查

    3、什么时候用策略模式?

      当你负责的模块,基本满足以下情况时

    • 各判断条件下的策略相互独立且可复用
    • 策略内部逻辑相对复杂
    • 策略需要灵活组合

    四、前端常用的 -  发布/订阅模式

    1、问题背景

      需求 : 申请成功后,需要触发对应的订单、消息、审核模块对应逻辑。机智如我,我会如何做呢?

    function applySuccess() {
      // 通知消息中心获取最新内容
      MessageCenter.fetch();
      // 更新订单信息
      Order.update();
      // 通知相关方审核
      Checker.alert();
    }

      不就这样写吗,还想咋滴!!!是的,这么写没得毛病,但是呢,我们来思考几个问题:

      比如 MessageCenter.fetch() 是小彭写的,他哪天把模块的方法名改了,现在叫 MessageCenter.request(),你咋办,你这块逻辑改呗~

      再比如,你和阿宽并行开发的,阿宽负责订单模块,你一气呵成写下这段代码,然后一运行,报错了,一询问,发现,原来阿宽昨晚去蹦迪了,原本今天应该完成的订单模块 Order.update(),延迟一天,那你就只能先注释代码,等依赖的模块开发完了,你再回来添加这段逻辑咯~

      更可怕的是,你可能不只是涉及到这三个模块,maybe 还有很多模块,比如你申请成功,现在还需要上报申请日志,你总不能这样写吧?

    function applySuccess() {
      // 通知消息中心获取最新内容
      MessageCenter.fetch();
      // 更新订单信息
      Order.update();
      // 通知相关方审核
      Checker.alert();
    
      // maybe 更多
      Log.write();
      ...
    }

      到这里,我们的 发布-订阅模式 要按捺不住了。

    2、发布 / 订阅模式

      有没有觉得这个 EventEmitter 好熟悉啊,这不是面试常会问的?发布-订阅是一种消息范式,消息的发布者,不会将消息直接发送给特定的订阅者,而是通过消息通道广播出去,然后呢,订阅者通过订阅获取到想要的消息。我们用 发布-订阅模式 修改以下上边的代码:

    const EventEmit = function() {
      this.events = {};
      this.on = function(name, cb) {
        if (this.events[name]) {
          this.events[name].push(cb);
        } else {
          this.events[name] = [cb];
        }
      };
      this.trigger = function(name, ...arg) {
        if (this.events[name]) {
          this.events[name].forEach(eventListener => {
            eventListener(...arg);
          });
        }
      };
    };

      上边我们写好了一个 EventEmit,然后我们的业务代码可以改成这样 ~

    let event = new EventEmit();
    event.trigger('success');
    
    MessageCenter.fetch() {
      event.on('success', () => {
        console.log('更新消息中心');
      });
    }
    Order.update() {
      event.on('success', () => {
        console.log('更新订单信息');
      });
    }
    Checker.alert() {
      event.on('success', () => {
        console.log('通知管理员');
      });
    }

      但是这样就没问题了吗?其实还是有弊端的,比如说,过多的使用发布订阅,就会导致难以维护调用关系。所以,还是看大家的设计吧,这里只是让大家知道,发布订阅模式是个啥~

    3、什么时候用发布-订阅模式?

      当你负责的模块,基本满足以下情况时

    • 各模块相互独立
    • 存在一对多的依赖关系
    • 依赖模块不稳定、依赖关系不稳定
    • 各模块由不同的人员、团队开发

    五、前端常用的 - 装饰器模式

    1、装饰器模式是什么?

      个人理解:是为了给一个函数赋能,增强它的某种能力,它能动态的添加对象的行为,也就是我传入的就是一个对象。在 JS 世界中,世间万物,皆为对象。

      大家过年,都会买桔子树,那么我们买了桔子树之后,都会往上边挂一些红包,摇身一变,“红包桔子树”,牛掰!这个的红包就是装饰器,它不对桔子树原有的功能产生影响。

      再比如 React 中的高阶组件 HOC,了解 React 的都知道,高阶组件其实就是一个函数,接收一个组件作为参数,然后返回一个新的组件。那么我们现在写一个高阶组件 HOC,用它来装饰 Target Component

    import React from 'react';
    const yellowHOC = WrapperComponent => {
      return class extends React.Component {
        render() {
          <div style={{ backgroundColor: 'yellow' }}>
            <WrapperComponent {...this.props} />
          </div>;
        }
      };
    };
    export default yellowHOC;

      定义了一个带有装饰黄色背景的高阶组件,我们用它来装饰目标组件

    import React from 'react';
    import yellowHOC from './yellowHOC';
    
    class TargetComponent extends Reac.Compoment {
      render() {
        return <div>66666</div>;
      }
    }
    
    export default yellowHOC(TargetComponent);

      你看,我们这不就用到了装饰器模式了嘛?什么,你还听不懂?那我最后再举一个例子,不知道这个例子,能不能帮助你们理解

    const kuanWrite = function() {
      this.writeChinese = function() {
        console.log('我只会写中文');
      };
    };
    
    // 通过装饰器给阿宽加上写英文的能力
    const Decorator = function(old) {
      this.oldWrite = old.writeChinese;
      this.writeEnglish = function() {
        console.log('给阿宽赋予写英文的能力');
      };
      this.newWrite = function() {
        this.oldWrite();
        this.writeEnglish();
      };
    };
    
    const oldKuanWrite = new kuanWrite();
    const decorator = new Decorator(oldKuanWrite);
    decorator.newWrite();

    六、前端常用的 - 适配器模式

      个人理解,为了解决我们不兼容的问题,把一个类的接口换成我们想要的接口。

      举个例子, 我想听歌的时候,我发现我没带耳机,我的手机是 iphone 的,而现在我只有一个 Type-C 的耳机,为了能够听歌,我用了一个转换器(也就是适配器),然后我就可以开心的听歌了。

      再举个真实业务中的例子,前段时间需要做一个需求,是这样的。

      看这个图,图中红色方框区域是一个资源列表展示组件,该列表数据,有三处来源:本地上传、资源列表添加、后台返回资源。怎么理解呢?可以看到图中,该流程主要是:

    • 右边的“资源概况”是调接口,返回的一个 MaterialsList ,可以从右边点击 “+” 添加进来
    • 也可以通过选择本地文件上传
    • 如果是编辑场景下,还有后台接口返回的数据

      由于历史原因和之前后台接口返回的数据结构问题,这三个数据格式是不同的。

    // 本地资源文件上传之后的数据结构
    export interface ResourceLocalFileType {
      uuid: string;
      name: string;
      size: number;
      created: number;
      lastModified: number;
      resourceType: number;
      cancel: () => void;
      status: string;
    }
    // 资源概况接口返回的数据结构
    export interface ResourcePackageFileType {
      uuid: string;
      materialName: string;
      materialLink: string;
      materialType: number;
      uid?: string;
      ext?: string;
    }
    // 原先数据后台返回的数据接口
    export interface ResourceBackendFileType {
      uuid: string;
      resourceName: string;
      resourceLink: string;
      resourceType: number;
      version: string;
      ext: string;
    }

      很蛋疼,三个数据来源,三种时候数据结构,我们的资源列表组件是只能接收一种数据格式的列表,我不想破坏纯展示型组件的内部逻辑,想保持该组件的职责:展示!那该怎么处理?采用适配器模式,将不同的数据结构适配成展示组件所能接受的数据结构

      首先,定义一个统一的数据格式:AdapterResourceFileType

    export interface AdapterResourceType {
      uuid: string;
      created: number;
      fileNo: number;
      fileName: string;
      fileOrigin: string;
      fileStatus: string;
      fileInfo: {
        type: number;
        size?: number;
        [key: string]: any;
      };
      // 本地图片额外操作
      action?: {
        cancel?: () => void;
        [key: string]: any;
      };
    }

      然后通过适配器模块,适配成我们需要的接口API。

      在数据进行组件列表展示时,将来源不同的数据经过适配器处理,进行整合,然后传递给展示组件,以达到我们的目的

      适配器就是:为了解决我们不兼容的问题,把一个类的接口换成我们想要的接口,可能不太能帮助理解,我再举个现实业务中的例子。比如你请求一个接口,接口返回 data = { user: xxx, userName: '' },但是你页面中用的是 userInfo 和 nickName,不兼容啊,为了解决和这个不兼容问题,我们写成这样 return { userInfo: data.user, nickName: data.userName } ,这样就将请求接口的字段换成我们想要的接口字段了。

      简言之:适配器不是让你拥有两种能力,装饰器才是给一个函数赋能,增强它的某种能力。适配器是:你给我B,但我只能接受A,需要适配器将B转成我能接受的数据

    七、前端常用的 - 代理模式

    1、什么是代理模式

      我们再来讲一个叫做 代理模式,说到代理哈,我脑海里第一个浮现的词语 : “事件委托、事件代理”,这算吗?算哒。我举些例子,让大家知道代理模式是个啥玩意。作为程序员嘛,女朋友比较难找,就算找到了,咱这么瘦弱,怕是保护不了啊,所以我花钱找了个保镖来保护我,稳妥。这就是代理模式。

      你翻qiang吗?你能 google 吗?老实人哪会什么翻qiang,我是不会的,会我也说我不会。其实正常来讲,我们直接访问 google 是无响应的。那怎么办呢,通过第三方代理服务器。小飞机?懂 ?

      门票都被抢光了,无奈之下,只能找黄牛,这里,黄牛就起了代理的作用,懂?

      程序世界的代理者也是如此,我们不直接操作原有对象,而是委托代理者去进行。代理者的作用,就是对我们的请求预先进行处理或转接给实际对象

    代理模式是为其它对象提供一种代理以控制这个对象的访问,具体执行的功能还是这个对象本身,就比如说,我们发邮件,通过代理模式,那么代理者可以控制,决定发还是不发,但具体发的执行功能,是外部对象所决定,而不是代理者决定。

    // 发邮件,不是qq邮箱的拦截
    const emailList = ['qq.com', '163.com', 'gmail.com'];
    
    // 代理
    const ProxyEmail = function(email) {
      if (emailList.includes(email)) {
        // 屏蔽处理
      } else {
        // 转发,进行发邮件
        SendEmail.call(this, email);
      }
    };
    
    const SendEmail = function(email) {
      // 发送邮件
    };
    
    // 外部调用代理
    ProxyEmail('cvte.com');
    ProxyEmail('ojbk.com');

      下边再来举一个例子,来至 《JavaScript 设计模式与开发实践》

    // 本体
    var domImage = (function() {
      var imgEle = document.createElement('img');
      document.body.appendChild(imgEle);
      return {
        setSrc: function(src) {
          imgEle.src = src;
        }
      };
    })();
    
    // 代理
    var proxyImage = (function() {
      var img = new Image();
      img.onload = function() {
        domImage.setSrc(this.src); // 图片加载完设置真实图片src
      };
      return {
        setSrc: function(src) {
          domImage.setSrc('./loading.gif'); // 预先设置图片src为loading图
          img.src = src;
        }
      };
    })();
    
    // 外部调用
    proxyImage.setSrc('./product.png');

    2、代理模式与装饰器模式区别:

      装饰器模式是给自己添置更高级的装备,增强自己的属性;代理模式是和队友分工合作,把自己不擅长、不想搞的领域交给队友。

      比如说,你很柔弱,你保护不了女朋友,所以你通过健身,拥有一身肌肉。代理模式是,你柔弱,然后花钱请了个有八块腹肌的保镖,但是这个保镖可以决定保护不保护你女朋友。

    如何高效学习 —— 学习新的东西的时候,把它扩展成自己比较熟悉的知识,更容易记忆,而且不容易忘。

    3、什么时候用代理模式?

      当你负责的模块,基本满足以下情况时:

    • 模块职责单一且可复用
    • 两个模块间的交互需要一定限制关系

    八、前端常用的 - 职责链模式

    1、问题背景

      需求 :我们申请设备之后,接下来要选择收货地址,然后选择责任人,而且必须是上一个成功,才能执行下一个。小伙伴们惊讶了,这不简单嘛?

    function applyDevice(data) {
      // 处理巴拉巴拉...
      let devices = {};
      let nextData = Object.assign({}, data, devices);
      // 执行选择收货地址
      selectAddress(nextData);
    }
    
    function selectAddress(data) {
      // 处理巴拉巴拉...
      let address = {};
      let nextData = Object.assign({}, data, address);
      // 执行选择责任人
      selectChecker(nextData);
    }
    
    function selectChecker(data) {
      // 处理巴拉巴拉...
      let checker = {};
      let nextData = Object.assign({}, data, checker);
      // 还有更多
    }

      你看,这不就完事了,有啥难的,然后过了第二天,你又接了两个新的流程需求,可能一个就两步骤,一个可能多了“检查库存”这个步骤

      你不由惊了,哎呀妈呀,老夫聊发少年狂,键盘伺候,Ctrl C + Ctrl V,直接copy然后改一下逻辑??这里就是要讲的责任链模式。

    2、什么是责任链模式呢?

      我给你们找了个定义 : 避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。

    const Chain = function(fn) {
      this.fn = fn;
      this.setNext = function() {}
      this.run = function() {}
    }
    
    const applyDevice = function() {}
    const chainApplyDevice = new Chain(applyDevice);
    
    const selectAddress = function() {}
    const chainSelectAddress = new Chain(selectAddress);
    
    const selectChecker = function() {}
    const chainSelectChecker = new Chain(selectChecker);
    
    // 运用责任链模式实现上边功能
    chainApplyDevice.setNext(chainSelectAddress).setNext(chainSelectChecker);
    chainApplyDevice.run();

      这样的好处是啥?首先是解耦了各节点关系,之前的方式是 A 里边要写 B,B 里边写 C,但是这里不同了,你可以在 B 里边啥都不写

      其次,各节点灵活拆分重组,正如上边你接的两个新需求。比如两个步骤的你就只需要这么写完事

    const applyLincense = function() {}
    const chainApplyLincense = new Chain(applyLincense);
    
    const selectChecker = function() {}
    const chainSelectChecker = new Chain(selectChecker);
    
    // 运用责任链模式实现上边功能
    chainApplyLincense.setNext(chainSelectChecker);
    chainApplyLincense.run();

    3、什么时候使用责任链模式?

      当你负责的模块,基本满足以下情况时

    • 你负责的是一个完整流程,或你只负责流程中的某个环节
    • 各环节可复用
    • 各环节有一定的执行顺序
    • 各环节可重组

      补充一下:不是让大家强行套用设计模式,而是想表达:我们首先需要理解,其次需要形成一种肌肉记忆,正如前边说的策略模式、发布-订阅模式的例子一样,大家在真实开发场景中肯定都有遇到,只是没有想到,原来这就是设计模式,或者说,原来这里可以用到设计模式去设计。

    学习链接:https://juejin.cn/post/6844904138707337229

  • 相关阅读:
    Android Things专题 1.前世今生
    用Power BI解读幸福星球指数
    [leetcode]Simplify Path
    字段的划分完整的问题
    k-means算法MATLAB和opencv代码
    【Oracle】RAC下的一些经常使用命令(一)
    Java中经常使用缓存Cache机制的实现
    jenkins环境自动部署
    jenkins环境搭建
    springboot单元测试@test的使用
  • 原文地址:https://www.cnblogs.com/goloving/p/15587579.html
Copyright © 2011-2022 走看看