- Joined
- Apr 27, 2024
Oh, geez, I feel like I'm speaking way out of my depth making any recommendations in here. Take this with a grain of salt, I'm still very much a rookie compared to others in here and I could just be saying totally obvious stuff.I don't intend to become a game dev but I think a lot of the patterns could be reused elsewhere. Got any recommendations?
Well, so far, the thing that I'm slowly working on is the problem of initializations that are contingent on other initializations. At its core, the first thing I figured out was the idea that, if two components don't have a firm initialization order (and indeed, if you want the flexibility to not worry about that), they need to have a logic branch (I've just been calling it doOrQueue because I like coming up with funny names for my patterns) that will A: check the thing they're looking for, and if it's initialized, grab the relevant reference, or B: if it's not, register to an event dispatcher that broadcasts at the end of initialization that tells anyone listening that "hey, I'm ready, so if you were waiting on me to initialize before grabbing some data, now's the time".
The problem with this is that if we don't know our initialization order (or we want to have one that's in any way asynchronous), we can't check whether an object is ready, since we neither have a way to find that object, and if even if we did, the thing doesn't exist yet, so we couldn't check any of its members - even if we can account for the thing not existing, it's got no event dispatcher for us to register to while we're here in code, and the code path has to terminate, so at this point we can't "mark our place until it's ready" in any way.
The way I figure we work around this is to have a class whose job is essentially being the registry/manager for these other classes we're trying to get ahold of, that contains reference/event dispatcher pairs for each component it's responsible for. It doesn't matter if class A doesn't exist, because since we have access to a registry we know it will eventually belong to and an alias, on that registry that we know it will occupy, we can look at that value, see if it's occupied, and if it isn't, "take a number and sit down" by registering to the event dispatcher that the manager has (rather than keeping it on the member itself). When the member finishes initializing and registers to the manager, the manager broadcasts the dispatcher that's the pair of that particular member.
The other part of the trick is that the manager/registry class is composed of entirely static members, and may itself be a singleton. Since it's static, there's no race condition/initialization traffic jam - they exist right at the start of runtime, require no instantiation, and you can register to them immediately. It breaks the worry about race conditions by acting as a sturdy rock that you can guarantee will always be available, and so you can depend on it to be the top of your chain of references. As long as you have that one rock, anything that needs anything else can wait, even if they need manager C which registers to manager B which registers to manager A, (but this might be unnecessary if managers B and C are their own static classes with their own domains). The only real constraint that enforces is that you can't have instances of that registry, e.g. only one set of values, but that's a feature, not a bug, since you should only ever need one instance of a given type of registry, otherwise you're doing something wrong.
There's sort of a microcosm of this that exists within objects as well - if you're building objects generally compositionally rather than using (multiple) inheritance or with some combination of the two, it makes sense for an object to have a component that acts as a similar sort of manager for its sibling components, but you can similarly find that sort of "rock" in the base object - so long as it contains the same kind of reference/delegate to what will eventually be the registry component, you're good to go, and you can also do certain things on this kind of instance-based registry that you can't on a static one, since as an instance, you can have the thing instantiate more member dispatchers on demand, which I think might open up the ability for the registry itself to be contained in a TMap, with each member registering with a specific key, and the thing generates and assigns a dispatcher to it at that time. You might need to templatize or find a way to make a wrapper that boils the typing out of it though, because it's likely that a TMap registry consisting of many different objects, unless they can polymorphically adhere to a common base class (which could be more trouble than it's worth), will cause those kinds of typing conflicts otherwise.
Something I also want to add to that is a system that replaces components actively looking up their dependencies with passively receiving dependency injection where possible - basically, if we know what dependencies an object needs, we can set up a similar system that's responsible for both keeping track of which dependencies each object in its domain needs, as well as queueing objects to initialize when all the dependencies in their "package" are known to be ready. That is, if we have a registry that contains empty references that can be registered to by objects A, B and C, anytime one of them registers, the system can ask "are there any other objects that depend on this reference? Send them a signal that we have it now", and then after that, on those systems, also ask "Do we now have all the dependencies we need for this? If so, send the cue for it to initialize."
Basically, it consolidates a lot of the initialization responsibilities into a single manager, and it also makes initialization as well as finding dependencies passive for the class receiving the dependency injection, and active on the parts of the injecting class, which sets up a more easily-followable logical chain, and since that all goes through a central manager class, the logic becomes easier to follow and also enforces a specific usage that makes it easier to intuitively do the right thing than the wrong thing. I think these sorts of complex systems live and die on how consistent they are, and so things like that that force you to adhere to certain infallible rules do a lot in keeping the thing from feeling hacked together.
That's really important to me, at least, because I'm someone who gets demotivated if I feel like my program is gradually getting worse and more hacky, but the inverse is true.
The problem with this is that if we don't know our initialization order (or we want to have one that's in any way asynchronous), we can't check whether an object is ready, since we neither have a way to find that object, and if even if we did, the thing doesn't exist yet, so we couldn't check any of its members - even if we can account for the thing not existing, it's got no event dispatcher for us to register to while we're here in code, and the code path has to terminate, so at this point we can't "mark our place until it's ready" in any way.
The way I figure we work around this is to have a class whose job is essentially being the registry/manager for these other classes we're trying to get ahold of, that contains reference/event dispatcher pairs for each component it's responsible for. It doesn't matter if class A doesn't exist, because since we have access to a registry we know it will eventually belong to and an alias, on that registry that we know it will occupy, we can look at that value, see if it's occupied, and if it isn't, "take a number and sit down" by registering to the event dispatcher that the manager has (rather than keeping it on the member itself). When the member finishes initializing and registers to the manager, the manager broadcasts the dispatcher that's the pair of that particular member.
The other part of the trick is that the manager/registry class is composed of entirely static members, and may itself be a singleton. Since it's static, there's no race condition/initialization traffic jam - they exist right at the start of runtime, require no instantiation, and you can register to them immediately. It breaks the worry about race conditions by acting as a sturdy rock that you can guarantee will always be available, and so you can depend on it to be the top of your chain of references. As long as you have that one rock, anything that needs anything else can wait, even if they need manager C which registers to manager B which registers to manager A, (but this might be unnecessary if managers B and C are their own static classes with their own domains). The only real constraint that enforces is that you can't have instances of that registry, e.g. only one set of values, but that's a feature, not a bug, since you should only ever need one instance of a given type of registry, otherwise you're doing something wrong.
There's sort of a microcosm of this that exists within objects as well - if you're building objects generally compositionally rather than using (multiple) inheritance or with some combination of the two, it makes sense for an object to have a component that acts as a similar sort of manager for its sibling components, but you can similarly find that sort of "rock" in the base object - so long as it contains the same kind of reference/delegate to what will eventually be the registry component, you're good to go, and you can also do certain things on this kind of instance-based registry that you can't on a static one, since as an instance, you can have the thing instantiate more member dispatchers on demand, which I think might open up the ability for the registry itself to be contained in a TMap, with each member registering with a specific key, and the thing generates and assigns a dispatcher to it at that time. You might need to templatize or find a way to make a wrapper that boils the typing out of it though, because it's likely that a TMap registry consisting of many different objects, unless they can polymorphically adhere to a common base class (which could be more trouble than it's worth), will cause those kinds of typing conflicts otherwise.
Something I also want to add to that is a system that replaces components actively looking up their dependencies with passively receiving dependency injection where possible - basically, if we know what dependencies an object needs, we can set up a similar system that's responsible for both keeping track of which dependencies each object in its domain needs, as well as queueing objects to initialize when all the dependencies in their "package" are known to be ready. That is, if we have a registry that contains empty references that can be registered to by objects A, B and C, anytime one of them registers, the system can ask "are there any other objects that depend on this reference? Send them a signal that we have it now", and then after that, on those systems, also ask "Do we now have all the dependencies we need for this? If so, send the cue for it to initialize."
Basically, it consolidates a lot of the initialization responsibilities into a single manager, and it also makes initialization as well as finding dependencies passive for the class receiving the dependency injection, and active on the parts of the injecting class, which sets up a more easily-followable logical chain, and since that all goes through a central manager class, the logic becomes easier to follow and also enforces a specific usage that makes it easier to intuitively do the right thing than the wrong thing. I think these sorts of complex systems live and die on how consistent they are, and so things like that that force you to adhere to certain infallible rules do a lot in keeping the thing from feeling hacked together.
That's really important to me, at least, because I'm someone who gets demotivated if I feel like my program is gradually getting worse and more hacky, but the inverse is true.
I hope there's something useful in there, but I'm not too weathered at this stuff and I don't know how simple these concepts are to better trained eyes (I don't talk to other programmers very often, for some reason I'm oddly self-conscious about it). I admit I haven't finished it and I don't know how well it's going to work in practice, since the last handful of weeks have just been figuring the darn thing out in my head. I don't have enough whiteboards to keep track of all this stuff.
To answer your question more directly, most of what I've been doing has just been learning and reciting the Gang of Four patterns so far, since I feel like understanding the patterns at an abstract enough level to spot them (and problems of their "shape") in the wild has been helping me to develop a stronger more consistent intuition for what to do and why. Not to say I've been contriving places to use them, it just happens that I keep running into situations where they intuitively seem like the right answer (IE I come up with a solution in my head and it just happens to align with some Go4 idea or another). From what I've seen, it's pretty foundational stuff that's generic and very applicable in most of the OOP space.
Last edited: