It seemed like a good idea at the time.
I would like to experiment with remote proxy implementations of the proxy pattern in Dart. You know what might work for that? Dart isolates. Well, probably not, but it seems like a good idea...
I start by simplifying the
Car
class, which will serve as the real subject in the pattern, to just an automobile
that can drive, stop, and report state:// Real Subject class Car implements Automobile { String state = 'idle'; void drive() { state = 'driving'; } void stop() { state = 'idle'; } }The proxy subject will then be tasked with communicating with the real subject across isolates. I am unsure exactly how that is going to work, but the proxy subject will need a
SendPort
, at the very least, to request the real car update itself. So I start with:// Proxy Subject class ProxyCar implements Automobile { SendPort _s; String _state = "???"; String get state => _state; void drive() { _s.send(#drive); } void stop() { _s.send(#stop); } }Where the
SendPort
comes from and how the _state
private instance variable gets updated are questions that I will try to answer in a bit.First, I want to establish the other isolate to which the main thread will talk. Like all isolates, I need to accept a
SendPort
through which it can send information back to the main thread. And I am pretty sure that I need for the main thread to be able to send information to the isolate, so the first thing I do is create a ReceivePort
that I can send back:other(SendPort s) { var r = new ReceivePort(); s.send(r.sendPort); // Will establish real car here... }OK, back in the main thread, I do the usual isolate dance. I create a
ReceivePort
so that I can sent its sendPort
property into the spawned other()
isolate:main() { ProxyCar car; var r = new ReceivePort(); Isolate. spawn(other, r.sendPort). // Wait for isolate to be ready, then return first message, which is send // port back to the isolate then((_) => r.first). // Create proxy car with receive stream and isolate send port then((s) { /** Create proxy car here... **/ }). // Will drive and report state here... }After spawning the isolate, I wait for the returned
Future
to complete, indicating that the isolate is ready. Then, I return the first message sent back from other()
, which is the SendPort
through which I can send messages from main()
to other()
. Here, I note a problem. If I read the
first
property, then the receive port's stream is closed. That will cause problems as the ProxyCar
tries to receive messages from the receive port. So instead, I convert the ReceivePort
to a broadcast stream:main() { ProxyCar car; var r = new ReceivePort(); var receiveStream = r.asBroadcastStream(); Isolate. spawn(other, r.sendPort). // Wait for isolate to be ready, then return first message, which is send // port back to the isolate then((_) => receiveStream.first). // Create proxy car with receive stream and isolate send port then((s) { car = new ProxyCar(receiveStream, s); }). // Will drive and report state here... }With that, I think I see a first pass implementation for the proxy car—it needs a stream on which to listen for messages from the real car and a send port through which messages can be sent to the real car. Something like this should work:
// Proxy Subject class ProxyCar implements Automobile { SendPort _s; var _r; String _state = "???"; ProxyCar(this._r, this._s) { _r.listen((message) { print("[ProxyCar] $message"); _state = message; }); } // state, drive, stop declared already... }The only messages that will come through that
ReceivePort
stream will (for now) be state updates, so I assign them to the _state
instance variable. The rest is already in place—the already declared state
, stop()
, and start()
methods will send messages through the SendPort
supplied to the constructor.So the rest of the main thread becomes:
Isolate.
spawn(other, r.sendPort).
// Wait for isolate to be ready, then return first message, which is send
// port back to the isolate
then((_) => receiveStream.first).
// Create proxy car with receive stream and isolate send port
then((s) { car = new ProxyCar(receiveStream, s); }).
// Drive proxy car, then wait for state message
then((_) { car.drive(); }).
then((_) => receiveStream.first).
// Proxy car state is ready, so print
then((_) { print("Car is ${car.state}"); }).
// Stop proxy car, then wait for state message
then((_) { car.stop(); }).
then((_) => receiveStream.first).
// Proxy car state is ready, so print
then((_) { print("Car is ${car.state}"); });
Last, I need the real subject to work with the opposite send and receive ports. I will follow the same constructor signature, even though a broadcast stream is not necessary in the other isolate. That will make the creation of the real car look like:other(SendPort s) { var r = new ReceivePort(); s.send(r.sendPort); var receiveStream = r.asBroadcastStream(); new Car(receiveStream, s); }And the real class needs to establish a listener for drive and stop messages from the proxy:
class Car implements Automobile { SendPort _s; var _r; Car(this._r, this._s) { _r.listen((message) { print(message); if (message == #drive) drive(); if (message == #stop) stop(); _s.send(state); }); } String state = 'idle'; void drive() { state = 'driving'; } void stop() { state = 'idle'; } }Phew! That was a lot harder than I expected. There is almost certainly some cleanup that I can perform. Perhaps the car classes can create their own
ReceivePort
instances. A little Future
improvement is in order. Still, the code works:$ ./bin/drive.dart Symbol("drive") [ProxyCar] driving Car is driving Symbol("stop") [ProxyCar] idle Car is idleThe real subject receives the
#drive
symbol. Then the proxy subject receives a message that the real subject is driving. Then the output of the current state from the proxy is that the car is driving.I suppose I should have known that isolate code would get messy like this. Hopefully I can clean it up tomorrow.
Day #61
No comments:
Post a Comment