Design#
Static Typing#
An important constraint of the design of this framework was that both
config authors and container users should be able to statically reason
about the objects and values they’re using. That is, it’s important
that all dilib
functionality is compatible with mypy
and pyright
type checking, with no magic or plugins required.
Prevent Pollution of Objects#
The dependency between DI configs and actual objects in the object graph should be one way: the DI config depends on the object graph types and values. This keeps the objects clean of particular decisions made by the DI framework. It also means you can switch DI frameworks with no changes to the objects themselves.
(dilib
offers optional mixins that violate this decision
for users that want to favor easier syntax.
See Easier syntax.)
Child Configs are Singletons by Type#
In dilib
, when you set a child config on a config object,
you’re not actually instantiating the child config
(despite what it looks like).
Rather, you’re creating a spec that will be instantiated
when the root config’s .get()
is called.
This means that the config instances are singletons by type
(unlike the actual objects specified in the config, which are by alias).
It would be cleaner to create instances of common configs and
pass them through to other configs
(that’s what DI is all about, after all!). However, the decision was made
to not allow this because this would make
building up configs almost as complicated as building up the
actual object graph users are interested in
(essentially, the user would be engaged in an abstract meta-DI problem).
As such, all references to the same config type are
automatically resolved to the same instance,
at the expense of some flexibility and directness.
The upside, however, is that it’s much easier to create nested configs,
which means users can get to designing the actual object graph quicker.
Scale and Config Composability#
It’s important that configs are naturally composable: all configs can automatically be both root configs for certain applications and child configs for others.
For example, an application that only cares about engines would do:
def main() -> None:
config = dilib.get_config(EngineConfig, ...)
container = dilib.get_container(config)
engine = container.config.engine
# Now do something with the engine
But that same exact EngineConfig
can also be nested in a CarConfig
:
class CarConfig(dilib.Config):
engine_config = EngineConfig()
This means that you can scale to a large number of configs and objects without building monolithic config objects. And the line between configs can represent different subdomains or even teams of the project.