Hacker News new | ask | show | jobs
by valand 1957 days ago
Another option is injecting an optional dependency without connecting the box.

I see OP doesn't mention this option, but is slightly related to both option 1 (connect the box) and 3 (message bus / event emitter). This option is similar to how OS provides an API to user space application program. For example Windows provides an API to an application to flash its window without exposing all of its state like mentioned in option 1. https://docs.microsoft.com/en-us/windows/win32/api/winuser/n...

Here's the detail:

A self-powered object, let's call it workingIndicatorObject, can be introduced to work on the working indicator. It provides 1.) `getState() -> WORKING/NOT-WORKING` a function to get its state, and 2.) `register() -> None`, a function to register user activities. These functions are dependencies for UserSection component and InventoryTable component respectively. In term of lifetime and hierarchy, it precedes and outlives both components.

The workingIndicatorObject MUST have its own lifecycle to regulate its internal state. Its core lifecycle MUST NOT be managed under the same event loop as the GUI components, assuming the program is written on top of a framework that has it (React, Vue, etc). This is to assure that it doesn't directly affect the components managed by the framework (loose coupling). Although, a binding MAY be made between its state and the framework-managed event loop, for example, in React environment, wrapping it as a React hook. An EventEmitter can also be provided for a component to listen to its internal event loop.

Injecting it as a dependency can use features like React Context or Vue's Provide/Inject pattern. Its consumer must treat it as an optional dependency for it to be safe. For example, the UserSection component can omit drawing the "working" indicator if it doesn't receive the `getState` function as a dependency.

Internally, the workingIndicatorObject can use a set. The set is used to store a unique value (a Symbol in Javascript). `register` function creates a Symbol, stores it in the set, and deletes it after $TIMEOUT, while firing event both at addition and deletion. `getState` function is a function that returns `set.size()`. When bound to the component, an addition to the set fires an "update" command to the component, which redraw the component along its internal state. This is just one example of implementation and there are other simpler ways to have the same behaving workingIndicatorObject.

This approach allows UserSection and InventoryTable to only know `getState()` and `register()` function and nothing else, and having both of them optional. Using static type system such as TypeScript can help a lot in this since we can pin down the signatures of both function to `null | () => WORKING | NOT_WORKING` and `null | () => void`, where we can enforce both dependent components to check if it exists and to call it with the correct types, otherwise the compiler yells.

1 comments

Your example is pretty frontend-specific, but I wanted to highlight what I consider its core point, and which is something I only figured out a year ago (incidentally, when I built my own DI system for a game):

As per dependency injection, you inject stuff the other party needs. As per "dependency inversion principle", the thing you inject stuff into owns the interface for what it needs. That means a concrete object you want to inject in many places should be made to conform to interfaces defined by the "recipients" of the injection, which may - and likely will be - different from each other. And in particular, an interface may be as simple as a function.

So, for instance, if a component needs a log sink to write to, and it only ever writes a single type of messages, you do not need to ship the whole logger object (and with it, the knowledge of what a logger is). All it needs is a function `(String) => void`, so all you need to inject is your logger's Info() method.

Thank you for extracting the core point. That is on point!

Another core point is actually the architecture of things, which lives/dies first/last, who depends on who, and how to connect them that each party is "happy" with what's on their plate.

> incidentally, when I built my own DI system for a game

Indeed systems in game are fun to play with! I've had my share of designing a game engine once and I learnt a lot from it too. I'm intrigued, what game were you working on?

> Indeed systems in game are fun to play with! I've had my share of designing a game engine once and I learnt a lot from it too. I'm intrigued, what game were you working on?

It's an unpublished roguelike that I'm working on, slowly, on hobby time. ASCII graphics, focused on distance combat, and with a hybrid ECS/event-based architecture, where the entire game state is stored in an in-memory SQLite database. More of a research project into the benefits of such architectural decisions than an actual game.

The DI system kind of grew out of my design goal to make the game logic testable in headless mode, without rendering or input (I have only so much time to write and maintain it, and I sometimes code over SSH). I started architecting things with dependency inversion in mind, and then figured I could have the whole game's object graph instantiated in a single place. I decided to make it declarative and composable (pretty much concatenative) by writing code that automatically instantiates the objects in correct order, and then I realized I've reinvented a DI container.

That is cool! I've been wanting to make a roguelike myself, but haven't got the time due to amount of work.

I wonder though what's the upside of storing the game objects in SQLITE?