前端常见设计模式

1. 外观模式

外观模式(Facade Pattern)是一种结构型设计模式,其主要目的是简化复杂系统的接口并提供一个更高级别的接口以供外部使用。

可以将外观模式想象成一个门面或者外观,类似于房子的门面,它把整个系统隐藏在其背后。对于外部使用者而言,只需要通过门面提供的接口来操作系统,而不需要关心背后的实现细节。

外观模式的一个生动的例子是手机的操作界面。手机的操作界面为用户提供了一个简单易用的接口,可以通过点击屏幕上的图标、按钮来进行操作,但实际上在背后有许多不同的系统组件在协作工作。用户不需要关心这些组件的具体实现,只需要使用操作界面提供的接口即可。

     

很多我们常用的框架和库基本都遵循了外观设计模式,比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已。

(1)兼容浏览器事件绑定

let addMyEvent = function (el, ev, fn) {
    if (el.addEventListener) {
        el.addEventListener(ev, fn, false)
    } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn)
    } else {
        el['on' + ev] = fn
    }
};

(2)封装接口

let myEvent = {
    // ...
    stop: e => {
        e.stopPropagation();
        e.preventDefault();
    }
};

优点

       1.减少系统相互依赖。外观模式可以将客户端和子系统解耦,因为客户端只需要与外观对象交互,而不需要了解底层子系统的实现细节。
       2.提高灵活性。外观模式可以提高系统的灵活性,因为它提供了一个更简单的接口,使得客户端可以更容易地使用系统中的功能。
       3. 提高了安全性。因为外观对象可以控制客户端对子系统的访问,并提供一个安全的接口。
缺点

       1.不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。

例如,如果系统需要增加一个新的子系统,那么需要在外观类中增加一个对应的方法,这样就会修改外观类的代码,从而可能导致客户端的代码也需要修改。

2.代理模式


代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理模式的生动理解可以类比为一个人找代理去购物。这个人想购买一些物品,但是由于某些原因无法亲自前往商店购买,于是他找到了一个代理人去帮他购买。

代理模式中的代理对象就像是这个代理人,它可以代替真实对象进行一些操作。

例子1:

       假设当A 在心情好的时候收到花,小明表白成功的几率有60%,而当A 在心情差的时候收到花,小明表白的成功率无限趋近于0。小明跟A 刚刚认识两天,还无法辨别A 什么时候心情好。如果不合时宜地把花送给A,花被直接扔掉的可能性很大,这束花可是小明吃了7 天泡面换来的。但是A 的朋友B 却很了解A,所以小明只管把花交给B,B 会监听A 的心情变化,然后选择A 心情好的时候把花转交给A,代码如下:

let Flower = function () {}
    let xiaoming = {
        sendFlower: function (target) {
            let flower = new Flower();
            target.receiveFlower(flower)
        }
    }
    let B = {
        receiveFlower: function (flower) {
            A.listenGoodMood(function () {
                A.receiveFlower(flower)
            })
        }
    }
    let A = {
        receiveFlower: function (flower) {
            console.log('收到花', flower)
        },
        listenGoodMood: function (fn) {
            setTimeout(function () {
                fn()
            }, 1000)
    }
   }
   xiaoming.sendFlower(B);

 例子2:

           HTML元 素事件代理

<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
<script>
  let ul = document.querySelector('#ul');
  ul.addEventListener('click', event => {
    console.log(event.target);
  });
</script>

例子3:
        假设有一个图片加载的场景,可以使用代理模式来实现图片的懒加载。具体地,当用户滚动页面时,只加载可视区域内的图片,而不加载整个页面的所有图片。这样可以减少网络请求,提高页面加载速度。


<div id="container">
  <img src="placeholder.jpg" data-src="image1.jpg" alt="image1">
  <img src="placeholder.jpg" data-src="image2.jpg" alt="image2">
  <img src="placeholder.jpg" data-src="image3.jpg" alt="image3">
  <img src="placeholder.jpg" data-src="image4.jpg" alt="image4">
  <img src="placeholder.jpg" data-src="image5.jpg" alt="image5">
</div>

        

// js部分
function lazyLoadImage(image) {
  // 模拟图片加载
  image.src = image.dataset.src;
  image.removeAttribute('data-src');
}

const imageContainer = document.querySelector('#container');
const imageList = imageContainer.querySelectorAll('img');

