What is global state?
Join the DZone community and get the full member experience.
Join For FreeInsanity: doing the same thing over and over again and expecting different results. -- attributed to Albert Einstein
Global state is essentially the same thing as insanity in this definition: a way to affect the execution of code hidden from sight, so that two apparently identical lines actually produce a different result depending on some external factor.
For example:
new SomeClass()->printId(); new SomeClass()->printId(); // output: 1, 2
has some global state (a static counter) affecting a field inside SomeClass instances. Therefore, it may not be easy to replicate scenarios (like in tests) multiple times.
Examples
Global and environmental variables, along with constants are simple examples of global state. The same goes for configuration directives and files which code silently depends upon, as long as they are global for each instance of the affected objects.
Speaking about objects singletons and static classes containing fields are another example of global state. More subtle cases are hidden localizations like translations of output and of symbols (LC_ALL?)
Parameterization is made difficult by global state, either because the seam for collaborators is hidden (config files and enviromental/global variables) or not accessible (singletons).
Testability
When there is some global state in an application, the affected unit tests won'tbe isolated from each other, and may change their results when run alone or in a different order with respect to being executed inside the whole test suite. Global state is one of the most common problems while working with legacy code which was written with little concern for testability (and separation of concerns).
A typical annoying example is a test that passes when alone, but fails in the full suite due to some state left lingering from previous tests. Usually, it is then debugged by executing the exact same test twice or multiple times in a single process and verifying that it passes consistently.
Actually global state cannot be always removed, even in a test environment: what this move would achieve would be a fully parameterized system, too general to be useful; imagine configuring every class name in your application, even in Factories.
It may be simpler to test with some global state in, like in the case of a default locale defined in place of stubbing the Translator object; or in the case of a Fake database connection instead of a Stub or a Mock.
Taking this approach to the extreme, we notice that global state is often hidden from our view because it's taken for granted. Base classes offered by the language (e.g. String) are not mocked or substituted by test doubles, even when they have quite some logic in them; all the classes and functions contained in our applications are global state as their implementation cannot be substituted, yet we don't consider them a trouble as singletons.
Constant
There is a reason why not all global state is necessarily bad: constant global state to allow context-free reasoning about code, and simplify testing and reuse of code considerably.
In fact, the very definition of state (for example from hardware logic networks) is that of a component that can change its behavior in time, keeping information about previous inputs. In short, any computation that is frequently accessed but does not have the capability to change its result or to produce side-effects is not state (it is global). ROM is instead considered a purely combinatorial network, not being real "memory" but merely a function translating addresses to words.
A singleton changing its responses after some calls is global state that makes testing difficult; a static class containing only pure functions may make tests long winded and infringe the object-oriented paradigm, but it's not as dangerous as the former.
However, that's why I see monkey patching in dynamic languages as problematic. Monkey patching commonly consists of open classes where you can add methods at any time after their initial definition.
class Array def sum inject {|sum, x| sum + x } end end
When you see a call to this method, you have to ask some questions:
- where it was added in the code base? Which sourcefile should I look at?
- When it was added to the code base? Am I sure that definitions can only be included at startup and I am not calling the method before it is defined?
- Are there multiple redefinitions of the method? Maybe from other libraries or code to integrate?
The same issues happened for prototype.js, which modifies the prototype object of base JavaScript objects like Array, effectively redefining and adding methods. The result is little interoperability with other libraries.
But even monkey patching should be fine as long as the modifications are really constant and definitive. If you use a single framework (like Ext Js), and you always use it in all pages of the application and in tests, then it can monkeypatch the base classes of the language safely, without making you debug a method that works in one environment but not in the other.
Conclusion
Global state is not only a global parameter for the internals of your application, but also the product of stateful interaction that changes the output and side-effects of code in different invocations.
Making global things constant is the first step towards simplifying reasoning about a design and raising testability. If you are able to run a unit test twice in the same method, you are officially free from global state in that scenario.
Pay attention when embracing open classes, editable prototypes, embedded calls to registries and files, and so on: they can add several dimensions to the variables that can affect the result of a piece of code. They will hide a dependency but not making it go away.
Opinions expressed by DZone contributors are their own.
Comments