Invert Your Inner Control Freak!
Arvind's recent posts have had me thinking. While the spellchecker he outlines is really just for illustrative purposes, its actually an example that is great to show the power, ease of use, and connectivity of MATLAB. I'd like to add to that the ability to showcase a software design pattern or two. Today I'd like to continue Arvind's spellchecker example to meander a bit around the topic of inversion of control.
Inversion of control is sometimes called the Hollywood principle:
"Don't call us, we'll call you!"
It is a technique for structuring software that encourages creating abstractions and avoiding direct dependencies on the components and services that actually make the software work. When first encountered it can be a bit counterintuitive, but it facilitates flexibility and modularity like nobody's business! The big takeaway is that components should really avoid doing work themselves and instead rely upon the abstractions that collaborators can fulfill. Got it? Not really? Lemme show by example. Take a look at the start of the spellcheck constructor from last post:
function obj = spellcheck(varargin) % Make Jazzy available to MATLAB import com.mathworks.spellcheck.*; % Setup a default language. obj.Dictionary = which('en_USx.dic'); % default % Create a jazzy spellchecker obj.Handle = SpellCheck(); obj.Handle.setDictionary(obj.Dictionary); obj.Handle.verbose = true; end
This looks innocuous enough but it does not practice the principle of the day. Here in this constructor snippet we are setting the Dictionary property explicitly as well as creating our spell checking engine (Jazzy). This is OK for small programs, but this approach leads us to rigidity in larger systems. The spellchecker now has a concrete dependency on both the Jazzy library and this particular dictionary. This means that we can't use this spellchecker technology in the absence of Jazzy (like with a different spellchecking engine, perhaps in -nojvm mode), or in the absence of the English dictionary.
"Well of course!" you might say, "How else would we get anything done?" you might ask. After all, we need to invoke this code somehow somewhere, why not in the constructor? Well, let's remove one of these hard dependencies to see how its done. Today I'll show this for the dictionary file, although the same process would still apply to the spell checking engine just as well.
The approach needed is to create an abstraction, and rather than hardcoding which dictionary in the constructor, we should pass in the abstraction. We ask the creator of our instance to provide what we depend on rather than ourselves creating a concrete version of what we depend on directly in the constructor. The nice benefit here is that when we ask for this to be provided in the constructor inputs, many more possibilities open up in the future for which things can be passed in. As the world evolves around us we see that this leads to much more flexibility and ability to react to change and reuse our software in contexts not envisioned when it was created.
For our spellchecker, let's remove our explicit reference of the dictionary. The first step is to create an abstraction:
classdef Dictionary < matlab.mixin.Heterogeneous properties(Abstract) DictionaryFile end end
Note, I've marked it Heterogeneous because I am going to want to take advantage of MATLAB's fundamental array nature, but I'd like to do so with different dictionary implementations.
Now that we have the abstraction we can just operate on that abstraction by asking that it be passed into the constructor:
function checker = spellcheck(dictionary) % Hold onto our dictionary checker.Dictionary = dictionary; % Create the jazzy spellchecker checker.Handle = com.mathworks.spellcheck.SpellCheck(); checker.Handle.setDictionary(dictionary.DictionaryFile); checker.Handle.verbose = true; end
It's totally subtle!...but I love seeing this. In my mind the ideal constructor is doing nothing but setting properties from the values passed in. If we did this exercise once more for the Handle property we would also pass in the SpellCheckEngine or some such and all the constructor would do would set these two properties.
For reference here is a complete simple version of this spellchecker. (note depends on the small java code developed a couple posts ago).
classdef spellcheck properties Dictionary; end properties(GetAccess=private, SetAccess=immutable) Handle; end methods function checker = spellcheck(dictionary) % Hold onto our dictionary checker.Dictionary = dictionary; % Create the jazzy spellchecker checker.Handle = com.mathworks.spellcheck.SpellCheck(); checker.Handle.setDictionary(dictionary.DictionaryFile); checker.Handle.verbose = true; end %% CHECK Method to check an input string function check(checker, inputStr) checker.Handle.checkSpelling(inputStr); end end end
Anyway, now that we are operating on this abstraction we can start creating some dictionaries to fulfill it:
classdef AmericanEnglishDictionary < Dictionary properties DictionaryFile = 'en_USx.dic'; end end
checker = spellcheck(AmericanEnglishDictionary);
checker.check('Do you speak MALTAB?');
Input : MALTAB Suggestion: MATLAB
Oops! Indeed:
checker.check('Do you speak MATLAB?');
Silence is golden.
But this doesn't work:
checker.check('¿Hablas MATLAB?');
Input : Hablas Suggestion: tablas
...and nor should it, because we are speaking a different language. No problem, let's do the same thing for Spanish and we are good to go:
classdef MexicanSpanishDictionary < Dictionary properties DictionaryFile = 'es_MXx.dic'; end end
checker = spellcheck(MexicanSpanishDictionary);
checker.check('¿Hablas MATLAB?');
El silencio es oro.
Well, now that we are running at full throttle we can take this abstraction however far we want. We no longer have to be tied to an individual language file, the implementing dictionary can do whatever it wants as long as it stays true to the Dictionary interface. For example, let's see if we can use this to take care of an entire continent, namely North America. First, we'll need a couple more dictionaries for the Canadians, one more English:
classdef CanadianEnglishDictionary < Dictionary properties DictionaryFile = 'en_CAx.dic'; end end
and a French dictionary. My apologies French Canadians! I know this is not correct, but it seems the Jazzy libraries with which we are demonstrating this concept don't have a fr_CAx.dic file. Sorry, but humor me this will get us through the example.
classdef CanadianFrenchDictionary < Dictionary properties DictionaryFile = 'fr_FRx.dic'; end end
Up to this point we could only operate on a single dictionary. However, now that we are operating on an outside abstraction our code still works with totally different implementations of this abstraction. For example, nothing now stops us from implementing this reusable multi-lingual dictionary that builds off of all the dictionaries we already have:
classdef MultiLingualDictionary < Dictionary properties DictionaryFile; end methods function dict = MultiLingualDictionary(file, dictionaries) dict.DictionaryFile = file; % Open our dictionary file and append all dictionary languages to it. Need % to open using UTF-8 encoding, which is the format for Jazzy dictionaries. [fileID, msg] = fopen(file,'w','native','UTF-8'); assert(fileID > 0, msg); cleaner = onCleanup(@() fclose(fileID)); for idx = 1:numel(dictionaries) fprintf(fileID, fileread(dictionaries(idx).DictionaryFile)); end end end end
and right off the bat I am going to use this to conquer the mighty North American spell checking problem!
northAmericanDictionaries = [MexicanSpanishDictionary AmericanEnglishDictionary CanadianEnglishDictionary CanadianFrenchDictionary]
northAmericanDictionaries = 1x4 heterogeneous Dictionary (MexicanSpanishDictionary, AmericanEnglishDictionary, CanadianEnglishDictionary, ...) array with no properties.
Let's now pass that array of dictionaries into our new MultiLingualDictionary and we are good to go.
checker =spellcheck(MultiLingualDictionary('northAmerican.dic', northAmericanDictionaries)); checker.check('¿Hablas MATLAB?'); checker.check('Do you speak MATLAB?'); checker.check('Parlez-vous MATLAB?'); checker.check('Do you speak MATLAB, eh?');
I kinda wanted that last one to yell at me. That's two strikes against me from my Canadian friends today.
So there we go, a much more flexible system we have now, at least as the dictionary management goes. I leave for you the exercise to do the same thing for the spellchecking engine.
What do you think? Are you convinced? Are you an inversion of control expert already? Tell me how you apply the technique in MATLAB.
- Category:
- Design Patterns,
- Software Design
Comments
To leave a comment, please click here to sign in to your MathWorks Account or create a new one.