Dependency, Injected
Now that we have conquered inversion of control, we can explore a common application of the technique, dependency injection. Dependency injection utilizes the inversion of control to, with the aid of a dependency injection framework, construct an application through configuration rather than code. This allows your application to be distributed to perhaps many different customers with many different needs, and rather than changing the foundational code to suit these needs, you can provide for them a configuration file as well as the specific implementation of the code they need. However, the core infrastructure can be shared among many different and varied clients because the core infrastructure does not directly depend on any of the concrete implementations of the code needed for each customer.
In other words, using our spell checking example, you can separate the core spell checking engine and interface along with the locale specific dictionaries and distribute them separately. We showed last post that this can be done using our inversion of control technique, but it requires your end user to build up the application. If you gave a user of your software the spellchecker and the MexicanSpanishDictionary, they still would need to create it by doing something like:
checker = spellcheck(MexicanSpanishDictionary)
checker = spellcheck with no properties.
This seems easy enough, but in real world applications you have more than one dependency and it is likely not this easy to construct your entire application's object graph. However, using dependency injection you can create your application and distribute it with different behaviors, in our case with a specific configuration for each locale we'd like to support.
If you are intrigued by this possibility, as it turns out you are in luck because there is a dependency injection framework on the file exchange to help out with this.
But first, did you do your homework? You may remember last post I suggested continuing the inversion of control technique on the spellchecker but applying the technique on the engine as well as the dictionary. Anyone do it? If not go read that post and try your hand at it because I am about to show you the solution (SPOILER ALERT) so we can extend this concept here. I'd like to show how dependency injection can be used across at least a couple layers so that going through that exercise will be helpful.
Alright the key here is that even though the spellcheck constructor was handling both the Dictionary and the Engine (Jazzy), for our purposes it really only needed the engine. The engine is what needed the Dictionary, and in fact it is conceivable that there may be some engines that don't rely on the same Dictionary interface at all. What did need the Dictionary was the Jazzy engine. So it follows that instead of the spellcheck class asking for the Dictionary in its constructor, the spellcheck class can simply ask for the engine. To do this we need to define an interface for what the engine needs to provide:
classdef SpellCheckEngine < handle methods(Abstract) checkSpelling(engine, inputStr); end end
Then we can operate on that interface directly in the spellcheck code:
classdef spellcheck properties(GetAccess=private, SetAccess=immutable) Engine; end methods function checker = spellcheck(engine) % Hold onto our spellchecking engine checker.Engine = engine; end %% CHECK Method to check an input string function check(checker, inputStr) checker.Engine.checkSpelling(inputStr); end end end
Finally, let's add our specific Jazzy engine and see how we can establish the same behavior as last post:
classdef JazzySpellCheckEngine < SpellCheckEngine properties(GetAccess=private, SetAccess=immutable) Jazzy end methods function engine = JazzySpellCheckEngine(dictionary) engine.Jazzy = com.mathworks.spellcheck.SpellCheck(); engine.Jazzy.setDictionary(dictionary.DictionaryFile); engine.Jazzy.verbose = true; end function checkSpelling(engine, inputStr) engine.Jazzy.checkSpelling(inputStr); end end end
s = spellcheck(JazzySpellCheckEngine(AmericanEnglishDictionary));
s.check('¿Hablas MATLAB?')
Input : Hablas Suggestion: tablas
Great! We are in business and each class looks very clean with a single responsibility. In fact, they are not even responsible for constructing their own dependencies! This is really an application of the Single Responsibility Principle. Something else has the responsibility of wiring all these objects together, not the objects themselves in their constructors.
Add some DI
OK, now the person responsible for creating your full object connections should not be your end user. You can see with even two collaborators that it is starting to get verbose.
s = spellcheck(JazzySpellCheckEngine(AmericanEnglishDictionary))
s = spellcheck with no properties.
Instead let's make it a configuration. The first step that is needed in order to leverage this dependency injection framework is to opt into allowing the framework to construct instances of your objects by deriving from the DI framework's mdepin.Bean interface. Note, I have placed the version of the spellcheck code which uses dependency injection into a package called di to keep the dependency injection version seperate from our first spellchecker. When we derive from the Bean interface we need to also pass the configuration passed into the constructor up the class hierarchy to the Bean superclass. For our classes this looks like the following (placed into a +di folder to place it into the package):
classdef spellcheck < mdepin.Bean properties Engine; end methods function checker = spellcheck(config) checker = checker@mdepin.Bean(config); end %% CHECK Method to check an input string function check(checker, inputStr) checker.Engine.checkSpelling(inputStr); end end end classdef SpellCheckEngine < mdepin.Bean methods(Abstract) checkSpelling(engine, inputStr); end methods function engine = SpellCheckEngine(config) engine = engine@mdepin.Bean(config); end end end classdef JazzySpellCheckEngine < di.SpellCheckEngine properties Dictionary end properties(GetAccess=private, SetAccess=immutable) Jazzy end methods function engine = JazzySpellCheckEngine(config) engine = engine@di.SpellCheckEngine(config); engine.Jazzy = com.mathworks.spellcheck.SpellCheck(); engine.Jazzy.setDictionary(engine.Dictionary.DictionaryFile); engine.Jazzy.verbose = true; end function checkSpelling(engine, inputStr) engine.Jazzy.checkSpelling(inputStr); end end end classdef Dictionary < matlab.mixin.Heterogeneous & mdepin.Bean properties(Abstract) DictionaryFile end methods function dictionary = Dictionary(config) dictionary = dictionary@mdepin.Bean(config); end end end classdef AmericanEnglishDictionary < di.Dictionary properties DictionaryFile = 'en_USx.dic'; end methods function dictionary = AmericanEnglishDictionary(config) dictionary = dictionary@di.Dictionary(config); end end end
The only differences here are the superclass lists to derive from mdepin.Bean and the changes to the constructors to both accept the configuration and forward that configuration to the superclasses.
However, now we can let the configuration create our object graph for us! First we create a structure for this context. Note that this configuration is specified as a structure, which feels a bit like code rather than configuration, but that is because the framework supports this context as a struct out of the box. This can very easily be extended to create a context class from a more classically declarative form like JSON or XML. You just need to create your own subclass of mdepin.Context in order to produce these forms of configuration. For our part, the structure version of these declarations will do just fine:
% Define the spellcheck class and give it a label "spellchecker" ctx.spellchecker.class = 'di.spellcheck'; % Assign a label in the configuration which defines the required engine ctx.spellchecker.Engine = 'spell_check_engine'; % Link the JazzySpellCheckEngine to our engine label ctx.spell_check_engine.class = 'di.JazzySpellCheckEngine'; % Assign a label in the configuration which defines the required dictionary ctx.spell_check_engine.Dictionary = 'american_english_dictionary'; % Link the AmericanEnglishDictionary to our dictionary label ctx.american_english_dictionary.class = 'di.AmericanEnglishDictionary';
With this structure based configuration the framework can then construct our full object graph for us.
s = mdepin.createApplication(ctx, 'spellchecker')
s = spellcheck with properties: Engine: [1x1 di.JazzySpellCheckEngine]
Does it work? If so it should find a typo here because we are using the English dictionary.
s.check('¿Hablas MATLAB?')
Input : Hablas Suggestion: tablas
¡Absolutamente!
There we have it, we have now separated out our construction logic, and using the dependency injection framework have made it such that our construction logic can be configuration rather than code. This allows flexibility in distribution of our MATLAB software packages because different clients can use different configurations.
Should everything use dependency injection? Not necessarily. It has some definite upsides to it, but it does require an additional dependency on a DI framework, and it can affect the way the software is written. For example, in this example we needed to modify our classes to derive from something outside our domain model (the Bean) and our constructor signatures are affected by the use of the framework. Using manual constructor injection is definitely a valid choice, it just requires that other collaborators in your software (factories perhaps) have the responsibility to construct the object graph. If you don't mind the framework dependency and you'd like to delegate that responsibility to a tool then a DI framework can work quite nicely for you.
Another related pattern is a Service Locator, which also has its own pros and cons, but supports similar use cases with a more dynamic resolution of needed dependencies. Many view Dependency Injection as a competing pattern to Service Locators. I tend to view them as complimentary. I'd like to cover a post on Service Locators in MATLAB in the future, but for now let's all take a break from the topic for a bit. That's code-speak for "We have some cool posts in the pipeline on some different topics." Remember that R2016a comes out in early March! hint grin
In the meantime have you ever wanted a DI framework in MATLAB? This file exchange example is relatively new so definitely give it a go. Can you see a dependency injection framework helping with your production code?
评论
要发表评论,请点击 此处 登录到您的 MathWorks 帐户或创建一个新帐户。