Refactor observable map. (#16)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a1cc51d..3549221 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+## 0.19.0
+
+* Refactor and deprecate `ObservableMap`-specific API
+    * `ObservableMap` no longer emits `#keys` and `#values` change records
+    * `ObservableMap.spy` is deprecated, becomes `.delegate` instead
+* Potentially breaking: `ObservableMap` may no longer be extended
+
+It is also considered deprecated to be notified of `length` changes.
+
 ## 0.18.1
 
 * Bug fix: Do not throw when `Observable<T>.notifyChange` is used
diff --git a/lib/observable.dart b/lib/observable.dart
index a2d1fca..641fa7b 100644
--- a/lib/observable.dart
+++ b/lib/observable.dart
@@ -5,10 +5,9 @@
 library observable;
 
 export 'src/change_notifier.dart' show ChangeNotifier, PropertyChangeNotifier;
-export 'src/collections.dart' show ObservableList;
+export 'src/collections.dart' show ObservableList, ObservableMap;
 export 'src/differs.dart' show Differ, EqualityDiffer, ListDiffer, MapDiffer;
 export 'src/records.dart'
     show ChangeRecord, ListChangeRecord, MapChangeRecord, PropertyChangeRecord;
 export 'src/observable.dart';
-export 'src/observable_map.dart';
 export 'src/to_observable.dart';
diff --git a/lib/src/collections.dart b/lib/src/collections.dart
index 7aaff1d..363e925 100644
--- a/lib/src/collections.dart
+++ b/lib/src/collections.dart
@@ -1 +1,2 @@
 export 'collections/observable_list.dart';
+export 'collections/observable_map.dart';
diff --git a/lib/src/collections/observable_list.dart b/lib/src/collections/observable_list.dart
index 5cc643a..1052df2 100644
--- a/lib/src/collections/observable_list.dart
+++ b/lib/src/collections/observable_list.dart
@@ -10,12 +10,14 @@
 /// efficient to notify _when_ something has changed, instead of constantly
 /// diffing lists to find a single change (like an inserted record). You may
 /// accept an observable list to be notified of mutations:
+///     ```
 ///     set names(List<String> names) {
 ///       clearAndWrite(names);
 ///       if (names is ObservableList<String>) {
 ///         names.listChanges.listen(smallIncrementalUpdate);
 ///       }
 ///     }
+///     ```
 ///
 /// *See [ListDiffer] to manually diff two lists instead*
 abstract class ObservableList<E> implements List<E>, Observable {
diff --git a/lib/src/collections/observable_map.dart b/lib/src/collections/observable_map.dart
new file mode 100644
index 0000000..50fac80
--- /dev/null
+++ b/lib/src/collections/observable_map.dart
@@ -0,0 +1,181 @@
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:collection/collection.dart';
+import 'package:observable/observable.dart';
+
+/// A [Map] that broadcasts [changes] to subscribers for efficient mutations.
+///
+/// When client code expects a read heavy/write light workload, it is often more
+/// efficient to notify _when_ something has changed, instead of constantly
+/// diffing lists to find a single change (like an updated key-value). You may
+/// accept an observable map to be notified of mutations:
+///     ```
+///     set grades(Map<String, int> grades) {
+///       buildBook(grades);
+///       if (names is ObservableMap<String>, int) {
+///         grades.changes.listen(updateBook);
+///       }
+///     }
+///     ```
+///
+/// *See [MapDiffer] to manually diff two lists instead*
+abstract class ObservableMap<K, V> implements Map<K, V>, Observable {
+  /// Creates a new observable map.
+  factory ObservableMap() {
+    return new _ObservableDelegatingMap(new HashMap<K, V>());
+  }
+
+  /// Like [ObservableMap.from], but creates an empty type.
+  factory ObservableMap.createFromType(Map<K, V> other) {
+    ObservableMap<K, V> result;
+    if (other is LinkedHashMap) {
+      result = new ObservableMap<K, V>.linked();
+    } else if (other is SplayTreeMap) {
+      result = new ObservableMap<K, V>.sorted();
+    } else {
+      result = new ObservableMap<K, V>();
+    }
+    return result;
+  }
+
+  /// Create a new observable map using [map] as a backing store.
+  factory ObservableMap.delegate(Map<K, V> map) {
+    return new _ObservableDelegatingMap<K, V>(map);
+  }
+
+  /// Creates a new observable map that contains all entries in [other].
+  ///
+  /// It will attempt to use the same backing map  type if the other map is
+  /// either a [LinkedHashMap], [SplayTreeMap], or [HashMap]. Otherwise it will
+  /// fall back to using a [HashMap].
+  factory ObservableMap.from(Map<K, V> other) {
+    return new ObservableMap<K, V>.createFromType(other)..addAll(other);
+  }
+
+  /// Creates a new observable map using a [LinkedHashMap].
+  factory ObservableMap.linked() {
+    return new _ObservableDelegatingMap<K, V>(new LinkedHashMap<K, V>());
+  }
+
+  /// Creates a new observable map using a [SplayTreeMap].
+  factory ObservableMap.sorted() {
+    return new _ObservableDelegatingMap<K, V>(new SplayTreeMap<K, V>());
+  }
+
+  /// Creates a new observable map wrapping [other].
+  @Deprecated('Use ObservableMap.delegate for API consistency')
+  factory ObservableMap.spy(Map<K, V> other) = ObservableMap<K, V>.delegate;
+}
+
+class _ObservableDelegatingMap<K, V> extends DelegatingMap<K, V>
+    implements ObservableMap<K, V> {
+  final _allChanges = new ChangeNotifier();
+
+  _ObservableDelegatingMap(Map<K, V> map) : super(map);
+
+  // Observable
+
+  @override
+  Stream<List<ChangeRecord>> get changes => _allChanges.changes;
+
+  // ChangeNotifier (deprecated for ObservableMap)
+
+  @override
+  bool deliverChanges() => _allChanges.deliverChanges();
+
+  @override
+  bool get hasObservers => _allChanges.hasObservers;
+
+  @override
+  void notifyChange([ChangeRecord change]) {
+    _allChanges.notifyChange(change);
+  }
+
+  @override
+  /*=T*/ notifyPropertyChange/*<T>*/(
+    Symbol field,
+    /*=T*/
+    oldValue,
+    /*=T*/
+    newValue,
+  ) {
+    if (oldValue != newValue) {
+      _allChanges.notifyChange(
+        new PropertyChangeRecord/*<T>*/(this, field, oldValue, newValue),
+      );
+    }
+    return newValue;
+  }
+
+  @override
+  void observed() {}
+
+  @override
+  void unobserved() {}
+
+  @override
+  operator []=(K key, V newValue) {
+    if (!hasObservers) {
+      super[key] = newValue;
+      return;
+    }
+
+    final oldLength = super.length;
+    V oldValue = super[key];
+    super[key] = newValue;
+
+    if (oldLength != length) {
+      notifyPropertyChange(#length, oldLength, length);
+      notifyChange(new MapChangeRecord<K, V>.insert(key, newValue));
+    } else {
+      notifyChange(new MapChangeRecord<K, V>(key, oldValue, newValue));
+    }
+  }
+
+  @override
+  void addAll(Map<K, V> other) {
+    if (!hasObservers) {
+      super.addAll(other);
+      return;
+    }
+
+    other.forEach((k, v) => this[k] = v);
+  }
+
+  @override
+  V putIfAbsent(K key, V ifAbsent()) {
+    final oldLength = length;
+    final result = super.putIfAbsent(key, ifAbsent);
+    if (hasObservers && oldLength != length) {
+      notifyPropertyChange(#length, oldLength, length);
+      notifyChange(new MapChangeRecord<K, V>.insert(key, result));
+    }
+    return result;
+  }
+
+  @override
+  V remove(Object key) {
+    final oldLength = length;
+    final result = super.remove(key);
+    if (hasObservers && oldLength != length) {
+      notifyChange(new MapChangeRecord<K, V>.remove(key as K, result));
+      notifyPropertyChange(#length, oldLength, length);
+    }
+    return result;
+  }
+
+  @override
+  void clear() {
+    if (!hasObservers || isEmpty) {
+      super.clear();
+      return;
+    }
+    final oldLength = length;
+    forEach((k, v) {
+      notifyChange(new MapChangeRecord<K, V>.remove(k, v));
+    });
+    notifyPropertyChange(#length, oldLength, 0);
+    super.clear();
+  }
+}
diff --git a/lib/src/observable_map.dart b/lib/src/observable_map.dart
deleted file mode 100644
index caebbda..0000000
--- a/lib/src/observable_map.dart
+++ /dev/null
@@ -1,167 +0,0 @@
-// Copyright (c) 2016, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library observable.src.observable_map;
-
-import 'dart:collection';
-
-import 'observable.dart';
-import 'records.dart';
-import 'to_observable.dart';
-
-// TODO(jmesserly): this needs to be faster. We currently require multiple
-// lookups per key to get the old value.
-// TODO(jmesserly): this doesn't implement the precise interfaces like
-// LinkedHashMap, SplayTreeMap or HashMap. However it can use them for the
-// backing store.
-
-/// Represents an observable map of model values. If any items are added,
-/// removed, or replaced, then observers that are listening to [changes]
-/// will be notified.
-class ObservableMap<K, V> extends Observable implements Map<K, V> {
-  final Map<K, V> _map;
-
-  /// Creates an observable map.
-  ObservableMap() : _map = new HashMap<K, V>();
-
-  /// Creates a new observable map using a [LinkedHashMap].
-  ObservableMap.linked() : _map = new LinkedHashMap<K, V>();
-
-  /// Creates a new observable map using a [SplayTreeMap].
-  ObservableMap.sorted() : _map = new SplayTreeMap<K, V>();
-
-  /// Creates an observable map that contains all key value pairs of [other].
-  /// It will attempt to use the same backing map type if the other map is a
-  /// [LinkedHashMap], [SplayTreeMap], or [HashMap]. Otherwise it defaults to
-  /// [HashMap].
-  ///
-  /// Note this will perform a shallow conversion. If you want a deep conversion
-  /// you should use [toObservable].
-  factory ObservableMap.from(Map<K, V> other) {
-    return new ObservableMap<K, V>.createFromType(other)..addAll(other);
-  }
-
-  /// Like [ObservableMap.from], but creates an empty map.
-  factory ObservableMap.createFromType(Map<K, V> other) {
-    ObservableMap<K, V> result;
-    if (other is SplayTreeMap) {
-      result = new ObservableMap<K, V>.sorted();
-    } else if (other is LinkedHashMap) {
-      result = new ObservableMap<K, V>.linked();
-    } else {
-      result = new ObservableMap<K, V>();
-    }
-    return result;
-  }
-
-  /// Creates a new observable map wrapping [other].
-  ObservableMap.spy(Map<K, V> other) : _map = other;
-
-  @override
-  Iterable<K> get keys => _map.keys;
-
-  @override
-  Iterable<V> get values => _map.values;
-
-  @override
-  int get length => _map.length;
-
-  @override
-  bool get isEmpty => length == 0;
-
-  @override
-  bool get isNotEmpty => !isEmpty;
-
-  @override
-  bool containsValue(Object value) => _map.containsValue(value);
-
-  @override
-  bool containsKey(Object key) => _map.containsKey(key);
-
-  @override
-  V operator [](Object key) => _map[key];
-
-  @override
-  void operator []=(K key, V value) {
-    if (!hasObservers) {
-      _map[key] = value;
-      return;
-    }
-
-    int len = _map.length;
-    V oldValue = _map[key];
-
-    _map[key] = value;
-
-    if (len != _map.length) {
-      notifyPropertyChange(#length, len, _map.length);
-      notifyChange(new MapChangeRecord.insert(key, value));
-      _notifyKeysValuesChanged();
-    } else if (oldValue != value) {
-      notifyChange(new MapChangeRecord(key, oldValue, value));
-      _notifyValuesChanged();
-    }
-  }
-
-  @override
-  void addAll(Map<K, V> other) {
-    other.forEach((K key, V value) {
-      this[key] = value;
-    });
-  }
-
-  @override
-  V putIfAbsent(K key, V ifAbsent()) {
-    int len = _map.length;
-    V result = _map.putIfAbsent(key, ifAbsent);
-    if (hasObservers && len != _map.length) {
-      notifyPropertyChange(#length, len, _map.length);
-      notifyChange(new MapChangeRecord.insert(key, result));
-      _notifyKeysValuesChanged();
-    }
-    return result;
-  }
-
-  @override
-  V remove(Object key) {
-    int len = _map.length;
-    V result = _map.remove(key);
-    if (hasObservers && len != _map.length) {
-      notifyChange(new MapChangeRecord.remove(key, result));
-      notifyPropertyChange(#length, len, _map.length);
-      _notifyKeysValuesChanged();
-    }
-    return result;
-  }
-
-  @override
-  void clear() {
-    int len = _map.length;
-    if (hasObservers && len > 0) {
-      _map.forEach((key, value) {
-        notifyChange(new MapChangeRecord.remove(key, value));
-      });
-      notifyPropertyChange(#length, len, 0);
-      _notifyKeysValuesChanged();
-    }
-    _map.clear();
-  }
-
-  @override
-  void forEach(void f(K key, V value)) => _map.forEach(f);
-
-  @override
-  String toString() => Maps.mapToString(this);
-
-  // Note: we don't really have a reasonable old/new value to use here.
-  // But this should fix "keys" and "values" in templates with minimal overhead.
-  void _notifyKeysValuesChanged() {
-    notifyChange(new PropertyChangeRecord(this, #keys, null, null));
-    _notifyValuesChanged();
-  }
-
-  void _notifyValuesChanged() {
-    notifyChange(new PropertyChangeRecord(this, #values, null, null));
-  }
-}
diff --git a/lib/src/to_observable.dart b/lib/src/to_observable.dart
index d117c89..b4cdaf0 100644
--- a/lib/src/to_observable.dart
+++ b/lib/src/to_observable.dart
@@ -7,8 +7,7 @@
 import 'dart:collection';
 
 import 'observable.dart' show Observable;
-import 'collections.dart' show ObservableList;
-import 'observable_map.dart' show ObservableMap;
+import 'collections.dart' show ObservableList, ObservableMap;
 
 /// Converts the [Iterable] or [Map] to an [ObservableList] or [ObservableMap],
 /// respectively. This is a convenience function to make it easier to convert
diff --git a/pubspec.yaml b/pubspec.yaml
index 7519461..8065fa9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: observable
-version: 0.18.1
+version: 0.19.0
 author: Dart Team <misc@dartlang.org>
 description: Support for marking objects as observable
 homepage: https://github.com/dart-lang/observable
diff --git a/test/observable_map_test.dart b/test/observable_map_test.dart
index 04aeccc..5e369d5 100644
--- a/test/observable_map_test.dart
+++ b/test/observable_map_test.dart
@@ -191,79 +191,6 @@
     expect(map.toString(), '{a: 1, b: 2}');
   });
 
-  group('observe keys/values', () {
-    ObservableMap map;
-    int keysChanged;
-    int valuesChanged;
-
-    setUp(() {
-      map = toObservable({'a': 1, 'b': 2, 'c': 3});
-      keysChanged = 0;
-      valuesChanged = 0;
-      sub = map.changes.listen((records) {
-        keysChanged += getPropertyChangeRecords(records, #keys).length;
-        valuesChanged += getPropertyChangeRecords(records, #values).length;
-      });
-    });
-
-    tearDown(sharedTearDown);
-
-    test('add item changes keys/values', () {
-      map['d'] = 4;
-      expect(map, {'a': 1, 'b': 2, 'c': 3, 'd': 4});
-      return new Future(() {
-        expect(keysChanged, 1);
-        expect(valuesChanged, 1);
-      });
-    });
-
-    test('putIfAbsent changes keys/values', () {
-      map.putIfAbsent('d', () => 4);
-      expect(map, {'a': 1, 'b': 2, 'c': 3, 'd': 4});
-      return new Future(() {
-        expect(keysChanged, 1);
-        expect(valuesChanged, 1);
-      });
-    });
-
-    test('remove changes keys/values', () {
-      map.remove('c');
-      map.remove('a');
-      expect(map, {'b': 2});
-      return new Future(() {
-        expect(keysChanged, 2);
-        expect(valuesChanged, 2);
-      });
-    });
-
-    test('remove non-existent item does not change keys/values', () {
-      map.remove('d');
-      expect(map, {'a': 1, 'b': 2, 'c': 3});
-      return new Future(() {
-        expect(keysChanged, 0);
-        expect(valuesChanged, 0);
-      });
-    });
-
-    test('set existing item does not change keys', () {
-      map['c'] = 9000;
-      expect(map, {'a': 1, 'b': 2, 'c': 9000});
-      return new Future(() {
-        expect(keysChanged, 0);
-        expect(valuesChanged, 1);
-      });
-    });
-
-    test('clear changes keys/values', () {
-      map.clear();
-      expect(map, {});
-      return new Future(() {
-        expect(keysChanged, 1);
-        expect(valuesChanged, 1);
-      });
-    });
-  });
-
   group('change records', () {
     List<ChangeRecord> records;
     ObservableMap map;
@@ -308,8 +235,6 @@
         expectChanges(records, [
           _lengthChange(map, 2, 3),
           _insertKey('c', 3),
-          _propChange(map, #keys),
-          _propChange(map, #values),
         ]);
       });
     });
@@ -324,11 +249,8 @@
       return new Future(() {
         expectChanges(records, [
           _changeKey('a', 1, 42),
-          _propChange(map, #values),
           _lengthChange(map, 2, 3),
           _insertKey('c', 3),
-          _propChange(map, #keys),
-          _propChange(map, #values),
         ]);
       });
     });
@@ -341,8 +263,6 @@
         expectChanges(records, [
           _removeKey('b', 2),
           _lengthChange(map, 2, 1),
-          _propChange(map, #keys),
-          _propChange(map, #values),
         ]);
       });
     });
@@ -356,8 +276,6 @@
           _removeKey('a', 1),
           _removeKey('b', 2),
           _lengthChange(map, 2, 0),
-          _propChange(map, #keys),
-          _propChange(map, #values),
         ]);
       });
     });