// 使用代理模式
const lazyLoadImageProxy = new Proxy(lazyLoadImage, {
  apply: function (target, thisArg, args) {
    const image = args[0];
    const rect = image.getBoundingClientRect();
    // 只加载可视区域内的图片
    if (rect.top < window.innerHeight) {
      return target.apply(thisArg, args);
    }
  }
});


        for (let i = 0; i < imageList.length; i++) {
            imageList[i].addEventListener('load', function () {
                // 确保图片加载后再添加代理
                this.removeEventListener('load', arguments.callee);
                const imageProxy = new Proxy(lazyLoadImageProxy, {
                    get: function (target, property) {
                        if (property === 'src') {
                            // 先显示占位图
                            this.setAttribute('src', 'placeholder.jpg');
                            // 再使用代理加载图片
                            return target;
                        } else {
                            return target[property];
                        }
                    }
                });
                imageProxy(this);
            });
        }

  
在这个场景中,代理对象可以负责判断图片是否在可视区域内,如果在可视区域内,就调用实际对象加载图片;如果不在可视区域内,就不加载图片。同时,代理对象还可以在图片加载完成后,缓存图片的信息,以便下次使用。这样可以提高用户的体验,同时也减轻了服务器的负担。


优点

  •  代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用

 

  •  代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;

缺点

  • 处理请求速度可能有差别,非直接访问存在开销。

3.工厂模式

      工厂模式(Frontend Factory Pattern)是一种创建对象的设计模式,它可以用于解决对象创建时的复杂度和重复性问题。通过工厂模式,我们可以将对象的创建封装到一个工厂函数中,从而简化对象创建的过程。

假设有一个汽车生产工厂,工厂里有多条生产线,每条生产线都有自己的特定功能,比如制造车身、组装发动机、安装座椅等。当有客户需要购买汽车时,工厂会根据客户的要求,在不同的生产线上生产出不同的汽车,最终交给客户。

通常来说,工厂模式包含以下几个角色:

  1. 工厂函数(Factory):一个用于创建其他对象的函数,它负责创建对象并返回。

  2. 抽象接口(Interface):定义了工厂函数创建对象所需的基本结构和属性。

  3. 具体实现(Concrete):实现抽象接口定义的基本结构和属性,同时也可以包含其他的自定义属性和方法。

// 定义抽象接口
class Button {
  constructor(text) {
    this.text = text;
  }
  render() {
    console.log(`Rendering ${this.text} button...`);
  }
}

// 定义具体实现
class SubmitButton extends Button {
  constructor() {
    super('Submit');
    this.disabled = false;
  }
  render() {
    console.log(`Rendering ${this.text} button...`);
    console.log(`Disabled: ${this.disabled}`);
  }
}

// 定义工厂函数
function createButton(type) {
  switch(type) {
    case 'submit':
      return new SubmitButton();
    default:
      return new Button('Default');
  }
}

// 创建按钮实例
const myButton = createButton('submit');
myButton.render(); // 输出 "Rendering Submit button... Disabled: false"

        

场景

  • 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
  • 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
  • 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性。

优点

  • 创建对象的过程可能很复杂,但我们只需要关心创建结果。
  • 构造函数和创建者分离, 符合“开闭原则”。
  • 一个调用者想创建一个对象,只要知道其名称就可以了。
  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。

缺点

  • 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度。
  • 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度。

4.单例模式

单例模式是一种创建型设计模式

顾名思义,单例模式中Class的实例个数最多为1。当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。

想象一下,你正在编写一个游戏,需要一个全局的游戏管理器,它负责处理所有游戏相关的事务,例如:渲染游戏画面、处理用户输入、处理游戏逻辑等等。为了确保只有一个游戏管理器实例,并且在全局范围内能够访问它,你可以使用单例模式来实现这一点

class GameManager {
  constructor() {
    if (GameManager.instance) {
      return GameManager.instance;
    }
    this.score = 0;
    GameManager.instance = this;
  }
  
  static getInstance() {
    return GameManager.instance || new GameManager();
  }
  
  addScore(points) {
    this.score += points;
    console.log(`Score: ${this.score}`);
  }
}

// 使用单例
const gameManager1 = new GameManager();
const gameManager2 = GameManager.getInstance();
gameManager1.addScore(10); // Score: 10
gameManager2.addScore(20); // Score: 30
console.log(gameManager1 === gameManager2); // true

