I find “god objects” completely unproblematic. It’s trivial (although, granted, can be time consuming depending on what IDE tooling you have available) to refactor them if you need to split off some state for some reason. You have the entire set of fields right there for auditing. It’s great. Literally just cut and paste the code into a different struct and chase down the compile errors. On the other hand, tracking down uses of global variables is perilous. You might get halfway through a massive refactor and realize that this logic which you thought was self contained, actually has a dependency on global variable, connecting it intrinsically to some other system that now ruins your whole refactor.
Globals are a PITA when testing. A god object can at least be mocked. No contest.
couldn’t have said it better myself, and an underrated benefit of god objects is that in any debugger you only have to register one variable to track the state of the app, so convenient.
What’s even the argument against “god objects”? It’s just a thought-terminating cliche.
I believe the argument against is in unmaintainability since the more the object grows the more things is probably doing and different things might end up intertwined.
But i don’t think is automatically true, it’s more about how much the parts are growing and if you are using the objects in different contexts even. Also, it might be subjective how much someone find it easy to work on it.
I prefer to reason on small objects tho, it’s giving me a feeling of control.
I would say there’s no argument for globals and against “god objects”, but there are definitely arguments against “god objects” in favour of individual arguments from a readability perspective.
The same refactoring argument you made above applies a bit here as well - with larger context objects it can be hard to spot that you don’t need all that context and could maybe refactor to a subsystem that just needs Io, for example. But this is mostly manageable by just writing better code and avoiding the lazy trap of extending or passing context unless you need the majority of the info.
Nothing in particular, for me they are mainly just a different way to structure the project, without any intrinsic dis-/advantages. Yes you can mock them in testing, but you can also e.g. setup your global allocators to rewire to the testing allocator in tests, or manually initialize everything you need for a particular test. I also don’t find it particularly difficult to refactor with globals, but maybe that’s just because I’m working on a much smaller project where I’ve at least read every single line.
To give an example where I prefer the structure of globals: I have a bunch of graphics pipeline objects (shader + some additional options) throughout my codebase, and in my opinion these should be stored and initialized locally in the same file as their main use cases (but sometimes I do want to reuse them in other files). With a god object I think you’d either centralize all the pipelines and their initialization in one place, or end up with a giant object that repeats parts of your file structure, both are not desirable for me.
I’ve only really encountered this problem when coding Haskell, which is about the only other language that takes “dependency injection” as far as Zig does. In Haskell it manifests as the monad your working inside (i.e. your composite “context type” that gets passed from function to function) becoming 10 layers deep. It’s almost a “context stack” and you have to “lift” functions to work on the right layer of the stack. It gets horrible at times.
When I find myself in that situation I take it as a “code smell” and a sign that I need to think about separation of concerns more. Functions which concentrate on working with just one or two parts of the context, and not the whole thing. Not always possible, and sometimes you end up with a gordian knot of stuff, but I try to keep the area where that exist to as small of a “nexus” as possible.
People have talked about “context objects” vs “global state”. I would say the choice depends on what your talking about. With a logger, I’m fine with it being module-global, because if I move the code I’ll want it to use the new module-global logger. For the application config, I feel it should live at the top of my application and only the specific parts necessary should be passed in to the lower level code.
Edit: Extra deciding factor for where I put things: mutability. I don’t have function mutate globals. You need to be able to track who is updating what, so that can mean a global gets passed as an argument in some cases because that function will write to it.
I will also add to the replies above that the term comes from OOP, where self is passed as-is into every method without the ability to fine-tune what the method gets access to. In the worst case, it’s equivalent to a global context – self contains everything ever needed by any method, and all code is laid out as 100 methods of this one class.
I’ve encountered this at work, and it really hurts the ability to reason about the code locally without wading through much more than you’d have to otherwise.
When using regular functions (without the self), it’s a lot easier to pass down only those pieces of context that are actually needed. However, another great approach I’ve seen in languages with advanced type systems is using intersection types for this narrowing down. To give a pseudo-code example:
struct Context:
alloc: Allocator
io: Io
logger: Logger
def fn(ctx: Intersection[Allocator, Logger]) -> int:
ctx.io # reported as a type error
This way it’s still quite ergonomic because you pass around one value of the same type, but you can see just from the signature that fn doesn’t do IO.
There is a crucial difference: it allows creation of multiple instances, whereas globals do not (you would have to create another instance of the process itself to get the same effect).
It matters when the wall is near.
Global can be measured, God cannot.
For runtime and config.
Check if long-lived data can be extracted and structured before it passes to there.
What will come in to there need to check, what is the minimum and maximum.
Any possibility of a simple what will happen inside that process? Check it out.
Config may have 1 till a few levels deep. Does the config really do only configuration?
If not, can that config be exposed in front of the first public API? Is the intent clear? If God Object has ctx that can be shared upfront but can be controlled by others is not a God.
This *Global Object need to be tame based on your couples reply.
My safe assumption is only a few but the intent is not clear (not narrowed enough).
Maybe for the ergonomic logger use typed per-domain methods, not one generic func() for the logger with some level derived, not passed. Don’t make the caller choose a level, make it minimal (or no option).
Your real problem is more like in lifetime, copied allocation, and dependencies control.
Lifetime needs a timeout. Pass to queue. Queue failed to retry? Notify the caller.
Some dependencies that haven’t wrapper yet where it has a corruption layer need a wrapper.
It seems you may create another Global Object, but now with clear usage. Seems like one struct passed to anywhere? If you can’t control it, isolate it.
Some previouses replies from others also have context from my suggestion.
Not sure with copied allocation (yet),
I’m still figuring properly managed mem size purpose while squeezing memory consumption at runtime.
Hope you get where you expected.
In Zig files are structs aren’t they? Up until now with my (relatively small) projects my god object is something like “lib.zig”. Why pass them around everywhere?
I agree that there is a difference, but if I understand correctly, multiple instances give you the ability to pick, for example, a different allocator for a particular instance instead of being stuck with a global one. Which is valuable in its own right, but I don’t think it resolves the issue I mentioned – not being able to tell at a glance what capabilities of self a method uses without reading its (and its callees’) source code.
I know OOP has a bad reputation, but the idea behind dependency inversion has nothing wrong.
You don’t carry a full, flat set of dependencies. You carry a web of dependencies.
In a complex system, you must think in terms of slices with incomplete knowledge. You must yield control to some sort of control plane. Managing dependencies is part of that. Your coding units should always have a context that is small enough to reason about. But if you open up every detail the complexity is innate and you cannot reduce it. So the crux is NOT to usher the complexity here and there, debating over globals or god objects. It only changes the form but doesn’t reduce complexity in any sense.
I’d usually think of dependencies as capabilities I need for the current task, without diving too deep into how it’s implemented. Again, this is not to advocate for a “OOP” pile of shxt, but it is a call to the human in front of the screen – when your brain cannot hold that much complexity, you know to delegate some out of it.
And there is no mechanical, static answer as to when you should extract some dependencies into capabilities (interfaces in any sense). It ultimately depends on your understanding, and your knowledge evolves. The more you understands a problem, the more you are able to hold in your brain at the same time, so trying to optimize for a universal, optimal layout of a program is to hit a carrot pod tied to your own neck. The more your are stuck in the direction trying to “perfect” the program, the more you are building a pyramid only you know how to navigate in.
So, dependencies are never satisfying. The fact of using a dependency implies imperfection – you are incapable of managing every detail here, so you are asking from the environment for that capability, and also giving the env a chance to share / optimize from a global point of view.
Now, back to the question. If you feel that you are carrying too much, then it’s a perfect time to admit that you are not able to hold that many threads at once. It’s no shame to admit. It’s time to build some “expert subsystems” to handle details that are less important to the subject under discussion. If you do find it tedious to carry allocators, caches, io, app config, runtime context, trce, logger, zstd (oops, that’s really a lot!), then don’t be shy to gate some of them behind an expert object, sacraficing some unproven performance, for mental wellbeing.
@andrewrk has no problem with a big object, not because he loves making god objects everywhere, but because he knows the fields pretty well as a veteran system programmer, and to that point the problem for him isn’t the same problem for op, making dependencies explicit does remove a type of surprises from code review. My suggestion to op is that you take the best of both worlds. Globals are indeed bad because they are implicit, and you have to dive into the body of functions to spot them, but that also doesn’t mean you have to build the most optimal but unhumanly pile of data segments in your app. Make some expert objects.
To begin with, I can think of these groups:
- io & caches → smart io object, maybe also carry allocators here,
- app config, runtime content → domain object, maybe trace id and logger can also live here.
- zstd, this is pretty focused compared to other dependencies, so i guess this is important to your use case. If not, maybe tuck it into that smart io object too.
Every junior dev can jump out and point to you: hey this is inefficient, this is against some random motto they have in their programming aesthetics, but ultimately you own that code, and you choose the mistakes to make. To me I think undisciplined implicit deps is a filthy crime much larger than a web of dependencies that look so inefficient. The latter I know pretty well how to fix, and so I can spoil myself for not solving it now; the former is what bit me so hard that I don’t want it to incubate in the first place.
I just thought that one could see globals as implicit fields of an implicit (god) object, that is implicitly passed into every function. That would be quite some implication ![]()
Yes I see it that way too, and modules via import paths are already the necessary evil that lives in almost every file
Are you saying, you prefer C’s extern or MATLAB’s default global namespace? I like namespaces and I like explicit.
FWIW I bite the bullet in ZML we have lot of function that requires io, allocator and also “platform” which is an accelerator specific object but typically you only have one per executable.
I personally store things like io, gpa or similar context in structs belonging to longer-running things. For example, an HTTP client is something that is reused, so I store such dependencies there. It’s possibly slightly wasteful, but leads to a better API, in my opinion. For utilities, I always pass it as a parameter. I don’t like context objects passed throughout the application, it makes it harder to see what dependencies are needed where. That said, I could see it as acceptable using global io and gpa in a self-contained app.
I am biased but I think the real answer is dependency-injection.
Tokamak is based around its dependency-injection container and while it’s not perfect, and it’s probably never going to be feature-rich when compared to its counterparts in dynamic languages, it is IMO quite usable already.
For example, I can structure my code to different modules, and my application is superset of all of them. Plumbing will be auto-discovered and the application will start up sort of magically but still predictably. I can even define “overrides” and startup hooks in extra module and include that module conditionally (comptime only). That way I can easily use curl client instead of the std.http one, etc.
There is special treatment for webapps, as you can define req-scoped deps and have them available for injection only in the given subtree. So you can have one extra layer of safety if you work with user-credentials or something (if it’s not below /xxx/*, you know it’s not available)
BTW: Tokamak DI can be used alone, even if you don’t want to use web/cli/tui
Word of caution: it is one-man project, and it’s likely that you’re going to have different expectations which I will not be able to satisfy - it’s fine, feel free to use it as basis for your own DI container, others did already and I think this kind of collaboration may even work the best in the new era of LLMs.
EDIT: Here’s one link to a real, non-web application.
https://codeberg.org/cztomsik/clown-code/src/branch/main/src/main.zig