2

According to Why is Global State so Evil?, I know I should not allow the existence of global state, one of the reasons is so called "it makes program state unpredictable".

However, I'm not trying to oppose "not using global state", what I don't understand is, why do we say "global state" makes program state unpredictable, instead of my user requirements? Because I think "Dependency Injection" doesn't turn an unpredictable program into predictable program, for example, suppose I have a rpg game, which user has "HP" attribute, and "HP" may increase in non-battle scene (eg:use heal item), and may decrease during battle:

Global state version:

class PlayerInfo {
    static HP = 0;
}

class NonBattleScene {
    onHealItemUsed(const healItem){
        PlayerInfo.HP += healItem.hp;
    }
}

class BattleScene {
    onMonsterAttacked(const monster){
        PlayerInfo.HP -= monster.atk;
    }
}

// main function to start the app
onLoad() {
    PlayerInfo.HP=(current hp from saved data);
    this.showScene(new NonBattleScene());
}

Dependency injection version:

class PlayerInfo {
    constructor() {
        this.HP = 0;
    }
}

class NonBattleScene {
    constructor(const playerInfo) {
        this.playerInfo=playerInfo;
    }

    onHealItemUsed(const healItem) {
        this.playerInfo.HP += healItem.hp;
    }
}

class BattleScene {
    constructor (const playerInfo) {
        this.playerInfo=playerInfo;
    }

    onMonsterAttacked(const monster) {
        this.playerInfo.HP -= monster.atk;
        //some other logic
    }
}

// main function to start the app
onLoad() {
    this.playerInfo = new PlayerInfo();
    this.playerInfo.HP = (current hp from saved data);
    this.showScene(new NonBattleScene(this.playerInfo));
}

I found the both versions are "unpredictable" in terms of programming logic: NonBattleScene triggers side effect on HP, then BattleScene relies on the HP to perform other actions.

"Dependency injection" seems doesn't change the "predictableness": the place that adds HP in "global state" version still continue to add HP after implying "Dependency injection" (also for subtract HP), which the core function remains unchanged.

I think if the program is "unpredictable" when using global state, the "dependency injection" one should still remains "unpredictable" because the logic of the program remains unchanged.

So my question is, why we blame "global state" as "unpredictable", but not my user requirements? Isn't "One object modifies the state and then another object relies on that state" a necessary evil for some user requirements?

Note: I'm not trying to oppose other reasons of rejecting global state, while there may other reasons to avoid global state, eg: harder to test, performance, etc... it doesn't mean all reasons are reasonable. "Unpredictable" is one of the reasons that I don't understand, which the question above doesn't explain why the program using global state is unpredictable, but the "dependency injection" one isn't.

Also the answers currently explains the "readability" in a new paragraph: "Further, global state hurts the readability of your code....", which means "unpredictable" and "readability" are 2 separate reasons instead of "unpredictable because of poor readability". So I don't understand why would "global state hurts the readability" be the reasons of "unpredictable".

6
  • 7
    "One object modifies the state and then another object relies on that state" is not a user requirement. The user doesn't know or care about what objects you have in your specific implementation. Feb 6 at 8:05
  • 4
    This seems to be already answered in the linked thread - specifically the top answer which uses the term 'unpredictability' has gone on to clarify that its used in the context of humans reading code (as well as humans writing tests for code, and helping protect future programmers from mistakes) -- it's not about predictability from a computer's perspective - i.e. the linked answer is not talking about determinism of the program or its state; instead, the term 'unpredictability' in this context roughly translates to "unpredictable for a human trying to reason over the code" Feb 6 at 10:38
  • 5
    Why do we down-vote questions because someone misunderstands something. This person is asking a question to clear up this misunderstanding. No need to punish them for it. This is a good question, IMO, even if there is a duplicate which is obvious to other people. Feb 6 at 16:32
  • 1
    Global state is a tool, and any tool has its use. However, it also has its misuses, and those are far more abundant. "Avoid Global State" is a good guideline, but beware that there are situations where it can be used to write good code, too. It's a big "depends".
    – T. Sar
    Feb 8 at 20:23
  • 1
    I think you are mistaking dependency injection with object oriented programming. The example you give isn't really an example of dependency injection, nor is dependency the alternative to having global state.
    – Helena
    Feb 9 at 20:25

4 Answers 4

14

why do we say "global state" makes program state unpredictable

This is a question that's better addressed by working backwards. When you have a bug in your program of the form "HP is lost unexpectedly", how are you going to find it?

If HP is encapsulated, such as a private member variable of a class, then it's relatively simple to find all the places that change it. If it's a true global variable, or (as in your example) a non-encapsulated variable, that's harder.