实现单例模式需要解决以下几个问题:

  • 如何确定Class只有一个实例?
  • 如何简便的访问Class的唯一实例?
  • Class如何控制实例化的过程?
  • 如何将Class的实例个数限制为1?

我们一般通过实现以下两点来解决上述问题:

  • 隐藏Class的构造函数,避免多次实例化。
  • 通过暴露一个 getInstance() 方法来创建/获取唯一实例。

Javascript中单例模式可以通过以下方式实现:

// 单例构造器
const FooServiceSingleton = (function () {
  // 隐藏的Class的构造函数
  function FooService() {}

  // 未初始化的单例对象
  let fooService;

  return {
    // 创建/获取单例对象的函数
    getInstance: function () {
      if (!fooService) {
        fooService = new FooService();
      }
      return fooService;
    }
  }
})();

实现的关键点有:

  • 使用 IIFE创建局部作用域并即时执行;IIFE 是 Immediately Invoked Function Expression 的缩写,指的是立即调用的函数表达式。
  • getInstance() 为一个 闭包 ,使用闭包保存局部作用域中的单例对象并返回。

我们可以验证下单例对象是否创建成功:

const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();

console.log(fooService1 === fooService2); // true

场景例子

  • 定义命名空间和实现分支型方法 。为了避免变量名冲突和代码组织混乱,我们经常使用命名空间和单例模式
var myNamespace = myNamespace || {};

myNamespace.Singleton = (function() {
  // 私有属性和方法
  var privateProperty = 'Hello World';
  function privateMethod() {
    console.log(privateProperty);
  }
  
  // 创建单例对象
  var instance;
  function init() {
    // 公共方法
    function publicMethod() {
      console.log('The public can see me!');
      privateMethod();
    }
    
    // 公共属性
    var publicProperty = 'I am also public';
    
    // 返回公共接口
    return {
      publicMethod: publicMethod,
      publicProperty: publicProperty
    };
  }
  
  // 返回单例对象
  return {
    getInstance: function() {
      if (!instance) {
        instance = init();
      }
      return instance;
    }
  };
})();

// 使用单例对象
var singleA = myNamespace.Singleton.getInstance();
var singleB = myNamespace.Singleton.getInstance();
console.log(singleA === singleB); // true
singleA.publicMethod(); // The public can see me! Hello World
  • 登录框
var LoginModal = (function() {
  var instance;

  function init() {
    // 创建登录框 DOM 元素
    var loginModal = document.createElement('div');
    loginModal.innerHTML = '<h2>Login</h2><form><input type="text" placeholder="Username"><br><input type="password" placeholder="Password"><br><input type="submit" value="Log in"></form>';
    document.body.appendChild(loginModal);

    // 绑定登录表单的提交事件
    var loginForm = loginModal.querySelector('form');
    loginForm.addEventListener('submit', function(event) {
      event.preventDefault();
      var username = loginForm.querySelector('input[type="text"]').value;
      var password = loginForm.querySelector('input[type="password"]').value;
      console.log('username: ' + username + ', password: ' + password);
      // TODO: 发送登录请求
    });

    // 返回登录框 DOM 元素
    return loginModal;
  }

  return {
    getInstance: function() {
      if (!instance) {
        instance = init();
      }
      return instance;
    }
  };
})();

// 使用登录框单例对象
var loginModalA = LoginModal.getInstance();
var loginModalB = LoginModal.getInstance();
console.log(loginModalA === loginModalB); // true
  • vuex 和 redux中的store

优点

  • 划分命名空间,减少全局变量
  • 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
  • 且只会实例化一次。简化了代码的调试和维护

缺点

  • 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合
  • 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一 个单元一起测试。

5. 策略模式

策略模式是一种行为型设计模式

策略模式简单描述就是:对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。把它们一个个封装起来,并且使它们可以互相替换。

可以想象一下选择支付方式的场景。假设有三种支付方式:支付宝、微信和银行卡。在支付过程中,用户需要选择一种支付方式,然后根据所选的支付方式来进行支付操作。在这个场景中,支付方式就是算法,而选择支付方式的过程就是策略。

