Extract out common `ReplayAware` interface (#29)

This lets us unify the identifier logic across record and replay
object serializations.

Part of #11
diff --git a/lib/src/backends/record_replay/common.dart b/lib/src/backends/record_replay/common.dart
index 4690148..2368047 100644
--- a/lib/src/backends/record_replay/common.dart
+++ b/lib/src/backends/record_replay/common.dart
@@ -88,6 +88,21 @@
   bool matches(dynamic object) => object is T;
 }
 
+/// Marks a class that, when serialized, will be referred to merely by an
+/// opaque identifier.
+///
+/// Unlike other objects, objects that are replay-aware don't need to serialize
+/// meaningful metadata about their state for the sake of resurrection. Rather,
+/// they derive all the information they need to operate from the recording.
+/// As such, they are serialized using only an opaque unique identifier. When
+/// they are resurrected during replay, their identifier allows them to find
+/// invocations in the recording for which they are the target.
+abstract class ReplayAware {
+  /// The identifier of this object, guaranteed to be unique within a single
+  /// recording.
+  String get identifier;
+}
+
 /// Tells whether two objects are equal using deep equality checking.
 ///
 /// Two lists are deeply equal if they have the same runtime type, the same
diff --git a/lib/src/backends/record_replay/encoding.dart b/lib/src/backends/record_replay/encoding.dart
index 4e2146e..f43c474 100644
--- a/lib/src/backends/record_replay/encoding.dart
+++ b/lib/src/backends/record_replay/encoding.dart
@@ -9,13 +9,6 @@
 
 import 'common.dart';
 import 'events.dart';
-import 'recording_directory.dart';
-import 'recording_file.dart';
-import 'recording_file_system_entity.dart';
-import 'recording_io_sink.dart';
-import 'recording_link.dart';
-import 'recording_random_access_file.dart';
-import 'replay_proxy_mixin.dart';
 import 'result_reference.dart';
 
 /// Encodes an object into a JSON-ready representation.
@@ -43,13 +36,7 @@
   const TypeMatcher<p.Context>(): _encodePathContext,
   const TypeMatcher<ResultReference<dynamic>>(): _encodeResultReference,
   const TypeMatcher<LiveInvocationEvent<dynamic>>(): _encodeEvent,
-  const TypeMatcher<FileSystem>(): _encodeFileSystem,
-  const TypeMatcher<RecordingDirectory>(): _encodeFileSystemEntity,
-  const TypeMatcher<RecordingFile>(): _encodeFileSystemEntity,
-  const TypeMatcher<RecordingLink>(): _encodeFileSystemEntity,
-  const TypeMatcher<RecordingIOSink>(): _encodeIOSink,
-  const TypeMatcher<RecordingRandomAccessFile>(): _encodeRandomAccessFile,
-  const TypeMatcher<ReplayProxyMixin>(): _encodeReplayEntity,
+  const TypeMatcher<ReplayAware>(): _encodeReplayAwareEntity,
   const TypeMatcher<Encoding>(): _encodeEncoding,
   const TypeMatcher<FileMode>(): _encodeFileMode,
   const TypeMatcher<FileStat>(): _encodeFileStat,
@@ -116,25 +103,7 @@
 Map<String, dynamic> _encodeEvent(LiveInvocationEvent<dynamic> event) =>
     event.serialize();
 
-String _encodeFileSystem(FileSystem fs) => kFileSystemEncodedValue;
-
-/// Encodes a file system entity by using its `uid` as a reference identifier.
-/// During replay, this allows us to tie the return value of of one event to
-/// the object of another.
-String _encodeFileSystemEntity(
-    RecordingFileSystemEntity<FileSystemEntity> entity) {
-  return '${entity.runtimeType}@${entity.uid}';
-}
-
-String _encodeIOSink(RecordingIOSink sink) {
-  return '${sink.runtimeType}@${sink.uid}';
-}
-
-String _encodeRandomAccessFile(RecordingRandomAccessFile raf) {
-  return '${raf.runtimeType}@${raf.uid}';
-}
-
-String _encodeReplayEntity(ReplayProxyMixin entity) => entity.identifier;
+String _encodeReplayAwareEntity(ReplayAware entity) => entity.identifier;
 
 String _encodeEncoding(Encoding encoding) => encoding.name;
 
diff --git a/lib/src/backends/record_replay/recording_file_system.dart b/lib/src/backends/record_replay/recording_file_system.dart
index 7e33e14..dc28b12 100644
--- a/lib/src/backends/record_replay/recording_file_system.dart
+++ b/lib/src/backends/record_replay/recording_file_system.dart
@@ -5,6 +5,7 @@
 import 'package:file/file.dart';
 import 'package:meta/meta.dart';
 
+import 'common.dart';
 import 'mutable_recording.dart';
 import 'recording.dart';
 import 'recording_directory.dart';
@@ -109,6 +110,9 @@
     });
   }
 
+  @override
+  String get identifier => kFileSystemEncodedValue;
+
   /// The file system to which invocations will be forwarded upon recording.
   @override
   final FileSystem delegate;
