At this rate, every pattern in Design Patterns in Dart is going to include a websocket. That is a little crazy considering that I do not use websockets all that often in actual client code, but they have the dual benefits of having an accessible API and being conceptually easy to understand. So who knows, maybe I will find a way to include them in every single pattern.
The pattern of current investigation is the bridge pattern. As I found last night, websockets can serve as a realistic alternative to vanilla HTTP for communication. The alternative implementations for communication make the situation well suited for the bridge pattern, which seeks to "bridge" between an abstraction and multiple implementations.
What I would like to examine tonight is creating the right implementor object. Last night, I made two quick decisions on when to create the
Communication
implementor. Perhaps I could have done that better.The abstraction in this bridge pattern example remains a
Messenger
class. It could be a mobile app for posting status updates or sending direct messages. It could be a web form that does the same thing. Either way, a messenger needs a reference to a concrete implementation of Communication
and it needs to know how to post updates. So the interface that refined Messenger
classes will need to extend looks like:// Abstraction abstract class Messenger { Communication comm; Messenger(this.comm); void updateStatus(); }The refined messenger class that I am using is a
WebMessenger
. Its constructor stores a text input element from which it can obtain messages to send:class WebMessenger extends Messenger { InputElement _messageElement; WebMessenger(this._messageElement) : super(new HttpCommunication()); // ... }The bit after the constructor is a redirection to the superclass constructor, which supplies a
Communication
implementation to the Messenger
superclass. Here is where I make my first decision about when to construct a Communication
implementation. By default, I opt to create an HttpCommunication
implementation of the Communication
interface. Since the superclass requires a Communication
object in its constructor, I either had to create one right in the constructor as I have done here or I could have required client code to do so. The choice of whether the client should provide the implementation or if it should be done in the refined abstraction comes down to the details of the code. To be perfectly honest, I did it this way because it was quickest. It probably was not the correct choice, however. Yesterday's implementation has the client choose when to switch between the websocket and HTTP concrete implementations. If the client code chose when to switch, it probably should determine initial state as well.
But let's move the choice of implementation from the client and instead put in into the
WebMessenger
. It makes sense to start with a low-overhead HttpCommunication
object. Given that, the redirecting constructor can stay as-is. But, should the client find itself at the mercy of a power user who is updating status more often than 3 times a minute, then the WebMessenger
should switch to a websocket.So, after each message, I log the message along with a timestamp:
class WebMessenger extends Messenger { // ... List _history = []; // ... void updateStatus() { comm.send(message); _log(message); } void _log(message) { _history.add([new DateTime.now(), message]); } // ... }Then, after logging I do a simple calculation of the frequency with which this user is updating. If it is too often or too slow, I switch
Communication
implementations:class WebMessenger extends Messenger { // ... void updateStatus() { comm.send(message); _log(message); _maybeChangeCommunication(); } // ... _maybeChangeCommunication() { if (_history.length < 3) return; var last = _history.length - 1, dateOld = _history[last-2][0], dateNew = _history[last][0], diff = dateNew.difference(dateOld); if (diff.inSeconds < 60) { if (comm is! WebSocketCommunication) comm = new WebSocketCommunication(); } else { if (comm is! HttpCommunication) comm = new HttpCommunication(); } } }That does the trick. If I send the first 3 message updates out in quick succession, then
WebMessenger
upgrades the connection to the WebSocketCommunication
implementation. The server sees the first three messages come in over HTTP, then the next few over websockets:$ ./bin/server.dart [HTTP] message=asdf+1 [HTTP] message=asdf+2 [HTTP] message=asdf+3 [WebSocket] message=asdf 4 [WebSocket] message=asdf 5 [WebSocket] message=asdf 6 [HTTP] message=asdf+7
I then wait a minute between messages 5 and 6, after which I have crossed the lower threshold and am again in the HttpCommunication
implementation.
That seems a nice example of when it make sense for the refined abstraction to have responsibility for choosing the implementation—websockets come through again! I do think that some or all of the choice behavior could move into the abstraction itself. Whether that is a good idea depends on the variance in refined abstractions. If a mobile messenger wanted different thresholds or added a new implementation, then the choose-your-implementation behavior would need to continue to reside in the refined abstractions. But if the web and mobile (and other) refined abstractions could share the same choices, then the subclasses could be wonderfully brief.
Code for the bridge client and the backend server are on the Design Patterns in Dart public repository.
Day #81