<html>
<head>
    <title>策略模式-校验表单</title>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
    <form id = "registerForm" method="post" action="http://xxxx.com/api/register">
        用户名:<input type="text" name="userName">
        密码:<input type="text" name="password">
        手机号码:<input type="text" name="phoneNumber">
        <button type="submit">提交</button>
    </form>
    <script type="text/javascript">
        // 策略对象
        const strategies = {
          isNoEmpty: function (value, errorMsg) {
            if (value === '') {
              return errorMsg;
            }
          },
          isNoSpace: function (value, errorMsg) {
            if (value.trim() === '') {
              return errorMsg;
            }
          },
          minLength: function (value, length, errorMsg) {
            if (value.trim().length < length) {
              return errorMsg;
            }
          },
          maxLength: function (value, length, errorMsg) {
            if (value.length > length) {
              return errorMsg;
            }
          },
          isMobile: function (value, errorMsg) {
            if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
              return errorMsg;
            }                
          }
        }
        
        // 验证类
        class Validator {
          constructor() {
            this.cache = []
          }
          add(dom, rules) {
            for(let i = 0, rule; rule = rules[i++];) {
              let strategyAry = rule.strategy.split(':')
              let errorMsg = rule.errorMsg
              this.cache.push(() => {
                let strategy = strategyAry.shift()
                strategyAry.unshift(dom.value)
                strategyAry.push(errorMsg)
                return strategies[strategy].apply(dom, strategyAry)
              })
            }
          }
          start() {
            for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
              let errorMsg = validatorFunc()
              if (errorMsg) {
                return errorMsg
              }
            }
          }
        }

        // 调用代码
        let registerForm = document.getElementById('registerForm')

        let validataFunc = function() {
          let validator = new Validator()
          validator.add(registerForm.userName, [{
            strategy: 'isNoEmpty',
            errorMsg: '用户名不可为空'
          }, {
            strategy: 'isNoSpace',
            errorMsg: '不允许以空白字符命名'
          }, {
            strategy: 'minLength:2',
            errorMsg: '用户名长度不能小于2位'
          }])
          validator.add(registerForm.password, [ {
            strategy: 'minLength:6',
            errorMsg: '密码长度不能小于6位'
          }])
          validator.add(registerForm.phoneNumber, [{
            strategy: 'isMobile',
            errorMsg: '请输入正确的手机号码格式'
          }])
          return validator.start()
        }

        registerForm.onsubmit = function() {
          let errorMsg = validataFunc()
          if (errorMsg) {
            alert(errorMsg)
            return false
          }
        }
    </script>
</body>
</html>

场景例子

  • 如果在一个系统里面有许多类,它们之间的区别仅在于它们的'行为',那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
  • 一个系统需要动态地在几种算法中选择一种。
  • 表单验证

优点

  • 利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
  • 提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展
  • 利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案

缺点

  • 会在程序中增加许多策略类或者策略对象
  • 要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy

6.迭代器模式

迭代器模式是一种行为型设计模式,用于提供一种顺序访问聚合对象元素的方法,而不需要了解聚合对象的内部结构。这种模式在许多编程语言中被广泛使用,例如 JavaScript 中的迭代器。

以 JavaScript 中的数组为例,我们可以使用 for 循环遍历数组的每个元素,如:

