I've been reading about Dependency Injection and I was wondering what it would look like in VBA. Let's start with the Wikipedia definition ...
"In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client's state. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern."
Design decisions
It seems there are frameworks available to help implement this pattern in C#. I do not believe that they exist for VBA; not that I'd use them if they did exist. I want a simple implementation.
Also, I want to avoid the proliferation of interfaces because they can clutter the project explorer in VBA. Also, it seems odd to me that to facilitate loose coupling between two classes one has to define a third abstract class. Can't we achieve loose coupling using IDispatch, i.e. declaring variable objects to be of type Object? Well I choose to use IDispatch, let's see how it goes.
The Modules
In the example I have two business logic classes, TaxCalculator and TaxBiller. I have two logger classes, TempFileLogger and ImmediateWindowLogger which both support the Log method and so are interchangeable. I also have a DevelopmentIdentity class to supply identity services, I have omitted a substitute which queries ActiveDirectory. There is some client code. Finally, there is a modDependencyInjector module which holds a pool of services in a dictionary.
There are no separate interfaces as noted above. That is a design decision to prevent proliferation of classes (we already have five!).
The Logger classes, ImmediateWindowLogger and TempFileLogger
The logger classes provides logging services.
The ImmediateWindowLogger class is simple for illustrative purposes. It has a single method which takes the message to log.
Option Explicit
Public Sub Log(ByVal sMsg As String)
Debug.Print sMsg
End Sub
The TempFileLogger class is simple shares the same interface, one single method. So the two are interchangeable. TempFileLogger differs in that it writes to a temporary file.
Option Explicit
Private msTempFile As String
Private moFSO As Scripting.FileSystemObject
Private Sub Class_Initialize()
msTempFile = Environ$("temp") & "\LogFile.txt"
Set moFSO = New Scripting.FileSystemObject
If moFSO.FileExists(msTempFile) Then Kill msTempFile '* remove previous
Debug.Print "Logging to:"; msTempFile
End Sub
Public Sub Log(ByVal sMsg As String)
Dim oTxt As Scripting.TextStream
If moFSO.FileExists(msTempFile) Then
Set oTxt = moFSO.OpenTextFile(msTempFile, ForAppending)
Else
Set oTxt = moFSO.OpenTextFile(msTempFile, ForWriting, True)
End If
oTxt.WriteLine sMsg
oTxt.Close
Set oTxt = Nothing
End Sub
The Identity class, DevelopmentIdentity
This class provides identity services but the given class DevelopmentIdentity is a mock class. A proper identity services class would probably query ActiveDirectory but that is beyond the scope of this blog post. Here is the DevelopmentIdentity class
Option Explicit
Public Function HasPermission(ByVal sGroupName As String) As Boolean
HasPermission = True ' grant all to a developer
End Function
Public Function LogonUsername() As String
LogonUsername = Application.UserName
End Function
The Dependency Injector module, modDependencyInjector
So the point about dependency injection is that the business classes do not assemble their dependencies (services) themselves; it is done externally. But they have to be decided somewhere, that is why we have a separate module, modDependencyInjector, which maintains a collection (I have chosen Scripting.Dictionary) of services waiting to be injected into any new business logic classes. In the listing below one can see that one chooses Logger class and not both (which breaks the Dictionary). In a better example, a configuration file could be read to determine which services class to select.
The dependency injection mechanism is implemented in InjectDependenciesToDict() where a request dictionary is populated; this is called from a class's constructor. Technically this isn't injection; in C# a framework could read a class's meta data and be clever in its mechanism. As we are using the venerable VBA then such as clever mechanism is beyond reach.
Option Explicit
Private mdicServices As Scripting.Dictionary
Public Property Get Services() As Scripting.Dictionary
If mdicServices Is Nothing Then
Set mdicServices = New Scripting.Dictionary
'* choose a logger
mdicServices.Add "Logger", New ImmediateWindowLogger
'mdicServices.Add "Logger", New TempFileLogger
'* choose an identity service
mdicServices.Add "Identity", New DevelopmentIdentity
End If
Set Services = mdicServices
End Property
Sub InjectDependenciesToDict(ByVal dicDependenciesOfNewObject As Scripting.Dictionary, _
vDependencies As Variant)
Dim dicServices As Scripting.Dictionary
Set dicServices = Services
Dim vDependency As Variant
For Each vDependency In vDependencies
If dicServices.Exists(vDependency) Then
Set dicDependenciesOfNewObject.Item(vDependency) = dicServices.Item(vDependency)
End If
Next
End Sub
The Business Logic classes, TaxCalculator and TaxBiller
In the business logic classes below (whose purpose is self-explanatory, I hope) one can see the services being requested in the class constructor's Class_Initialize(). The returned services are kept in a class level dictionary. To access a service in the body of the class one calls into the Item method of that dictionary. So to get the logging service one writes mdicServices("Logger") and to get the identity service one writes mdicServices("Identity"). Note, that Item is called implicitly as it is the default method.
Here is the TaxCalculator class
Option Explicit
Private mdicServices As New Scripting.Dictionary
Private Sub Class_Initialize()
InjectDependenciesToDict mdicServices, Array("Logger", "Identity")
End Sub
Public Function CalculateTax(ByVal lAmount As Long) As Long
If mdicServices("Identity").HasPermission("Taxcalc") Then
CalculateTax = lAmount * 0.2
mdicServices("Logger").Log "Authorised, Calculated tax at 20%"
Else
mdicServices("Logger").Log "Not authorised to run this"
End If
End Function
Here is the TaxBiller class
Option Explicit
Private mdicServices As New Scripting.Dictionary
Private Sub Class_Initialize()
InjectDependenciesToDict mdicServices, Array("Logger")
End Sub
Public Sub SendTaxBill(ByVal lAmount As Long)
mdicServices("Logger").Log "Bill for $" & lAmount & " sent to Mr Simpson"
End Sub
Some client code
So we need to illustrate how easy it is to call this code. In the client code below, one can see the dependency injection is completely absent and thus unobtrusive.
Option Explicit
Sub Test()
Dim oTaxCalculator As TaxCalculator
Set oTaxCalculator = New TaxCalculator
Dim oTaxBiller As TaxBiller
Set oTaxBiller = New TaxBiller
Dim lTaxable As Long
lTaxable = 50
Dim lBill As Long
lBill = oTaxCalculator.CalculateTax(lTaxable)
oTaxBiller.SendTaxBill lBill
End Sub
Final thoughts
The term Dependency injection implies some mechanism takes over the instantiation and initialization of a class whereas the above code does not do that. Nevertheless, services are decoupled so the objective is achieved. Enjoy!
No comments:
Post a Comment