-
Hello folks, I recently discovered this neat library for DI injection on Python. I've gave it a try but I could not find "the way" to handle injecting primitive values (mostly coming from application configuration). For example import lagom
class Database:
pass
class MyService:
def __init__(self, foo: str, db: Database) -> None:
self.foo = foo
self.db = db
container = lagom.Container()
container[MyService] This example obviously fails
It cannot resolve the Because Lagom does not allow to register services by name, I can not do something like container["foo"] = "some value" Creating new types on the flyWhat I can do, is taking advantages of the typing system Foo = NewType("Foo", str)
class MyService:
def __init__(self, foo: Foo, db: Database) -> None:
self.foo = foo
self.db = db
container[Foo] = "some value" This actually works really well but I think it starts to be tedious when you have more of such primitive values. Also, it hides actual types just for the purpose of injection. I'm not sure if this is the way recommend. Manually registeringThe other way is tosimply manually register container[MyService] = lambda c: MyService("some value", c[Database]) This works as well but we're loosing the auto wiring power. If going this way, I would be able to omit specifying the Using a settings dedicated object to injectLast way I was thinking of is using a dedicated settings object that will be injected class Settings:
foo: str = "some value"
class MyService:
def __init__(self, settings: Settings, db: Database) -> None:
self.foo = settings.foo
self.db = db This way, we don't even need to register anything. The issue is that I feel this is actually a code smell. I don't like the fact that my service is relying on the whole settings object while I just need a single property from it. It also can make refactoring tedious if our settings object changes. What are you thoughts on this? How do you handle such cases? Thank you! |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments
-
@g0di this is a great question. It's really something that I should add to the documentation as it's a fairly important topic. I'll first address the approach that lagom deliberately doesn't take (and won't in the future). Which is the one you mention like this: container["foo"] = "some value" one of design principles of lagom is Next to answer your question for my apps using lagom I actually use some combination of the three approaches you mention. So I would say all three approaches are "recommended". Which one I use depends on the exact usecase and life stage of the project. Creating a type (Creating new types on the fly)I use this quite a lot as I think it communicates the purpose quite well. Me (and my IDE) now know exactly what the purpose of this string is DatabaseConnectionString = NewType("DatabaseConnectionString", str)
class SomeDatabase:
def __init__(self, dsn: DatabaseConnectionString) -> None:
self.do_something(dsn)
container[DatabaseConnectionString] = "some value"
This is definitely a legitimate concern. When I find myself doing this a lot this is when I might move on to the next approach: Settings Object (Using a settings dedicated object to inject)If I have a number of settings all related it doesn't make sense to me to create a type for each one so I'll group them into an object. @dataclasses.dataclass
class MyServiceSettings:
retry_attempts: int
timeout: int
failure_message: str
class MyService:
def __init__(self, settings: MyServiceSettings, db: Database) -> None:
self.foo = settings.foo
self.db = db
# then later I can have
container[MyServiceSettings] = MyServiceSettings(retry_attempts=5, timeout=400, failure_message="oops")
I address this by having multiple settings objects specific to what they are setting. This doesn't have to be 1 to 1 for classes but I try and keep them all focussed. Manually registeringI also use this approach as I like one of the statements in the zen of python |
Beta Was this translation helpful? Give feedback.
-
Thank you for the time you took to answer me. This is really valuable, in particular the approach consisting on splitting settings on smaller objects per service. I definitively understand the reason behind not using magic strings and I would add that this is anyway problematic because you loose the type at resolve time. I'm not sure there is any better ways that the ones you actually outlined to handle this case and I'm happy you could yourself confirm your vision regarding those. |
Beta Was this translation helpful? Give feedback.
-
Thanks 👍 Glad to hear this is helped. I should also mention if you find any patterns that work really well and could be added to the library I'm happy to consider suggestions and/or PRs. Dealing with configuration is such an important topic. |
Beta Was this translation helpful? Give feedback.
@g0di this is a great question. It's really something that I should add to the documentation as it's a fairly important topic.
I'll first address the approach that lagom deliberately doesn't take (and won't in the future). Which is the one you mention like this:
one of design principles of lagom is
Everything should be done by type. No reliance on names/magic strings.
(from https://lagom-di.readthedocs.io/en/latest/CONTRIBUTING/#design-goals).Next to answer your question for my apps using lagom I actually use some combination of the three approaches you mention. So I would say all three approaches are "recommended". Which one I use depends on the exact use…