乐趣区

设计模式JavaScript实现

GoF 合作出版的《设计模式》这本书提供了许多有关与面向对象软件设计中常见问题的解决方案。这些模式已经出现了相当长的一段时间,已经被证明在许多情况下都非常有用。

单体模式

一个特定类仅有一个实例。这意味着当您第二次使用同一个类创建新对象的时候,应该得到与第一次所创建对象完全相同对象。

使用对象字面量创建一个简单的对象也是一个单体的例子, 因为在 JavaScript 中没有类,只有对象。当您创建一个新对象时,实际上没有其他对象与其类似,因此新对象已经是单体了。

var obj = {myprop: 'my value'};

使用 new 操作符

JavaScript 没有类,但是可以通过 new 语法使用构造函数来创建对象,有时有可能需要使用这种语法的单体实现。这种思想在于当使用同一个构造函数以 new 操作符来创建多个对象时,应该仅获得指向完全相同的对象的新指针。

静态属性中的实例

在构造函数的静态属性中缓存该实例。您可以使用类似 Universe.instance 的属性并将实例缓存在该属性中。这中方案的缺点在于 instance 属性是公开可访问的属性,在外部代码中可能会修改该属性。

function Universe() {if (typeof Universe.instance === 'object') {return Universe.instance;}

  this.start_time = 0;

  Universe.instance = this;
}

// 测试
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true

闭包中的实例

可以将该实例包装在闭包中。这样可以保证该实例的私有性。其代价是带来了额外的闭包开销。

function Universe() {
  var instance = this;

  this.start_time = 0;

  Universe = function () {return this;};
}

如果需要使原型和构造函数指针按照预期的那样运行,可以通过做一些调整来实现这个目标:

function Universe() {
  var instance;

  Universe = function Universe() {return instance;};

  Universe.prototype = this;

  instance = new Universe();

  instance.constructor = Universe;

  instance.start_time = 0;

  return instance;
}

另一种解决方案也是将构造函数和实例包装在即时函数中。

var Universe;

(function () {
  var instance;

  Universe = function Universe() {if (instance) {return instance;}
    instance = this;

    this.start_time = 0;
  };
}());

工厂模式

设计工厂模式的目的是为了创建对象。它通常在类或者类的静态方法中实现,具有下列目标:

  1. 当创建相似对象时执行重复操作
  2. 在编译时不知道具体类型 (类) 的情况下,为工厂客户提供一种创建对象的接口。

通过工厂方法(或类)创建的对象在设计上都继承了相同的父对象这个思想,它们都是实现专门功能的特定子类。有时候公共父类是一个包含了工厂方法的同一个类。

下面是工厂模式的实现示例

function CarMaker() {}

CarMaker.prototype.drive = function () {return this.doors;};

CarMaker.factory = function (type) {
  var constr = type,
      newcar;
  
  if (typeof CarMaker[constr] !== 'function') {
    throw {
      name: "Error",
      message: constr + "not exist"
    };
  }
  if (typeof CarMaker[constr].prototype.drive !== 'function') {CarMaker[constr].prototype = new CarMaker();}
  newcar = new CarMaker[constr]();
  return newcar;
};

CarMarker.Compact = function () {this.doors = 4;};
CarMarker.Convertible = function () {this.doors = 2;};
CarMarker.SUV = function () {this.doors = 24;};

内置对象工厂

var o = new Object(),
    n = new Object(),
    s = Object('1'),
    b = Object(true);

o.constructor === Object;
n.constructor === Number;
s.constructor === String;
b.constructor === Boolean;
// 都为 true

迭代器模式

在迭代器模式中,通常有一个包含某种数据集合的对象。该数据可能存储在一个复杂数据结构内部,而要提供一种简单的方法能够访问数据结构中每个元素。对象的消费者并不需要知道如何组织数据,所有需要做的就是取出单个数据进行工作。

在迭代器模式中,对象需要提供一个 next()方法。依次调用 next()必须返回下一个连续的元素。当然,在特定数据结构中,” 下一个 ” 所代表的意义是由您来决定的。

示例

var agg = (function () {
  var index = 0,
      data = [1, 2, 3, 4, 5],
      length = data.length;

  return {next: function () {
      var element;
      if (!this.hasNext()) {return null;}
      element = data[index];
      index = index + 2;
      return element;
    },
    hasNext: function () {return index < length;}
  };
}());

装饰者模式

在装饰者模式中,可以在运行时动态添加附加功能到对象中。装饰者模式的一个比较方便的特征在于其预期行为的可定制和可配置特性。可以从仅具有一些基本功能的普通对象开始,然后从可用装饰资源池中选择需要用于增强普通对象的那些功能,并且按照顺序进行装饰,尤其是当装饰顺序很重要的时候。

通过原型链继承实现

function Sale(price) {this.price = price || 100;}
Sale.prototype.getPrice = function () {return this.price;};

Sale.decorators.fedtax = {getPrice: function () {var price = this.uber.getPrice();
    price += price * 5 / 100;
    return price;
  }
};

