Creating Optional Dependencies
In my article yesterday, I introduced the "Dependency Train" concept. That's what happens when you import a function from your code library, but you end up having to import several additional modules just to satisfy all of the dependencies. You're left with a whole "train" of code modules when all you needed was a single "seat" (function).
You end up in this situation when you're modules are tightly coupled. So what can you do about it? There are a few ways to handle this situation.
Violate "don't repeat yourself"
One way to preserve loose coupling--which results in more "standalone" modules--is to create private copies of the functions from the source module in the calling module. This eliminates the dependency between the two modules, but it does result in repetitive code.
The concept is simple. You copy the function from its primary code module. You then paste it in the calling module but mark it as private to avoid ambiguity.
This makes sense in the following situations:
- Simple methods with no complex logic.
- Procedures that are unlikely to change.
- When the routine is part of a much larger module and you don't need any of the other functions in the module. Copying the one function avoids bloat.
This approach has the following downsides:
- If there is a bug in the copied function, you will have to fix it in many places.
- You have code duplication if you end up importing the source module anyway.
Pick your battles
I used to go to great lengths to keep all of my standard code library modules completely standalone. The problem was that it resulted in a lot of code duplication. The reason is that most of the functions I was copying to other modules for private use came from modules that I was importing into my application anyway.
A prime example of this was my StringFunctions module. That module has several simple methods that exist largely to make my code more readable. For example, I have a Conc()
function that I was including as a private function in more than half of my code library modules.
Over time I realized that I included that StringFunctions module in all of my projects. I was never introducing a new dependency when I called a function from that module. I was wasting time and introducing duplicate code for little or no benefit.
There were a few code modules that I could safely assume would be in every application. Those were the modules with functions that I used most often. Which meant that many of these dependencies could essentially be ignored.
I now maintain a "Standard Library" of code modules that I import into every new project at the very beginning. I freely call functions from those modules now secure in the knowledge that I won't be introducing new dependencies.
Use a unique comment token
One of the modules in my "Standard Library" is a class module (clsApp) that includes application-level properties and methods, such as the current user name and the title bar text. I also expose other class modules from within clsApp, such as clsStatus and clsRegistry, which provide more readable access to the Access status bar and the Windows registry, respectively.
However, I don't need access to the status bar or Windows registry in every project. So, to avoid creating a dependency on the clsStatus or clsRegistry classes, I would comment out the code referencing those classes using a unique "comment token."
This is easiest to demonstrate with an example:
' Notes
' - Find and replace '$$ with blank to enable Status property (requires clsStatus)
' - Find and replace '&& with blank to enable Reg property (requires clsRegistry)
'$$Private m_objStatus As clsStatus
'&&Private m_objReg As clsRegistry
'$$Public Property Get Status() As clsStatus
'$$ Set Status = m_objStatus
'$$End Property
'&&Public Property Get Reg() As clsRegistry
'&& Set Reg = m_objReg
'&&End Property
Private Sub Class_Initialize()
'$$ Set m_objStatus = New clsStatus
'&& Set m_objReg = New clsRegistry
End Sub
If I wanted to enable the Status
property of the above class, I could perform a global find and replace on '$$
.
This worked fine for awhile, but it always felt kludgey to me. Probably because it was. The other thing to note is that the comment tokens would need to be globally unique throughout my entire code library. This would have been a maintenance nightmare if I stuck with this approach for long.
Use conditional compilation
A much cleaner approach is to take advantage of conditional compilation. Those are the lines in VBA that begin with a pound/hashtag sign ("#"). Lines that begin with that character are subject to "preprocessing."
What is preprocessing? That is a step that programming languages take prior to compilation. So, before any compile time checking occurs, the preprocessing lines are evaluated. This allows us to place code that would otherwise fail to compile into our projects.
How can we take advantage of this with our code libraries? Again, this is simplest to demonstrate with an example:
' Notes
' - Replace the '$$ and '&& kludges with conditional compilation
#Const EnableStatusProperty = True 'If True, requires import of clsStatus class
#Const EnableRegProperty = False 'If True, requires import of clsRegistry class
#If EnableStatusProperty Then
Private m_objStatus As clsStatus
#End If
#If EnableRegProperty Then
Private m_objReg As clsRegistry
#End If
#If EnableStatusProperty Then
Public Property Get Status() As clsStatus
Set Status = m_objStatus
End Property
#End If
#If EnableRegProperty Then
Public Property Get Reg() As clsRegistry
Set Reg = m_objReg
End Property
#End If
Private Sub Class_Initialize()
#If EnableStatusProperty Then
Set m_objStatus = New clsStatus
#End If
#If EnableRegProperty Then
Set m_objReg = New clsRegistry
#End If
End Sub
The best of both worlds
As you can see, this is a very clean way to avoid the "Dependency Train" problem.
It allows us to create optional dependencies. Every piece of code that relies on another code library module gets wrapped inside a conditional compilation #If ... Then statement. The conditional compilation constant(s) are all listed at the top of the code module.
Now when we re-import an updated version of our code library module, we simply have to go through and set the conditional compilation flags to much what was there before. If we don't remember how the flags were set, we should be able to keep compiling and adjusting flags until the project fully compiles.
And if we're using version control, we don't have to worry about forgetting what was there before.
Image by Bruno /Germany from Pixabay