12

I encountered the following situation: I have two modules, TokenService and Wifi which are initialized from main(). The modules themselves don't know of the existence of each other and function completely independent, yet they need access to the same global resource which in this case is the nonvolatile storage flash, or short nvs, which must be initialized for the module to function correctly. The flash initialization function however is not idempotent, this means I can't call it twice, yet it should be called at least once when the first of either module goes through initialization. Here's a visualization of the thing:

Two modules accessing same global resource

I have not yet found a suitable elegant way to accomplish this. My current approach is to defer the initialization of the global resource back to the user of the module via a lambda, but it's not satisfying. I imagine that this is not the first time anyone has encountered this problem so I'm looking for the idiomatic approach to solve this.

glades
  • 315
  • 2
  • 6
  • Referring it back to the caller via a lambda is, at the very least, one of the idiomatic ways of doing this. – jmoreno Apr 29 '23 at 01:28

4 Answers4

43

The answer to the problem "a third-party module has unsuitable semantics..." is always "...so I code an adapter that warps those semantics into something suitable." In other words, write e.g. a façade which does nothing except remember the state of initialization, and forwards any actual calls to the actual module.

Kilian Foth
  • 107,706
  • 45
  • 295
  • 310
36

There are several ways to solve this problem.

One is to use an adapter or facade around the nvs, as described in the answer by @KilianFoth.

Another option is to use dependency injection. Then the contract of both TokenService and Wifi can state that they expect to receive a ready-to-use object that will handle their storage needs. That object could be the single instance of nvs, which has been initialized by main, or it could even be a storage mock that is provided by the test environment.

Bart van Ingen Schenau
  • 71,712
  • 20
  • 110
  • 179
  • 5
    I would prefer the second one in this case. Of course, if you still need the lazy init, you can pass a lazy-init adapter or facade to both TokenService and Wifi. – user253751 Apr 26 '23 at 15:33
  • Dependency injection is my preferred solution. Not least of which it makes testing easier but even better it makes the entire system more configurable for future needs. – slebetman Apr 26 '23 at 22:59
  • 3
    Isn't dependency injection orthogonal to the facade question? You could inject an instance of the original library, and that wouldn't solve the problem at all, iiuc. Or you use a concrete facade class without injection, which solves the issue, just with worse testability. – Peter - Reinstate Monica Apr 27 '23 at 08:15
  • 3
    @Peter-ReinstateMonica a "pure" dependency injection solution would be to initialize the adapter in main (or in a container) before instantiating either module. It would lose the benefits of lazy init obviously. – Rad80 Apr 27 '23 at 10:56
  • 8
    @Peter-ReinstateMonica The fundamental issue is that `Wifi` and `TokenService` each don't know about the existence of the other, so it's hard to make them collaborate on who should initialize `nvs` exactly once. But if they both take an already-initialized `nvs` instance as a parameter, then it's their caller's problem. Their caller is `main`, which *does* know about both `Wifi` and `TokenService`, and if we're doing dependency injection it also knows about `nvs`. So `main` can coordinate initializing `nvs` exactly once and then passing it to both modules. – Ben Apr 28 '23 at 01:30
  • @Ben Sure. All I'm sayin' is that *injection* and *initialization* are logically independent. You could in principle inject an uninitialized nvs (I know, that would be stupid, sure). You could also *inject nothing* and simply initialize nvs in main before the two other modules are created or started. The two modules then would simply use nvs directly, as they do now, without any injection. nvs would be ready to use and the modules didn't need to care about initialization at all. *That main (or a wrapper) initializes nvs before first use is orthogonal to injection.* – Peter - Reinstate Monica Apr 28 '23 at 12:04
  • This is how I would do it... Dependency injection reduces coupling, increases testability, maintainability, and portability... Even better would be to create a memory storage interface so that it would be possible to change to a different storage method in the future without touching any code except the memory storage interface. – Questor Apr 28 '23 at 18:16
  • @Peter-ReinstateMonica: The main advantage of using DI here, is that it makes the dependency that `Wifi` and `TokenService` have _explicit_. If you move initialization to `main` (or any other user, really) then there's a risk of forgetting to initialize, which may result in bugs that may not be that easy to diagnose at a glance. By making the dependency _explicit_, however, it's now a compile-time error not to pass the dependency, and if passing it requires initializing it, you're golden. – Matthieu M. Apr 29 '23 at 11:01
  • @MatthieuM. Sure. I'm all for it. That was not the point. – Peter - Reinstate Monica Apr 29 '23 at 11:32
0

Why are you so concerned about Wifi and TokenService not being aware of each other while they're allowed to know about implementation details of the storage module?

Only let each module know what they actually need about the storage and leave everything else about the manufacturer module inside a module of your own whose only job is bridging the gap between what your modules need and what the manufacturer module offers. As other answers say, there are pleny of patterns to do that.

Rad80
  • 251
  • 1
  • 7
-1

This looks like use case for a singleton.
Forward all the calls to flash through a singleton - the first time it is accessed, it will handle the initialization logic, and stick around until program exits.

Thomas
  • 1
  • Not, kinda. nvs already is a singleton by OPs very definition. Their question is how to get two modules to actually use this singleton as one – Hobbamok Apr 28 '23 at 08:57
  • @Hobbamok through a Singleton instance that acts as gateway and handles the initialization. – Thomas Apr 28 '23 at 15:00
  • 1
    Singleton's are (generally speaking) an anti-pattern. They create spaghetti code with hidden dependencies. It is better to use dependency injection. – Questor Apr 28 '23 at 18:19