Sale.decorators.quebec = {getPrice: function () {var price = this.uber.getPrice();
    price += price * 7.5 / 100;
    return price;
  }
};

Sale.prototype.decorate = function(decorator) {var F = function () {},
      overrides = this.constructor.decorators[decorator],
      i, newobj;
  F.prototype = this;
  newobj = new F();
  newobj.uber = F.prototype;
  for (i in overrides) {if (overrides.hasOwnProperty(i)) {newobj[i] = overrides[i];
    }
  }
  return newobj;
};

// 测试
var sale = new Sale(100);
sale = sale.decorate('fedtax');
sale = sale.decorate('quebec');
sale.getPrice();

使用列表实现


function Sale(price) {this.price = (price > 0) || 100;
  this.decorators_list = [];}

Sale.decorators = {};
Sale.decorators.fedtax = {getPrice: function (price) {return price + price * 5 / 100;}
};
Sale.decorators.quebec = {getPrice: function (price) {return price + price * 7.5 / 100;}
};

Sale.prototype.decorate = function (decorator) {this.decorators_list.push(decorator);
};

Sale.prototype.getPrice = function () {
  var price = this.price,
      i,
      max = this.decorators_list.length,
      name;
  for (i = 0; i < max; i += 1) {name = this.decorators_list[i];
    price = Sale.decorators[name].getPrice(price);
  }
  return price;
}

// 测试
var sale = new Sale(100);
sale.decorate('fedtax');
sale.decorate('quebec');
sale.getPrice();

策略模式

策略模式支持您在运行时选择算法。代码的客户端可以使用同一个接口来工作,但是它却根据客户正在试图执行任务的上下文,从多个算法中选择用于处理特定任务的算法。

数据验证示例


var validator = {types: {},

  messages: [],

  config: {},

  validate: function (data) {
    var i, msg, type, checker, result_ok;

    this.messages = [];

    for (i in data) {if (data.hasOwnProperty(i)) {type = this.config[i];
        checker = this.types[type];

        if (!type) {continue;}
        if (!checker) {
          throw {
            name: 'ValidationError',
            message: 'No handler to validate type' + type;
          };
        }

        result_ok = checker.validate(data[i]);
        if (!result_ok) {
          msg = "Invalid value for *" + i + "*," + checker.instructions;
          this.messages.push(msg);
        }
      }
    }
    return this.hasErrors();},
  hasErrors: function () {return this.message.length !== 0;}


};

validator.types.isNonEmpty = {validate: function (value) {return value !== "";},
  instructions: "this value cannot be empty"
};

validator.types.isNumber = {validate: function (value) {return !isNaN(value);
  },
  instructions: "this value can only be a valid number, e.g. 1, 3.14 or 2010"
};

// 测试
var data = {
  first_name: "Super",
  age: "unknown",
};
validator.config = {
  first_name: 'isNonEmpty',
  age: 'isNumber',
};
validator.validate(data);
if (validator.hasErrors()) {console.log(validator.message.join("\n"));
}

外观模式

外观模式是一种简单的模式,它为对象提供了一个可供选择的接口。这是一种非常好的设计实践,可保持方法的简洁性并且不会使它们处理过多的工作。如果原来有许多接受多个参数的 uber 方法,相比而言,按照本实现方法,最终将会创建更多数量的方法。有时候,两个或更多的方法可能普遍的被一起调用。在这样的情况下,创建另一个方法以包装重复的方法调用是非常有意义的。

外观模式非常适合于浏览器脚本处理,据此可将浏览器之间的差异隐藏在外观之后。

var myevent = {stop: function (e) {if (typeof e.preventDefault === 'function') {e.preventDefault();
    }
    if (typeof e.stopPropagation === 'function') {e.stopPropagation();
    }
    if (typeof e.returnValue === 'boolean') {e.returnValue = false;}
    if (typeof e.cancelBubble === 'boolean') {typeof e.cancelBubble = true;}
  }
};

代理模式

在代理设计模式中,一个对象充当另一个对象的接口。代理介于对象的客户端和对象本身之间,并且对该对象的访问进行保护。

使用这种模式的其中一个例子是我们可以称为延迟初始化的方法,代理接收初始化请求,但是直到该本体对象明确的将被实际使用之前,代理从不会将该请求传递给本体对象。

范例(略)

  1. 通过代理合并多个 http 请求以提高性能
  2. 缓存代理

中介者模式

应用程序,无论其大小,都是由一些单个的对象所组成。所有这些对象需要一种方式来实现相互通信,而这种通信方式在一定程度上不降低可维护性,也不损害那种安全的改变部分应用程序而不会破坏其余部分的能力。随着应用程序的增长,将添加越来越多的对象。然后在代码重构期间,对象将被删除或重新整理。当对象相互知道太多信息并且直接通信(调用对方的方法并改变属性)时,这将导致产生不良的紧耦合问题。