diff --git a/lib/src/backends/record_replay/recording_file_system_entity.dart b/lib/src/backends/record_replay/recording_file_system_entity.dart
index 91cb515..1083d26 100644
--- a/lib/src/backends/record_replay/recording_file_system_entity.dart
+++ b/lib/src/backends/record_replay/recording_file_system_entity.dart
@@ -51,6 +51,9 @@
   final int uid = newUid();
 
   @override
+  String get identifier => '$runtimeType@$uid';
+
+  @override
   final RecordingFileSystemImpl fileSystem;
 
   @override
diff --git a/lib/src/backends/record_replay/recording_io_sink.dart b/lib/src/backends/record_replay/recording_io_sink.dart
index ea20004..b79f578 100644
--- a/lib/src/backends/record_replay/recording_io_sink.dart
+++ b/lib/src/backends/record_replay/recording_io_sink.dart
@@ -47,6 +47,9 @@
   final int uid = newUid();
 
   @override
+  String get identifier => '$runtimeType@$uid';
+
+  @override
   MutableRecording get recording => fileSystem.recording;
 
   @override
diff --git a/lib/src/backends/record_replay/recording_proxy_mixin.dart b/lib/src/backends/record_replay/recording_proxy_mixin.dart
index a10815b..ddf1f43 100644
--- a/lib/src/backends/record_replay/recording_proxy_mixin.dart
+++ b/lib/src/backends/record_replay/recording_proxy_mixin.dart
@@ -6,6 +6,7 @@
 
 import 'package:meta/meta.dart';
 
+import 'common.dart';
 import 'events.dart';
 import 'mutable_recording.dart';
 import 'proxy.dart';
@@ -60,7 +61,7 @@
 /// Methods that return [Stream]s will be recorded immediately, but their
 /// return values will be recorded as a [List] that will grow as the stream
 /// produces data.
-abstract class RecordingProxyMixin implements ProxyObject {
+abstract class RecordingProxyMixin implements ProxyObject, ReplayAware {
   /// Maps method names to delegate functions.
   ///
   /// Invocations of methods listed in this map will be recorded after
diff --git a/lib/src/backends/record_replay/recording_random_access_file.dart b/lib/src/backends/record_replay/recording_random_access_file.dart
index e0bf90d..5e5146a 100644
--- a/lib/src/backends/record_replay/recording_random_access_file.dart
+++ b/lib/src/backends/record_replay/recording_random_access_file.dart
@@ -66,6 +66,9 @@
   final int uid = newUid();
 
   @override
+  String get identifier => '$runtimeType@$uid';
+
+  @override
   MutableRecording get recording => fileSystem.recording;
 
   @override
diff --git a/lib/src/backends/record_replay/replay_file_system.dart b/lib/src/backends/record_replay/replay_file_system.dart
index 468570f..5a36354 100644
--- a/lib/src/backends/record_replay/replay_file_system.dart
+++ b/lib/src/backends/record_replay/replay_file_system.dart
@@ -67,7 +67,7 @@
 /// Non-exported implementation class for `ReplayFileSystem`.
 class ReplayFileSystemImpl extends FileSystem
     with ReplayProxyMixin
-    implements ReplayFileSystem {
+    implements ReplayFileSystem, ReplayAware {
   /// Creates a new `ReplayFileSystemImpl`.
   ReplayFileSystemImpl(this.manifest) {
     methods.addAll(<Symbol, Resurrector>{
diff --git a/lib/src/backends/record_replay/replay_proxy_mixin.dart b/lib/src/backends/record_replay/replay_proxy_mixin.dart
index f4d6310..60a9502 100644
--- a/lib/src/backends/record_replay/replay_proxy_mixin.dart
+++ b/lib/src/backends/record_replay/replay_proxy_mixin.dart
@@ -57,7 +57,7 @@
 ///         });
 ///       }
 ///     }
-abstract class ReplayProxyMixin implements ProxyObject {
+abstract class ReplayProxyMixin implements ProxyObject, ReplayAware {
   /// Maps method names to [Resurrector] functions.
   ///
   /// Invocations of methods listed in this map will be replayed by looking for
@@ -79,16 +79,6 @@
   @protected
   final Map<Symbol, Resurrector> properties = <Symbol, Resurrector>{};
 
-  /// The unique identifier of this replay object.
-  ///
-  /// When replay-aware objects are serialized in a recording, they are done so
-  /// using only a unique String identifier. When the objects are resurrected
-  /// for the purpose of replay, their identifiers are used to match incoming
-  /// invocations against recorded invocations in the [manifest] (only
-  /// invocations whose target object matches the identifier are considered
-  /// possible matches).
-  String get identifier;
-
   /// The manifest of recorded invocation events.
   ///
   /// When invocations are received on this object, we will attempt find a
diff --git a/test/recording_test.dart b/test/recording_test.dart
index 919700c..545b146 100644
--- a/test/recording_test.dart
+++ b/test/recording_test.dart
@@ -900,6 +900,9 @@
   }
 
   @override
+  String get identifier => '$runtimeType';
+
+  @override
   final MutableRecording recording;
 
   @override