How much should the client know about the implementation of the
chain of responsibility pattern? I have reworked my spreadsheet cell formatting example to the point that I am quite satisfied with the code organization and how it responds:
What leaves me slightly less satisfied is how much the
Dart constructing code needs to know about the underlying implementation. Currently, I have it constructing the individual links in the chain
and assigning those links:
var textFormat = new TextFormatter();
var dateFormat = new DateFormatter(textFormat);
var numberFormat = new NumberFormatter(dateFormat);
I do not think it too horrible that the constructing code needs to know about the individual formatters—especially since it needs to know how to remove some of them in response to checkbox changes. What bugs me here is the same thing that has bugged me about other examples that I have built—the requirement that the constructing code know which link is first, which link has to be constructed first, and with which links each can and should link.
That is, the text-formatter is the default link, so it needs to be last in the chain, but it has to be constructed first so that the second-to-last link can point to it. It is also not a good idea to remove the text-formatter (even though I have a checkbox in the UI) because it is the default cell formatter. Lastly, I find it awkward to tell the on-change listener to number-format cells when establishing the request handler:
container.onChange.listen(numberFormat);
Even though it looks like I am asking for number formatting, I am really asking for something in the cell formatting chain to handle the request—number format just happens to be the first.
In some (most?) implementations of the chain of responsibility this might actually be OK or desired organization. In this case, I think a facade wants to be in front of this complexity. So I define
CellFormatter
such that it declares the individual link components:
class CellFormatter {
var _numberFormat = new NumberFormatter();
var _dateFormat = new DateFormatter();
var _textFormat = new TextFormatter();
var first;
// ...
}
I also declare a pointer to the first cell-formatting object so that I know where to start requests and can easily update it. In the constructor, I then establish the links in the chain and initially set
first
:
class CellFormatter {
// ...
CellFormatter() {
_numberFormat.nextHandler = _dateFormat;
_dateFormat.nextHandler = _textFormat;
first = _numberFormat;
}
// ...
}
The
call()
method is then simple enough—I send the event request onto the
call()
method of the first link in the chain:
class CellFormatter {
// ...
void call(Event e) { first.call(e); }
// ...
}
And, as mentioned, it is easy to manipulate the chain with methods like
ignore()
and
obey()
. A simple implementation for the number formatter could be:
class CellFormatter {
// ...
void ignore(String format) {
if (format == 'number') first = _dateFormat;
// ...
}
void obey(String format) {
if (format == 'number') first = _numberFormat;
// ...
}
}
I feel more comfortable putting this knowledge in a class than I would in the main code. At least for this example, this seems like too much maintenance to mingle with other code.
This does lead to some significant improvements in the main code. Obviously, it is much simpler to instantiate one object instead of three (and to omit the linking):
var cellFormat = new CellFormatter();
I also get the benefit of an easier to read (and hence easier to maintain) assignment of the cell-formatter:
container.onChange.listen(cellFormat);
This one-liner reads nicely and has low cognitive requirements: when the spreadsheet container sees changes, it cell-formats the source elements. No worry about the chain or the first element in the chain. Formatting is done by the cell formatter. Simple!
(see last night's post for why
cellFormat
behave like a function here)
The nicest win in this approach may be in the code that ignores/obeys formatters. Previously, when ignoring the number formatter, I had to obtain a subscription from the initial listener so that I could then cancel it and start a new listener with the second link in the chain:
var subscription= container.onChange.listen(numberFormat);
query('#no-numbers').onChange.listen((e){
var el = e.target;
if (el.checked) {
subscription.cancel();
subscription = c.stream.listen(dateFormat);
}
// ...
});
Now that
CellFormatter
internally maintains a reference to the
first
item in the chain, I no longer need to worry about the stream subscription sending to the wrong first element. The resulting code is mercifully free from subscription code, only obeying or ignoring links as requested:
container.onChange.listen(cellFormat);
query('#no-numbers').onChange.listen((e){
var el = e.target;
if (el.checked) {
cellFormat.ignore('number');
}
// ...
});
That seems a nice improvement.
Overall, I am very happy with the improvement in the code as a results of this approach. I have yet to examine it in depth, but I believe this is likely a facade pattern as it hides some of the underlying complexity of the chain of responsibility from the calling context. Regardless of what it is called, I very much like it!
This seems a nice stopping point for my research into the chain of responsibility. Up next, who knows?
Play with the code on DartPad: https://dartpad.dartlang.org/adf7e28a00eba10491e9.
Day #102
‹prev | My Chain | next›