const arr = [1, 2, 3, 4, 5];
for(let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

上述代码中的 for 循环就是一种迭代器的实现方式。它提供了一个统一的接口来访问数组中的每个元素,并隐藏了数组内部的实现细节。另外,我们还可以使用其他迭代器实现方式,例如 forEachmapfilter 等等。这些迭代器都提供了一种简洁易懂、易于维护的访问方式。

迭代器模式的优点在于它能够将遍历与聚合分离,使得聚合对象的实现更加简单和灵活,同时也使得迭代算法更加易于重用和修改。

迭代器模式解决了以下问题:

  • 提供一致的遍历各种数据结构的方式,而不用了解数据的内部结构
  • 提供遍历容器(集合)的能力而无需改变容器的接口

一个迭代器通常需要实现以下接口:

  • hasNext():判断迭代是否结束,返回Boolean
  • next():查找并返回下一个元素

为Javascript的数组实现一个迭代器可以这么写:

const item = [1, 'red', false, 3.14];

function Iterator(items) {
  this.items = items;
  this.index = 0;
}

Iterator.prototype = {
  hasNext: function () {
    return this.index < this.items.length;
  },
  next: function () {
    return this.items[this.index++];
  }
}

验证一下迭代器是否工作:

const iterator = new Iterator(item);

while(iterator.hasNext()){
  console.log(iterator.next());
}
//输出:1, red, false, 3.14

ES6提供了更简单的迭代循环语法 for...of,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个Key为 Symbol.iterator 的方法,该方法返回一个iterator对象。

比如我们实现一个 Range 类用于在某个数字区间进行迭代:

function Range(start, end) {
  return {
    [Symbol.iterator]: function () {
      return {
        next() {
          if (start < end) {
            return { value: start++, done: false };
          }
          return { done: true, value: end };
        }
      }
    }
  }
}

验证一下:

for (num of Range(1, 5)) {
  console.log(num);
}
// 输出:1, 2, 3, 4

7. 观察者模式

观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会自动得到通知并更新自己的状态。

一个生动的例子是电视台与观众之间的关系。电视台是被观察者,观众是观察者。当电视台播出新节目时,所有观众都会收到通知,并根据自己的兴趣决定是否收看该节目。同样的,当电视台取消或改变节目时,所有观众也会得到通知并相应地做出调整。

观察者模式中Subject(被观察对象)对象一般需要实现以下API:

  • subscribe(): 接收一个观察者observer对象,使其订阅自己
  • unsubscribe(): 接收一个观察者observer对象,使其取消订阅自己
  • fire(): 触发事件,通知到所有观察者

用JavaScript手动实现观察者模式:

// 被观察者
function Subject() {
  this.observers = [];
}

Subject.prototype = {
  // 订阅
  subscribe: function (observer) {
    this.observers.push(observer);
  },
  // 取消订阅
  unsubscribe: function (observerToRemove) {
    this.observers = this.observers.filter(observer => {
      return observer !== observerToRemove;
    })
  },
  // 事件触发
  fire: function () {
    this.observers.forEach(observer => {
      observer.call();
    });
  }
}

验证一下订阅是否成功:

const subject = new Subject();

function observer1() {
  console.log('Observer 1 Firing!');
}


function observer2() {
  console.log('Observer 2 Firing!');
}

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.fire();

//输出:
Observer 1 Firing! 
Observer 2 Firing!

验证一下取消订阅是否成功:

subject.unsubscribe(observer2);
subject.fire();

//输出:
Observer 1 Firing!

场景

  • DOM事件
document.body.addEventListener('click', function() {
    console.log('hello world!');
});
document.body.click()
  • vue 响应式

优点

  • 支持简单的广播通信,自动通知所有已经订阅过的对象
  • 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
  • 增加了灵活性
  • 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。

缺点

过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解

8. 中介者模式

  • 在中介者模式中,中介者(Mediator)包装了一系列对象相互作用的方式,使得这些对象不必直接相互作用,而是由中介者协调它们之间的交互,从而使它们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用,保证这些作用可以彼此独立的变化。
  • 中介者模式和观察者模式有一定的相似性,都是一对多的关系,也都是集中式通信,不同的是中介者模式是处理同级对象之间的交互,而观察者模式是处理Observer和Subject之间的交互。中介者模式有些像婚恋中介,相亲对象刚开始并不能直接交流,而是要通过中介去筛选匹配再决定谁和谁见面。

场景

例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。

var goods = {   //手机库存
    'red|32G': 3,
    'red|64G': 1,
    'blue|32G': 7,
    'blue|32G': 6,
};
//中介者
var mediator = (function() {
    var colorSelect = document.getElementById('colorSelect');
    var memorySelect = document.getElementById('memorySelect');
    var numSelect = document.getElementById('numSelect');
    return {
        changed: function(obj) {
    switch(obj){
        case colorSelect:
            //TODO 更新商品颜色信息
            break;
        case memorySelect:
            //TODO 更新商品内存信息
            break;
        case numSelect:
            //TODO 更新商品数量信息
            break;
    }
}
        }
    }
})();
colorSelect.onchange = function() {
    mediator.changed(this);
};
memorySelect.onchange = function() {
    mediator.changed(this);
};
numSelect.onchange = function() {
    mediator.changed(this);
};
  • 聊天室里

聊天室成员类:

function Member(name) {
  this.name = name;
  this.chatroom = null;
}

Member.prototype = {
  // 发送消息
  send: function (message, toMember) {
    this.chatroom.send(message, this, toMember);
  },
  // 接收消息
  receive: function (message, fromMember) {
    console.log(`${fromMember.name} to ${this.name}: ${message}`);
  }
}
聊天室类:
function Chatroom() {
  this.members = {};
}

