5

I’ve read the answers to Why is Global State so Evil?, and I think the negative consequences do not apply in this situation. However, that’s what everyone says just before they get hit by a falling piano and my software architecture lessons have been a few years ago, so…

We are developing a desktop chat application in Python. In that application, there is some inherently global state: account configuration, client classes (from the chat protocol library) for those accounts, open conversations, etc..

In the current design, we use single global instances for the Account controller and the Client controller (handling the client objects from the chat library we are using) as well as the Conversation controller (handling conversation abstractions for chat sessions including group chats). These global instances are available from everywhere via a simple import foo.app as app. The controllers have models associated and read-only views on those models can independently be instantiated at various places in the application.

The following remarks apply:

  • Unit testing works fine so far: we can easly mock away those global instances with Pythons unittest.mock (in fact, the instances are only initialised during actual application start up, so they are None otherwise -> instant and obvious error if not mocked but used in testing).
  • Modification of the global data state associated with the controllers goes through the controllers only; and modification of the state emits well-defined callback signals so that consumers of the state can obtain updates.
  • The code has concurrency, but it is using Python’s asyncio: this implies co-operative multi tasking, thus the points at which other tasks can interfere are well-defined and obvious in the code (when we use actual threads, global state will not be accessed directly). So it is safe to assume that global state does not change within a (non-coroutine) method.
  • Most uses of the global state are simple lookups of the type "Which Client object is associated with the Account X?", "Get me the conversation object on account X with peer Y", "Which accounts and identities exist?" and subscribing to updates.

Now I wonder: Have we overlooked anything? And if so, what would be the appropriate fix? I feel that this design could work well enough, but I’m afraid we might have overlooked anything relevant which will fall on our feet at some later point.

  • 2
    I have a strong impression that the answers you linked to already answer the question. First two answers listed about a dozen of reasons, and you took four of those reasons and claimed that you can circumvent them. Well, what about the others? – Arseni Mourzenko Jul 13 '17 at 9:14
  • 1
    @ArseniMourzenko Thanks. I omitted them for various reasons. Inflexibility is addressed by our testing approach (you absolutely have to mock any access to the global objects, thus you need to think about the dependence on that global state). The other points raised there are very general, which is why I tried to make the question a bit more specific by including the use-case for the global state here. I hoped that it would make it easier for more experienced programmers to reason about if this makes sense or we’re on the way to doom here. We’re working mostly test-driven if that matters. – Jonas Schäfer Jul 13 '17 at 9:37
  • 1
    @ArseniMourzenko Also I’d like to see my claims proven wrong. I tried to find the right balance between detail and readability of the question, so if you (or anybody) feels that more detail is added to to judge the remaining points from the linked question, I’ll be happy to add it. – Jonas Schäfer Jul 13 '17 at 9:38
  • Well, at the risk of repeating myself, I'm convinced the other answers still apply to your question. For instance, first answer: “it makes program state unpredictable”, first comment to it: “Basically, because anyone can change the state, you can't rely on it.” Second answer: “Bugs from mutable global state”. I'll stop quoting here, but there is a lot in those answers that apply to your question. – Arseni Mourzenko Jul 13 '17 at 10:37
  • What if you want to reuse components in contexts where the global state doesn't exist? What if you want to instantiate multiple copies with different configurations? When components "low down" in a program depend on what's "above", your code is inflexible. It's also harder to understand because to understand it you need to keep in mind what's above. Bad testability is simply one manifestation of poor design: good testability doesn't imply good design. – Reinstate Monica Jul 13 '17 at 18:22
3

If you push any rule far enough its going to come down to a subjective choice. But 'global state is bad' is about as canon as you get.

We all know that sometimes it is quicker to use global state to solve a problem. Or perhaps a particular framework we are using restricts other options to such an extent that Global State is an acceptable solution.

But these are extreme positions where we accept we are doing a 'bad thing'(tm) and promise to be careful.

You have stated how you have managed to avoid some of the pitfalls of global state, but not what the extreme pressure, money, time or technical limitation was that forced you to go down that route!

Given that eliminating global state is fairly simple and is almost universally regarded as producing 'better' code. Why the hell would you not do it???

If you argument is 'global state is not bad, look at this code' then the only thing to do is write the same app both ways and see which has less lines of code, or whatever measure of goodness you want to apply. But you would have to post both full code bases or link to them on github or something.

Presumably they would both be fairly similar and we would be arguing over whether being able to run unit tests concurrently etc should be part of the 'measure of goodness'

  • To be clear: we are only a few steps down that road (fortunately!). I only outlined the change to move to global objects, nothing is committed yet. I feel some things have become easier with the change, others have become worse, especially my gut feeling (which is why I’m seeking for people attacking our choice). After all the input I’ve now received here, I think I’ll put some more time into finding ways to re-design the system without globals. I might post other questions for that! – Jonas Schäfer Jul 13 '17 at 20:16
2

IMHO there is a big misconception.

The problem is not so much global state but global variables.

Each application has to maintain some kind of global state. The point is that you should not do this via global variables or the singelton design pattern (which basically is a global variable too).

There is nothing wrong with having a single instance of some class handed around to many other objects to exchange information with it. But it should not be accessed as a global variable.

  • If I pass a single instance of some state object into all my classes and use it to "exchange information". Then its effectively a global variable, yes I have avoided some of the problems with global variables because the class has a reference to the data. But not all of them. Yes all applications will have some application level state. but that state relevant to the top level of the application. Not state which is relevant to every object in the application – Ewan Jul 13 '17 at 13:17
  • @Ewan "but that state relevant to the top level of the application. Not state which is relevant to every object in the application" not sure if I get your point but the "state object" should only be passed to those objects needing it. And also there is no rule restricting the number of "state objects" to one so we can have any number (with different scopes) – Timothy Truckle Jul 13 '17 at 13:21
  • I think if you did that it would be bad. I mean i can see you might need to pass the same repository object for example to two (or more) service classes in your app. and the repository might have a connection string which is set at the app level. But its seems to me that you are suggesting that you could have a settings class with a connection string, which you pass into those services classes and have them both access it instead of a global app.connectionString? – Ewan Jul 13 '17 at 13:46
  • @Ewan "you are suggesting that you could have a settings class with a connection string, which you pass into those services classes and have them both access it instead of a global app.connectionString?" No, that would not make sense at all. Each object gets its configuration it needs. The repository does not need to know the connection string, I would pass it to the Service objects upon their creation. – Timothy Truckle Jul 13 '17 at 13:57
2

If you have a program where the core part is effectively decoupled from "global state" (or can be decoupled for testing by "mocking away those global instances", as you wrote), you can interpret all the global state data as input data, and all mutations to the global state as output data, with no global state inside the core components.

If the program fits completely into this model, and the core part does not contain any "evil" hidden global state, then your design is fine. Note there are still "satellite parts" for connecting the "global state" to the core parts. To keep the program maintainable and evolvable, your goal should be to keep those "satellite parts" as small and simple as possible - otherwise these parts have a certain risk to become the weak spot in your design.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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