Summary
Deps is a minimalist Raku module for exploring dependency injection patterns through a clear, trait-based API. It offers both low‑level and high‑level interfaces, supports named registrations, trait‑driven function injection, and automatic instantiation of classes, while providing precise control over resolution lifecycles—New, Store, and Scope—and resolution precedence via priority.
1. Motivation and Background
Dependency injection decouples construction of dependencies from their use, improving modularity, testability, and configuration flexibility. Deps serves as an educational playground for understanding inversion of control in Raku and experimenting with DI concepts without the complexity of heavy frameworks.
2. Overview of Deps
Deps exposes:
- Low‑level API: Manually register and retrieve dependencies by type or name.
-
Block‑scoped DSL: Use
deps { ... }
to create nested containers and perform inline registrations or imports. -
Trait‑based injection: Apply
is injected
to functions/subroutines to have parameters auto‑provided. - Automatic instantiation: Construct classes by matching attributes to registered values.
3. Core Features
3.1 Low‑Level API
my Deps $deps .= new;
$deps.register: Bla;
$deps.register:
-> Int $a, Int :$b --> Str { "$a - $b" },
:name<value>
;
$deps.register: 13;
$deps.register: 42, :name<a>;
# Resolve
$deps.get(Bla); # Bla.new(value => "42 - 13", a => 42, b => 13)
$deps.get(Int, :name<a>); # 42
$deps.get(Int); # 13
3.2 Block‑Scoped DSL
deps {
injectable C.new: :13attr;
injected my C $obj;
# $obj == C.new(attr => 13)
}
3.3 Function‑Level Injection
sub handle-request(UInt $id) is injected {
injectable UserIdStorage.new: :$id;
process-request
}
Parameters typed as registered types are injected at call time.
3.4 Automatic Instantiation
class Example { has Int $.x; has Str $.y }
deps {
injectable 7;
injectable "hello", :name<y>;
instantiate(Example); # Example.new(x => 7, y => "hello")
}
4. Resolution Lifecycles
Deps supports three built‑in lifecycles:
-
New (transient): the default; each call to
.get
creates a fresh instance. -
Store (singleton): one instance per container; reused on every
.get
. -
Scope (scoped): one instance per
deps { ... }
block; fresh when entering a new block but reused within it.
Specify with :lifecycle<New|Store|Scope>
:
# Transient (default)
$deps.register: -> Int $n --> Int { $n * 2 };
# Singleton
$deps.register: MyService.new, :lifecycle<Store>;
# Scoped
$deps.register: RequestToken.new, :lifecycle<Scope>;
5. Priority
When multiple providers match a type (and optional name), :priority<Strict|Defer>
determines which wins.
$deps.register:
FileLogger.new,
:lifecycle<Store>,
:priority<defer>
;
$deps.register:
ConsoleLogger.new,
:lifecycle<Store>,
:priority<strict>
;
# .get(Logger) returns ConsoleLogger
6. Combined Example
role Repo {}
class RealRepo does Repo { has Str $.dsn }
class MockRepo does Repo { }
my Deps $deps .= new;
# Real (singleton)
$deps.register:
RealRepo.new(
|(dsn => $_ with %*ENV<DB>)
),
:lifecycle<store>,
:priority<defer>,
:name<repo>
;
# Mock (transient)
$deps.register:
MockRepo,
:lifecycle<new>,
:priority<strict>,
:name<repo>
;
deps-scope :parent($deps), {
injected my Repo $repo; # MockRepo wins by priority
}
7. Conclusion and Future Directions
With its lightweight core and idiomatic Raku traits, Deps is ideal for learning DI patterns. Correct lifecycle names—New, Store, and Scope—and built‑in priority give precise control over object lifetimes and resolution. Future enhancements may include circular dependency detection, named scopes, and asynchronous resolution workflows.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more