0

According to Why is Global State so Evil?, I believe we should avoid global state, so suppose I have an App that count user clicks in all pages like it:

public class GlobalState{
    public int clickCount=0;
}
public class Page1{
    public void onButtonPressed(){
        GlobalState.clickCount++;
    }
}
public class App{
    public static void main(String[] args){
        Page1 pageOne=new PageOne();
        pageOne.show();
    }
} 

I know it is bad, I should change the GlobalState as parameter:

public class Page1{
    public IGlobalData globalData;
    public Page1(IGlobalData globalData){
        this.globalData=globalData;
    }
    public void onButtonPressed(){
        globalData.setClickCount(globalData.getClickCount()+1);
    }
}
public class App{
    public static void main(String[] args){
        IGlobalData globalData=new GlobalData();
        Page1 pageOne=new PageOne(globalData);
        pageOne.show();
    }
} 

Which requires us to add additional code (eg:parameters) to access the click count, reduce the chance of unrelated pages change click count accidently.

However, what I don't know is, why is it called "alternative of global state"? Because I think "clickCount" state still exists in the memory of computer at the life time of the app running, which global states still exists. And I think the global state is essential for a user requirement and cannot be removed, otherwise how can I record the user click during the app using? I think the consequence of avoid global state in this case is (say to the users): "Don't create an app that counts user clicks of all pages" !

So my question is, why is Dependency Injection called "alternative of global state"?I think Dependency Injection is just helping us to access global states in better way, but not "avoid global state" actually because the global state still exists. Access global states in better way doesn't mean no global states. Or am I missed something about Dependency Injection?

