Friday, February 19, 2016

Consequences of the Chain of Responsibility


Before wrapping up my initial research on the chain of responsibility pattern, I take one last look at the consequences of code that follows it. In particular, I would like to touch on configuration and maintainability tonight.

The example that I currently favor for illustrating the pattern is a set of cell formatting objects for a spreadsheet application:



To auto-format the cells, each formatting object in the chain is given an opportunity to claim responsibility for the content. In this example, a cell with all numbers is converted to a double with two digits after the decimal, a date is converted to ISO 8601 format, and everything else defaults to text.

To test configuration, I add some checkboxes to disable some of the formatters:



The main takeaway from this is that it is possible to easily change the chain of responsibility at runtime. The Dart listener for the no-text formatting checkbox might look like:
  query('#no-text').onChange.listen((e){
    var el = e.target;
    if (el.checked) {
      date.nextHandler = null;
    }
    else {
      date.nextHandler = text;
    }
  });
The date formatter normally points to the text handler. That is, if the date formatter thinks the current input field is not a date, then it forwards the request onto the text formatter. By ticking this checkbox, the date formatter now thinks that there is no next handler.

Thanks to last night's CellFormatter base class, this just works:
abstract class CellFormatter {
  CellFormatter nextHandler;

  void processRequest(Event e) {
    if (!isCell(e)) return;
    if (handleRequest(e)) return;
    if (nextHandler == null) return;

    nextHandler.processRequest(e);
  }
  // ...
}
With the checkbox ticked, the date formatter no longer has a nextHandler so the date formatter simply does nothing. The result in the UI is that a cell that was previously a numeric and was right-aligned retains the alignment because there is no longer a text formatter to reset it:



This illustrates another important aspect of the chain of responsibility. It is best to ensure that requests do not just "fall off" the end of the chain like this. Ideally, I ought to have a default handler at the end then resets all styles.

One last thing that I would like to touch on before calling it a day is how robust the pattern is in the face of change. Currently, the first formatter in the chain processes event requests when changes are seen in cells:
  container.onChange.listen(number.processRequest);
What if I also wanted to apply changes when clicks are seen inside cells? Well, aside from Dart's lack of an easy means to merge event streams, this is fairly easy to support.

I create a StreamController object and add events to it whenever there are clicks or changes:
  var c = new StreamController();
  container.onChange.listen(c.add);
  container.onClick.listen(c.add);
Then, I hand the first formatter in the chain to this combo-stream:
  c.stream.listen(number.processRequest);
And that's all there is to it. Now I can force formatting by tabbing to another cell or, if I am feeling impatient, I can click on a cell to format its contents.

Nice! That may finish my exploration of the chain of responsibility pattern in Dart (though there could be one more Darty trick to try). I do think this spreadsheet cell example will serve nicely in Design Patterns in Dart. The code is fairly simple, yet still feels like a real example, which is pretty close to ideal. Plus, it serves well when discussing some of the consequences of the pattern!

Play with the code on DartPad: https://dartpad.dartlang.org/08105c648b253f64a47d.


Day #100


7 comments: