Alternatives To Global Variables
Global variables aren’t evil but you should avoid them unless necessary
--
Global variables are accessible from anywhere which makes them convenient and useful but also dangerous as they can be misused.
One could argue that they let you pass or access data across scopes or classes easily, could be handy in embedded applications with limited memory, and are also helpful in tracking value changes in debugging since all it needs is a single watchpoint.
And to be fair they don’t pose much of a problem in smaller applications when used in limits. But as your application evolves, it becomes increasingly difficult to understand and track whose reading and writing the values. Maintainability becomes a headache.
Disadvantages of Global Variables
Before we look into the possible alternatives, let's dig through the issues global variables throw at us and why using them is considered a bad idea.
Violates Encapsulation
Encapsulation refers to restricting direct access to data. Instead, it uses public accessor methods(getters and setters) as boundaries to securely access the data and hide sensitive information. In doing so, it manages to make the code easier to understand and reduces the coupling. Global variables behave in a completely opposite manner thereby breaking encapsulation.
Unit Testing Issues
Unit testing requires clean isolated environments. This can become a nightmare when there’s a global mutable state hanging around as it will only tie up the states together. You’ll never be sure if the global state was changed in the previous unit test. So, you’ll have to set all globals to predetermined values before every test. That’s an overhead.
Dealing With Concurrency
When more than one component can modify a global state, it can lead to inconsistent data, especially in multi-threading environments. While you can add locks and synchronizations to handle race conditions, more often than not, it leads to blocking of threads which can cause slow execution of the application.
Naming Conflicts
Global variables can also lead to namespace pollution wherein the same name is used in local and global variables. One can end up with naming conflicts when linking modules. Also, in a language like JavaScript, you could end up creating a global variable accidentally by forgetting var
. You can then unknowingly start using a global variable when the intention was to use a local variable. A workaround to avoid such conflicts is by using prefixes on global variable names.
While you could use a static class or an object literal as a container for all the global variables, it doesn’t really resolve the above-mentioned issues. Let’s look at a few alternatives that do better than that.
Singletons: The Fancy Option
Singleton pattern is one of the well-known design patterns which has often been scrutinized or rather hated for behaving as an anti-pattern.
At the core, Singleton lets you instantiate an object only once.
Now it may seem like singletons and global variables are the same as both are globally accessible, but it isn’t the case. Let’s look at the characteristics that identify a singleton instance and how it’s different from a global variable:
- It has a private constructor which ensures that no more than one instance can be created at a time.
- The values present in a singleton is modified through accessor methods.
- It cannot be freed until the program is terminated.
- The instance is typically created when it’s first accessed, unlike global variables that are created on program initialization(Note: in Swift, global variables are lazy by default, hence created only when you use it the first time).
In a scenario, where you need one central global point of access across the codebase, singletons are a good alternative for global variables.
We can call a singleton as a lazily initialized global class which is useful when you have large objects — memory allocation can be deferred till when it's actually needed. Singletons have practical use cases in logging, databases, and configuration managers.
Let’s look at how to create a singleton in Swift:
While singletons help reduce namespace pollution and naming conflicts, refactoring a codebase with them around isn’t easy. Dependencies are hidden which makes it difficult to inspect code at times. Also, they don’t offer thread-safety inherently. For example, to ensure a singleton class is thread-safe, you need to implement DispatchQueues in Swift. For Java, the getInstance()
method needs to be set to synchronized
.
For JavaScript, the Module Pattern can be used to hide properties and reveal only the ones that we want to be publicly available — thus shielding some parts of the global scope and preventing naming conflicts to an extent. Alongside Singletons, we can use it to create an accessor for the instance
property.
Dependency Injection: The Better Bet
Dependency Injection is a technique that allows the creation of objects outside of a class and provides the dependencies in different ways. Simply put, if an object A needs another object B you simply pass it — object A doesn’t need to get into the details of B. This creates an explicit dependency which when done properly helps in structuring the code in a modular way.
Now, one could easily start passing parameters down a chain of functions which could lead to a long list of parameters, code complexity and spaghetti code. The key is to know when you need a dependency and inject it accordingly using the following three ways:
- Constructor Injection — Dependencies that are required at class initialization should be injected in the constructor. The dependencies are declared as parameters of the constructors. This ensures you cannot create an object until the dependencies are ready.
- Setter Injection — Dependencies that can be changed on a need basis(partial dependencies) or are there to extend/override the functionality should be passed in setter injection. While this may look like a more flexible and better readable option than constructor injection but it has it’s own problems as well — you could end up with circular dependencies.
- Interface or Protocols— Using this way, the class conforms to the protocol or interface and exposes methods for injection. In essence, you create a common interface for multiple classes and pass on their concrete implementations as a dependency. This approach is useful when the dependencies may vary and you’d wish to swap them easily.
Depending upon your codebase’s architecture you could use any or mix all of the above injection tools.
Dependency injection is a better bet over global variables and singletons when used judiciously. It helps keep the code loosely coupled. This makes reusing, refactoring and unit testing far easier while also ensuring there are no hidden or fixed dependencies. For instance, using DI, you can quickly interchange services — like a mock and production API— in a plug and play fashion provided they conform to the same protocol.
Conclusion
Remember, Singletons and Dependency Injections are no like for like replacement of Global Variables. But they do help address certain pitfalls like tight coupling, namespace pollution, encapsulation, and better unit testing. It’s important that you don’t get bogged down in a particular way and instead choose the best alternative according to your use case. You could use singletons with DI for example.
Keep global variables to a bare minimum in the worst-case scenario in order to avoid an unmanageable and chaotic codebase.
That’s it for this one. Thanks for reading.