Chatroom.prototype = {
  // 增加成员
  addMember: function (member) {
    this.members[member.name] = member;
    member.chatroom = this;
  },
  // 发送消息
  send: function (message, fromMember, toMember) {
    toMember.receive(message, fromMember);
  }
}
测试一下:
const chatroom = new Chatroom();
const bruce = new Member('bruce');
const frank = new Member('frank');

chatroom.addMember(bruce);
chatroom.addMember(frank);

bruce.send('Hey frank', frank);

//输出:bruce to frank: hello frank

优点

  • 使各对象之间耦合松散,而且可以独立地改变它们之间的交互
  • 中介者和对象一对多的关系取代了对象之间的网状多对多的关系
  • 如果对象之间的复杂耦合度导致维护很困难,而且耦合度随项目变化增速很快,就需要中介者重构代码

缺点

系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。

9. 访问者模式

访问者模式(Visitor Pattern)是一种行为型设计模式,它的目的是封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。

假设我们正在开发一个旅游网站,我们的网站有许多旅游景点的介绍页面。我们想要为我们的用户提供一种收集信息的功能,可以让用户查看他们访问过的所有景点的总票价。

我们可以通过访问者模式来实现这一功能。我们可以将每个景点都表示为一个对象,每个景点对象都有一个 accept(visitor) 方法,允许访问者访问景点,并计算票价。我们可以定义一个名为 Visitor 的接口,然后实现一个 TicketPriceVisitor 类,它可以对每个景点进行价格计算并返回票价。

// 定义 Visitor 接口
class Visitor {
  visit(element) {
    throw new Error('visit method must be implemented');
  }
}

// 景点类
class SightseeingLocation {
  constructor(name, ticketPrice) {
    this.name = name;
    this.ticketPrice = ticketPrice;
  }

  accept(visitor) {
    return visitor.visit(this);
  }
}

// 票价访问者类
class TicketPriceVisitor extends Visitor {
  constructor() {
    super();
    this.totalPrice = 0;
  }

  visit(location) {
    this.totalPrice += location.ticketPrice;
  }
}

// 创建景点对象
const beijing = new SightseeingLocation('北京', 100);
const shanghai = new SightseeingLocation('上海', 80);
const guilin = new SightseeingLocation('桂林', 120);
const hangzhou = new SightseeingLocation('杭州', 90);

// 计算总票价
const visitor = new TicketPriceVisitor();
beijing.accept(visitor);
shanghai.accept(visitor);
guilin.accept(visitor);
hangzhou.accept(visitor);

console.log(`总票价:${visitor.totalPrice} 元`);
// 访问者  
class Visitor {
    constructor() {}
    visitConcreteElement(ConcreteElement) {
        ConcreteElement.operation()
    }
}
// 元素类  
class ConcreteElement{
    constructor() {
    }
    operation() {
       console.log("ConcreteElement.operation invoked");  
    }
    accept(visitor) {
        visitor.visitConcreteElement(this)
    }
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
elementA.accept(visitor)

访问者模式的实现有以下几个要素:

  • Visitor Object:访问者对象,拥有一个visit()方法
  • Receiving Object:接收对象,拥有一个 accept() 方法
  • visit(receivingObj):用于Visitor接收一个Receiving Object
  • accept(visitor):用于Receving Object接收一个Visitor,并通过调用Visitor的 visit() 为其提供获取Receiving Object数据的能力
Receiving Object:

function Employee(name, salary) {
  this.name = name;
  this.salary = salary;
}

Employee.prototype = {
  getSalary: function () {
    return this.salary;
  },
  setSalary: function (salary) {
    this.salary = salary;
  },
  accept: function (visitor) {
    visitor.visit(this);
  }
}
Visitor Object:

function Visitor() { }

Visitor.prototype = {
  visit: function (employee) {
    employee.setSalary(employee.getSalary() * 2);
  }
}

验证一下:

const employee = new Employee('bruce', 1000);
const visitor = new Visitor();
employee.accept(visitor);

console.log(employee.getSalary());//输出:2000

场景

对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作

需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。

优点

  • 符合单一职责原则
  • 优秀的扩展性
  • 灵活性

缺点

  • 具体元素对访问者公布细节,违反了迪米特原则
  • 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
  • 具体元素变更比较困难

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值