Incidentally, this is why the "getter/setter" approach is popular: even though it looks like a globally accessible variable, the access is still going through a function, so it can be intercepted if necessary.

Now consider: what happens if you add the requirement "play a sound whenever HP is lost". How many places do you have to change?

That's why the OO version of your example would be better with loseHP() and gainHP() methods.

5

I'm not sure unpredictable is the right word here. A better way to think about it is what amount of state could alter the result of a method call and how much do you need to read to find that out. If you're making extensive use of global state the answer could be "the entire application" which in your example is fine because it's small but in a real application could be a problem.

Software is fundamentally too large to entirely fit within a human mind all at once, so it needs to be chunked up so you can reason about individual parts, global state makes that chunking difficult.

Dependency injection still expands how much state could affect a method call, but in a limited and controlled way where it's easy to determine a class's dependencies without reading the entire class.

2

In my answer to that question, I don’t refer to unpredictability, because it’s not unpredictable, what it is, is difficult to track and sequence. Every function inherently has access to globals. If mutable, anything can change it.

The big thing that OOP has over procedural programming isn’t polymorphism or inheritance, inheritance is rarely used to good effect, and polymorphism can be and was done fairly easily without it.

No, the big thing is encapsulation, which reduces the amount of code you need to hold in your head to determine if the code is correct. It gives you a medium sized block of code that you can say “this code does what it is supposed to”.

With globals, you can’t say that. It’s always possible that it should be using globals, because the code doesn’t live in isolation, it’s inherently part of something larger and it could be that it needs to either use or change a global.

Globals aren’t evil, but they come at a cost, and thus you want to limit their usage as much as possible.

2

I think part of the problem here is that you are looking at one potential issue with global state and asking about a technique that doesn't really address that issue directly. Looking at the accepted answer on the question you reference, I think I see how you could misread it as to say that DI helps with predictability. The answer points to unpredictability but then says:

Further, global state hurts the readability of your code. If your code has an external dependency that isn't explicitly introduced into the code then whoever gets the job of maintaining your code will have to go looking for it to figure out where it came from.

The author of the answer is clearly introducing a second problem with global state. What follows is a discussion of DI and how it addresses this second problem.

Dependency injection (DI) is a helpful technique for dealing with some of the other issues related to global state. Different approaches are used for addressing predictability concerns. DI is often used with those other approaches and can make implementing them easier but isn't required to use them. DI really just helps manage how different parts of your application get references to their dependencies. It doesn't help with managing the state of those dependencies.

So, how does global state degrade predictability? I think this is easier to understand if you've had to maintain applications written with a lot of global state as I did early in my programming career. One of the more egregious approaches I would run into was the use of global variables as a message passing mechanism. An example of this is shown here:

game_state = "initialized"

def start():
   global game_state

   if not game_state == "configured":
       raise StateError("game not configured")
   
   game_state = "starting"
   # ... startup
   game_state = "running"

def configure():
   global game_state

   if not game_state == "initialized":
       raise StateError("game not in initialized state")
   
   game_state = "configuring"
   # ... configuration
   game_state = "configured"

There's nothing inherently unpredictable or unstable about this. It can be made to work reliably. There are many production systems written in this style. The problem with this is that the order in which these methods are called is very important. Someone needs to make sure that everything touching that state value isn't called too early or too late in this strict order of execution. And by someone, I mean this is really best done by one person. If you have two or more people working on the same codebase, you now need coordination between developers so that they don't interfere with each other. That is a huge drag on developer efficiency and often results in disagreements and even heated arguments.

And that's assuming we are talking about a single-threaded application. Once multi-threading comes into play, these problems grow exponentially. Trying to manage state correctly in this way in such a situation is difficult to impossible. An old (bad) approach that you sometimes still might come across is where a developer has peppered sleep calls throughout the code to avoid reading or writing to poorly managed state too soon. It might work in one environment ("it works on my machine") but fails to wait long enough or waits too long in another. This isn't so much an issue with whether the state is global, but more about how that state is managed. A bare global with no encapsulation is maybe the worst-case scenario for this issue but is by no means the only way you can have these issues.

The primary tool for managing shared mutable state is encapsulation which allows access and changes to state to be controlled in a consistent way.

DI is primarily useful for a different issue that comes with global state: tight coupling. When different parts of your application all depend on the same shared state, they are inherently tied together. You cannot easily repurpose those parts because everything tied to that global state becomes, more or less, an atomic unit. This is also important but not directly related to the issues of predictability.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.