JavaScript:为什么在 Addy 的观察者模式中有这么多 abstraction/interfacing?

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

我正在研究 Addy Osmani 的书中观察者模式的设计模式示例,"JavaScript Design Patterns"。我的问题是为什么在他的模式实现中有这么多抽象层次很重要?

例如,在他的示例中,只是为了添加一个观察者('push'一个数组的观察者),这涉及:

下面是设计模式的完整代码示例:

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(){
    // ...
  };
}

这里是 implementation/usage:

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 那样实现了更大程度的解耦?如果是这样,那到底是怎么回事?设计模式本身不会产生解耦吗?似乎 Subject 对象带来了很多不必要的代码。

Couldn't this be implemented more simply by inheriting from the ObserverList object?

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

Does this achieve a greater degree of decoupling doing it the way Addy does?

是的,因为 Subject 对象的接口完全不依赖于 ObserverList 接口(因为 Subject 已经重新实现了它自己的那些方法的接口所以它的接口与 ObserverList 接口分离。这有其优点和缺点。重新实现一个接口应该只有在有充分理由的情况下才能完成,因为它主要只是一堆额外的代码,没有添加任何实际有用的功能。

If so, how exactly is that?

通过重新实现您自己的版本来隐藏 ObserverList 的实际接口,从而分离这两个接口。 Subject 接口可以隐藏对底层 ObserverList 接口的更改。虽然 Subject 实现仍然依赖并耦合到 ObserverList 接口,但 Subject 接口本身独立于 ObserverList 接口。但是,也有很多理由不这样做,所以不要认为每个接口都应该与其他接口分离。随处跟随那将是一场灾难。

Seems like the Subject object brings a lot of unnecessary code.

是的,确实如此。


当您想要使用另一个对象的功能并且想要将部分或全部功能公开给您自己对象的客户时,您有多种设计选择。

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

  2. 您的对象可以包含其他对象的实例并公开该对象 publicly 以便您的对象的用户可以直接访问其他对象,而无需您重新实现任何东西。在这种特殊情况下,这可能是我的选择,因此在 Subject 对象中使用 publicly 可用观察者的代码将如下所示:

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

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

在 OO 语言中,这三个选项有时称为 isA、hasA 和 hidesA。在第一种情况下,您的对象 "is a" ObserverList 对象。在第二种情况下,你的对象 "has a" ObserverList 对象。在第三种情况下,你的对象 "hides a" ObserverList object inside its implementation.


每种设计选择都有利有弊。 None 的选择总是正确或错误的做事方式,因为每个选择都有不同 pros/cons,最佳选择取决于具体情况。

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

选项 2) 包含 ObserverList 的 public 实例并让对象的用户使用该 public 实例的情况是当您的对象实际上是一种不同类型的对象时,但是它想使用另一种对象的功能并向其用户公开。在我看来,这主要是这里发生的事情。

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