wcminipgasker2023
  • 721
  • 1
  • 3
  • 10
  • 6
    "Global" means "exists in the program's global scope" -- the object from the second example is not in the program's global scope, it is created within the scope of the `main` method and then passed down into the scope of a `Page1` instance; that means its scope is fully under your control. (I.e. you must explicitly pass the object into any function or class which needs to access it, which keeps it constrained and keeps the program's dependency graph under explicit control). On the other hand, there is no such control for methods or classes which depend upon global scoped objects. – Ben Cottrell Jul 07 '23 at 10:09
  • 2
    In short, it has nothing to do with whether the dependency is stateful, it's about maintaining control over that dependency and its accessibility. (Dependency injection is "inversion-of-control" so the dependency direction is controlled from the outside) – Ben Cottrell Jul 07 '23 at 10:15
  • @BenCottrell, I think a crucial subtlety is that DI only *enables* some additional control, it doesn't actually *enforce* additional control. You don't need to pass the shared object itself into the contexts which use it - you only need to inject something capable of getting a reference to the shared object. – Steve Jul 07 '23 at 11:47
  • 1
    "Because I think "clickCount" state still exists in the memory of computer at the life time of the app running" That's not a useful definition of global state. At the extreme, in a language that lets you use pointer arithmetic to access values at any arbitrary address, then literally all values end up as this definition of "global state" (even local variables and function parameters, which are accessible from the stack!) – Alexander Jul 07 '23 at 14:15
  • I think what mislead you is the way the question is phrased in the linked page. The person who asked was inquiring about alternatives to a design based on having globally accessible state. And alternative was pointed out that uses Dependency Injection (DI), which is fine, but DI is not specifically about managing state. DI is not about taking that state as a whole and passing it everywhere, and in fact, how state is managed and distributed throughout different classes is not even the primary concern of DI as a design technique. – Filip Milovanović Jul 07 '23 at 22:21
  • @Steve I'm not sure how that relates to my comment, which is about controlling scope and the direction of dependency, it's not about control over the state of an object, as control over state really has nothing to do with dependency injection. – Ben Cottrell Jul 08 '23 at 07:33
  • @BenCottrell, we're not at odds, but I picked up on the point that *"you must explicitly pass the object into..."*. It's actually far weaker than this - only a *path* to the object need be passed (assuming that objects can have references to one another). There's potentially a considerable amount of extra discipline (and design complexity) necessary to ensure access to those paths remains under control. The consequence is that for those who would use globals with no discipline, they will use DI with just as little discipline - so more complexity than using globals, but no more control. (1/2) – Steve Jul 08 '23 at 08:08
  • The corollary is that for those who do have the discipline to use DI to control access to shared variables, you could often just use globals and have cleaner, simpler code than you'd have with DI. The first thing the undisciplined will do with DI code they didn't write but need to add to, is hack in a globally-accessible path to the variables they want to share across scopes. (2/2) – Steve Jul 08 '23 at 08:10
  • @Steve That part of my comment is not about programmers themselves being controlled or disciplined, nor is it about whether the code is better or worse as a result; it is about the fact that DI is _inversion of control_ with regards to scope; controlled from the outside rather than the inside (Which is how access to globals would happen). My point is that DI ensures that any class or method which needs access to an object must be given that object explicitly from the outside for that object to be within its scope, because otherwise it has no way of knowing where to find it. – Ben Cottrell Jul 09 '23 at 15:21
  • Obviously when it comes to programmers directly controlling it themselves, it depends on the implementation (and typically there is an IoC container), but as always, any technique or pattern can be open to misuse if implemented poorly or used for the wrong reasons, but that's an entirely separate issue, creeping into the kind of opinionated/subjective territory about "best practice" and "right/wrong ways" which this site generally tries to avoid – Ben Cottrell Jul 09 '23 at 15:25
  • @BenCottrell, but if you store an object in a global variable, and then point to that variable from another class, the dependency between that class and that global object is also controlled from outside.and not inside - via the setting of a global variable instead of the supply of a parameter. And we've already established that the object to which the class refers doesn't need to be "explicitly" supplied - only a *path* to the object needs to be supplied. DI really doesn't "ensure" much by default. (1/2) – Steve Jul 09 '23 at 16:03
  • 1
    The discipline of how DI is used carries all the weight - which was exactly the problem with global variables, that too many lacked enough discipline! So my point, to be clear, is that you're overselling the guarantees of DI and fabricating false contrasts. Both allow external control of the dependency. Neither ensures anything by default. Both demand a large dose of discipline to use correctly, and inflict the same risks and errors when that discipline is lacking (2/2) – Steve Jul 09 '23 at 16:04
  • @Steve No, as I said above, my comments have nothing to do with control over the objects themselves (neither their state nor their lifetime), it is about control over scope and the implication that has for coupling. You seem to be focused on lifetime and mutation of object state but as I mentioned above, DI is not about state. I am not making any guarantees about control over the dependencies themselves as that is an entirely separate issue to coupling and scope. Nowhere in any of my comments have I made any claims nor guarantees about DI controlling state or lifetime of dependencies. – Ben Cottrell Jul 10 '23 at 06:55

3 Answers3

4

It's crucial to remember that the diktat against using the global scope is primarily a diktat against unstructured/disorganised access to shared variables.

The primary way in which programmers in the past accessed shared variables in an unstructured/disorganised way, was by declaring globals and reading and writing them freely from all over.

But the same result can also be achieved without the global scope, by simply making an object accessible from anywhere (or at least, from an unreasonably wide area of the program) by passing around direct or indirect references.

There's also a secondary issue in OO languages, which is that the global scope is avoided because it (potentially) prevents the objects inside a single shared library being used more than once. It's relatively common that once inexperienced developers start using global variables, they do so in a way that ties the whole codebase to being used by a single application/task at a time.

This has potentially become less relevant in recent years as libraries are often not shared between applications nowadays (due to so-called "DLL hell"), but in the past when memory was more scarce, there was greater importance in loading libraries into memory only once and ensuring that the same code could service callers from multiple applications.

