JavaScript:为什么Addy's观察者模式

JavaScript: Why so much abstraction/interfacing in Addy's Observer Pattern?

本文关键字:观察者模式 为什么 Addy JavaScript      更新时间:2023-09-26

我正在学习Addy Osmani的书《JavaScript设计模式》中观察者模式的设计模式示例。我的问题是,为什么在他的模式实现中有这么多抽象级别很重要?

例如,在他的例子中,只需要添加一个观测者(将一个观察器"推"到数组中),这涉及到:

  • 使用本地CCD_ 1方法
  • 创建CCD_ 2方法
  • 用CCD_ 4对象继承/扩展CCD_
  • 创建要用作接口的Subject.addObserver()方法,但它在后台使用ObjectList.add方法
  • 针对新对象扩展了CCD_ 7方法
  • 通过在新扩展的对象上调用addObserver()方法来实现它

以下是整个设计模式的代码示例:

function ObserverList(){
  this.observerList = [];
}
ObserverList.prototype.add = function( obj ){
  return this.observerList.push( obj );
};
ObserverList.prototype.count = function(){
  return this.observerList.length;
};
ObserverList.prototype.get = function( index ){
  if( index > -1 && index < this.observerList.length ){
    return this.observerList[ index ];
  }
};
ObserverList.prototype.indexOf = function( obj, startIndex ){
  var i = startIndex;
  while( i < this.observerList.length ){
    if( this.observerList[i] === obj ){
      return i;
    }
    i++;
  }
  return -1;
};
ObserverList.prototype.removeAt = function( index ){
  this.observerList.splice( index, 1 );
};
function Subject(){
  this.observers = new ObserverList();
}
Subject.prototype.addObserver = function( observer ){
  this.observers.add( observer );
};
Subject.prototype.removeObserver = function( observer ){
  this.observers.removeAt( this.observers.indexOf( observer, 0 ) );
};
Subject.prototype.notify = function( context ){
  var observerCount = this.observers.count();
  for(var i=0; i < observerCount; i++){
 this.observers.get(i).update( context );
  }
};
// The Observer
function Observer(){
  this.update = function(){
    // ...
  };
}

以下是实现/用法:

HTML

<button id="addNewObserver">Add New Observer checkbox</button>
<input id="mainCheckbox" type="checkbox"/>
<div id="observersContainer"></div>

编写脚本

// Extend an object with an extension
function extend( extension, obj ){
  for ( var key in extension ){
    obj[key] = extension[key];
  }
}
// References to our DOM elements
var controlCheckbox = document.getElementById( "mainCheckbox" ),
  addBtn = document.getElementById( "addNewObserver" ),
  container = document.getElementById( "observersContainer" );

// Concrete Subject
// Extend the controlling checkbox with the Subject class
extend( new Subject(), controlCheckbox );
// Clicking the checkbox will trigger notifications to its observers
controlCheckbox.onclick = function(){
  controlCheckbox.notify( controlCheckbox.checked );
};
addBtn.onclick = addNewObserver;
// Concrete Observer
function addNewObserver(){
  // Create a new checkbox to be added
  var check  = document.createElement( "input" );
  check.type = "checkbox";
  // Extend the checkbox with the Observer class
  extend( new Observer(), check );
  // Override with custom update behaviour
  check.update = function( value ){
    this.checked = value;
  };
  // Add the new observer to our list of observers
  // for our main subject
  controlCheckbox.addObserver( check );
  // Append the item to the container
  container.appendChild( check );
}

现在,我将他的实现与相同模式的其他实现(书籍和博客)进行了比较。Addy似乎比观察者模式的其他实现者添加了更多的抽象。问题是,为什么?难道不能通过从ObserverList对象继承来更简单地实现这一点吗?这是否像Addy那样实现了更大程度的脱钩?如果是,那是怎么回事?设计模式本身不是造成了脱钩吗?push()0对象似乎带来了很多不必要的代码。

难道不能通过从Observer List对象?

是的。通过继承,就不会重新实现所有的ObserverList方法。大大减少了代码、测试和文档。

这是否实现了更大程度的脱钩做

是的,这样做是因为到Subject对象的接口根本不依赖于ObserverList接口(因为Subject已经重新实现了自己与这些方法的接口,所以它的接口与ObserverList接口解耦。这有其优缺点。重新实现接口应该只有充分的理由,因为它大多只是一堆额外的代码,没有添加实际有用的功能。

如果是,那是怎么回事?

通过重新实现您自己的版本来隐藏ObserverList的实际接口,可以解耦这两个接口。对底层ObserverList接口的更改可以被Subject接口隐藏。虽然Subject的实现仍然依赖并耦合到ObjectList.add()0接口,但Subject接口本身独立于ObserverList接口。但是,也有很多理由不这样做,所以不要认为每个接口都应该与其他接口解耦。如果在任何地方都这样,那将是一场灾难。

似乎Subject对象带来了很多不必要的代码。

是的,确实如此。


当您希望使用另一个对象的功能,并且希望向自己对象的客户公开部分或全部功能时,您有许多设计选择。

  1. 您的对象可以从其他对象继承,从而自动公开其整个接口(并允许您根据需要覆盖某些方法)。

  2. 您的对象可以包含另一个对象的实例,并公开该对象,这样您的对象的用户就可以直接访问另一对象,而无需重新实现任何内容。在这种特殊情况下,这可能是我的选择,因此在Subject对象中使用公开可用的观察器的代码看起来如下:

    var s = new Subject(); s.observer.add(function() { // this gets called when subject is changed });

  3. 您的对象可以包含另一个对象的私有实例,您可以在该私有实例上手动创建自己的接口。这就是你书中的代码正在做的事情。

在OO语言中,这三个选项有时被称为isA、hasA和hidesA。在第一种情况下,您的对象"是"ObserverList对象。在第二种情况下,您的对象"具有"ObserverList对象。在第三种情况下,对象在其实现中"隐藏"了一个ObserverList对象。


每种设计选择都有利弊。没有一种选择总是正确或错误的,因为每种选择都有不同的优缺点,哪种选择是最佳选择取决于情况。

选项1)继承的情况通常是,当您的对象是基础对象的扩展,并且从架构上讲,它被认为只是基础对象的一个更强大的版本,和/或它可能覆盖基础对象上的方法。事实并非如此。Subject()对象并不是更强大的ObserverList对象。这是一种不同类型的对象,恰好使用ObjectList0。

选项2)包含ObserverList的公共实例并允许对象的用户使用该公共实例的情况是,当您的对象确实是不同类型的对象,但它希望使用并向用户公开另一类型对象的功能时。在我看来,这主要是这里正在发生的事情。

选项3)的情况是,您不希望对象的接口与任何其他接口之间存在任何接口依赖关系。在这种情况下,您不能公开其他对象的接口来让您的对象的用户使用这些接口。相反,您必须用自己的接口覆盖任何其他接口。这意味着更多的代码、更多的测试、更多的文档和更多的维护。但是,您正在使用的底层接口的更改并不一定会导致您自己的接口发生更改。您可以选择隐藏界面中的任何潜在更改(以大量工作为代价)。但是,在获得控制权的同时,你还有更多的工作要做。如果ObserverList对象添加了三个新方法,在其他两个选项中,这些方法可以立即提供给您的用户,而无需执行任何新工作,但在选项3)中,您必须为它们创建新的封面方法,并对它们进行测试和记录,然后它们才能提供给客户。