中介者模式缓解了该问题并促进形成松耦合,在这种模式中,独立的对象之间并不直接通信,而是通过 mediator 对象。当其中一个 colleague 对象改变状态以后,它将会通知该 mediator,而 mediator 将会把该变化传达到任意其他应该知道此变化的 colleague 对象。

中介者示例

function Player(name) {
  this.points = 0;
  this.name = name;
}

Player.prototype.play = function () {
  this.points += 1;
  mediator.played();}

var scoreboard = {element: document.getElementById('results');

  update: function (score) {
    var i, msg = '';
    for (i in score) {if (score.hasOwnProperty(i)) {msg += i + ':' + score[i] + '|'
      }
    }
    this.element.innerHTML = msg;
  }
};

var mediator = {players: {},
  setup: function () {
    var players = this.players;
    players.home = new Player('Home');
    players.guest = new Player('Guest');
  },
  played: function () {
    var players = this.players,
    score = {
      Home: players.home.points,
      Guest: players.guest.points
    };
    scoreboard.update(score);
  },
  keypress: function (e) {
    e = e || window.event;
    if (e.whitch === 49) {mediator.players.home.play();
      return;
    }
    if (e.whitch === 48) {mediator.players.guest.play();
      return;
    }
  }
};
// 测试
mediator.setup();
window.onkeypress = mediator.keypress;

setTimeout(function () {
  window.onkeypress = null;
  alert('Game over!');
}, 30000);

观察者模式

观察者模式广泛应用于客户端 JavaScript 编程中。所有的浏览器事件是该模式的例子。它也叫订阅 / 发布模式。

设计这种模式背后的主要动机是促进形成松耦合。在这种模式中,并不是一个对象调用另一个对象的方法,而是一个对象订阅另一个对象的特定活动并在状态改变后获得通知。订阅者也称之为观察者,而被观察者的对象称为发布者或者主题。当发生了一个重要的事件时,发布者将会通知 (调用) 所有订阅者并且可能经常以事件对象的形式传递消息。

示例:杂志订阅

我们要实现一个功能:发布者 paper,每天出版报纸以及月刊杂志,订阅者 joe 被通知发生的新闻。

下面是通用发布者功能的一个实现示例

var publisher = {
  subscribers: {any: [] // 事件类型:订阅者(subscribers)
  },
  // 将订阅者添加到 subscribers 数组
  subscribe: function (fn, type) {
    type = type || 'any';
    if (typeof this.subscribers[type] === 'undefined') {this.subscribers[type] = [];}
    this.subscribers[type].push(fn);
  },
  // 从订阅者数组 subscribers 中删除订阅者
  unsubscribe: function (fn, type) {this.visitSubscribers('unsubscribe', fn, type);
  },
  // 循环遍历 subscribers 中的每个元素,并且调用他们注册时提供的方法
  publish: function (publication, type) {this.visitSubscribers('publish', publication, type);
  },
  visitSubscribers: function (action, arg, type) {
    var pubtype = type || 'any',
        subscribers = this.subscribers[pubtype],
        i,
        max = subscribers.length;

    for (i = 0; i < max; i += 1) {if (action === 'publish') {subscribers[i](arg);
      } else {if (subscribers[i] === arg) {subscribers.splice(i, 1);
        }
      }
    }
  }
};

将普通对象转换成发布者对象

function makePublisher(o) {
  var i;
  for (i in publisher) {if (publisher.hasOwnProperty(i) && typeof publisher[i] === 'function') {o[i] = publisher[i];
    }
  }
  o.subscribers = {any: [] };
}

功能实现:

paper 对象

var paper = {daily: function () {this.publish("big news totay");
  },
  monthly: function () {this.publish("interesting analysis", "monthly");
  }
};

将 paper 构造成一个发布者

makePublisher(paper);

订阅者对象 joe

var joe = {drinkCoffee: function (paper) {console.log('Just read' + paper);
  },
  sundayPreNap: function (monthly) {console.log('About to fall asleep reading this' + monthly);
  }
};

joe 向 paper 订阅

paper.subscribe(joe.drinkCoffee);
paper.subscribe(joe.sundayPreNap, 'monthly');

触发一些事件

paper.daily();
paper.monthly();

打印结果

Just read big news totay
About to fall asleep reading this interesting analysis

代码好的部分在于,paper 对象没有硬编码 joe,joe 对象也没有硬编码 paper。此外,代码中也没有那些知道一切的中介者对象。参与对象是松耦合的,我们可以向 paper 添加更多的订阅者而不需要修改这些对象。

让我们将例子更进一步扩展并且让 joe 成为发布者:

makePublisher(joe);
joe.tweet = function (msg) {this.publish(msg);
};

选择 paper 的公关部门需要读取读者的 tweet, 并且订阅 joe 的信息,那么需要提供方法 readTweets():

paper.readTweets = function (tweet) {console.log('Call big meeting! Someone' + tweet);
};
joe.subscribe(paper.readTweets);

执行

joe.tweet('hated the paper today');

打印

Call big meeting! Someone hated the paper today

参考

  1. 《JavaScript 模式》

欢迎微信关注前端阅读室

退出移动版