Steve
  • 6,998
  • 1
  • 14
  • 24
  • 1
    On the other hand, it has become more relevant in recent years with the widespread deployment of multicore processors and deep IO queues, leading to much more significant use of parallel and concurrent threads/tasks within a single application. – user1937198 Jul 07 '23 at 13:09
  • Objects not being usable from multiple threads because they share state that is either not safe or not efficient to access in a parallel manner is a major issue. – user1937198 Jul 07 '23 at 13:12
  • @user1937198, certainly, although to be clear, my point under the second heading was not that globals in shared libraries are thread-unsafe, it's that they would share application-specific data between *completely unconnected applications* where sharing of variables was never intended to occur - this is because globals are allocated per-executable. Thread-unsafe code is usually a very advanced form of unstructured/disorganised access under the first heading, rather than a case of inappropriate sharing under the second heading (although it can happen that threads wrongly share variables). – Steve Jul 07 '23 at 15:09
0

Global state and static memory are not the same thing. Once state becomes global you can access it from anywhere. No UML diagram or method signature is going to help you track down where it's getting used. You have to dig through the code.

Static memory, however, is not something you get to touch from anywhere just because it's static. Static is just about it's allocation and lifetime. But if no one gave you access to it then it doesn't matter if it still exists. If you can't access it without something handing it to you then it's not global.

public class App{
    public static void main(String[] args){
        NotGlobalState notGlobalState1=new NotGlobalState();
        Page page1=new Page(notGlobalState1);
        page1.show();
        page1.onButtonPressed();
        page1.show();
        NotGlobalState notGlobalState2=new NotGlobalState();
        Page page2=new Page(notGlobalState2);
        page2.show();
        page2.onButtonPressed();
        page2.show();
    }
} 

Which either shows:

0
1
1
2

or

0
1
0
1

Depending on if clickCount is static.

Here we see two pages accessing state that will live as long as main. Yet they aren't sharing the same variable unless you declared clickCount static. But either way, with encapsulation there is no way to access clickCount globally. It's protected from being accessed from places it was never sent. Static or not.

public class Page{
    public NotGlobalState notGlobalState;
    public Page(NotGlobalState notGlobalState){
        this.notGlobalState=notGlobalState;
    }
    public void onButtonPressed(){
        globalState.setClickCount(notGlobalState.getClickCount()+1);
    }
    public void show() {
        System.out.println(notGlobalState.getClickCount());
    }
}

And if you have to send it to access it then it's not global. Even if it is static:

public class NotGlobalState{
    private static int clickCount=0;
    //private int clickCount=0;
    public int getClickCount() { 
        return clickCount; 
    }
    public void setClickCount(int clickCount) { 
        GlobalState.clickCount = clickCount; 
        //this.clickCount = clickCount;
    }
    public void click() {
        GlobalState.clickCount++;
        //this.clickCount++;
    }
}

Static or not, it's encapsulated. So you have to be handed a reference to this to mess with it. Thus, not global. Even if it lives as long as main does.

Avoiding globals is all about formalizing access. You can make it live as long as the app without giving it to everything.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
0

If we consider a standard web app as an example, it is likely that it will have some global state for things like:

  • Initialization parameters/config.
  • DB connections / connection pools.
  • Logging.
  • Statistics/Performance metrics.

If you are using a framework it will often abstract most of the details of the shared/global state away - for example injecting configuration parameters/objects into the classes that need them.

Taking your example (click count) this could be re-located into a number of places depending upon your use cases, it could:

  • become a record in a database.
  • be calculated in a data warehouse that ingests the logs.
  • be handed as part of a statistics/metrics package.

The primary advantage of re-locating this "global state" into a system that is designed to handle it, is that it provides a mechanism for you to scale your app servers horizontally (add additional app servers). Global state is only global to (typically one process and) rarely more than one machine.

As others have stated, having a big blob of code that writes to database/store from random locations will make it difficult to trace access patterns, it's still worth creating useful classes/abstractions to group database operations logically.

DavidT
  • 1,074
  • 3
  • 5