Juice is a Swift dependency injection container.
With a swift package manager:
dependencies: [
.package(url: "https://github.com/andrey-shavelev/Juice", from: "0.1.0")
//...
]
Using Juice is simple. First, you create a Container and register all required components. Second, resolve the services you need, while Juice injects all the dependencies automatically. Please find some code samples bellow, or skip to «More Details» section, if you prefer.
let container = try Container { builder in
builder.register(injectable: FreshJuice.self)
.instancePerDependency()
.as(Juice.self)
builder.register(injectable: Orange.self)
.instancePerDependency()
.as(Fruit.self)
.asSelf()
}
class FreshJuice: InjectableWithParameter {
let fruit: Fruit
required init(_ fruit: Fruit) {
self.fruit = fruit
}
}
protocol Fruit {
}
struct Orange: Fruit, Injectable {
}
let orangeJuice = try container.resolve(Juice.self)
let compot = try container.resolveOptional(Compot.self)
let tea = try container.resolve(Tea?.self)
let appleJuice = try container.resolve(Juice.self, withArguments: Argument<Fruit>(Apple.self))
class IcyLemonade: InjectableWithFiveParameters {
let fruitJuice: Juice
let lemon: Lemon
let optionalSweetener: Sweetener?
let water: Water
let ice: Ice
required init(_ fruitJuice: Juice,
_ lemon: Lemon,
_ optionalSweetener: Sweetener?,
_ water: Water,
_ ice: Ice) {
self.fruitJuice = fruitJuice
self.lemon = lemon
optionalSweetener = optionalSweetener
self.water = water
self.ice = ice
}
}
let container = try Container { builder in
builder.register(injectable: IcyLemonade.self)
.singleInstance()
.asSelf()
///...
}
let container = try Container { builder in
builder.register(injectable: Pear.self)
.singleInstance()
.as(Fruit.self)
builder.register(injectable: Ginger.self)
.singleInstance()
.as(Spice.self)
builder.register(injectable: Jam.self)
.singleInstance()
.asSelf()
.injectDependency(into: \.fruit)
.injectDependency(into: \.spice)
}
class Jam: Injectable {
var fruit: Fruit!
var spice: Spice?
required init() {
}
}
let container = try Container { builder in
builder.register(injectable: Pear.self)
.singleInstance()
.as(Fruit.self)
builder.register(injectable: Ginger.self)
.singleInstance()
.as(Spice.self)
builder.register(injectable: Jam.self)
.singleInstance()
.asSelf()
}
class Jam: Injectable {
@Inject var fruit: Fruit
@Inject var spice: Spice?
required init() {
}
}
class Egg: InjectableWithParameter {
unowned var chicken: Chicken
required init(_ chicken: Chicken) {
self.chicken = chicken
}
}
class Chicken: InjectableWithParameter {
var egg: Lazy<Egg>
required init(_ egg: Lazy<Egg>) {
self.egg = egg
}
}
class RobotFactory: Injectable {
@Inject var armFactory: FactoryWith2Parameters<Side, Equipment, Arm>
@Inject var legFactory: FactoryWith2Parameters<Side, Equipment, Leg>
required init() throws {
}
func makeRobot(withName name: String) throws -> Robot {
return Robot(name: name,
leftArm: try armFactory.create(.left, .machineGun),
rightArm: try armFactory.create(.right, .lazer),
leftLeg: try legFactory.create(.left, .jumpJet),
rightLeg: try legFactory.create(.right, .jumpJet))
}
let container = try Container { builder in
builder.register(module: FruitModule())
}
struct FruitModule : Module {
func registerServices(into builder: ContainerBuilder) {
builder.register(injectable: Apple.self)
.instancePerDependency()
.asSelf()
}
}
let container = try Container { builer in
// Some component registered here
}
let childContainerWithoutAdditionalComponents = container.createChildContainer()
let childContainerWithAdditionalComponents = try container.createChildContainer { builer in
// Some additional components may be registered here
}
let namedChildContainer = container.createChildContainer(withName: "JuiceMaker")
class SomeService : Injectable {
@Inject var currentScope: CurrentScope
required init() {
}
func doAThing() {
let unitOfWorkContainer = try! currentScope.createChildContainer()
let doerOfThings = try! unitOfWorkContainer.resolve(DoerOfThings.self)
doerOfThings.doAThing()
}
}
let containerWithOranges = try Container {
$0.register(injectable: Orange.self)
.instancePerDependency()
.as(Fruit.self)
$0.register(injectable: FreshJuice.self)
.instancePerDependency()
.asSelf()
}
let childContainerWithApples = try container.createChildContainer {
$0.register(injectable: Apple.self)
.instancePerDependency()
.as(Fruit.self)
}
let appleJuice = try childContainerWithApples.resolve(FreshJuice.self)
Component registration builder has a fluent interface that varies slightly depending on a kind of component that you are registering.
let container = try Container { builder in
builder.register(injectable: Ramen.self)
.instancePerDependency()
.as(Soup.self)
.injectDependency(into: \.soySouce)
.injectOptionalDependency(into: \.miso)
// ... other registrations
}
- Here, we define the type of the component that is going to be registered:
builder.register(injectable: Ramen.self)
- Next, we specify the lifetime for the Ramen component:
.instancePerDependency()
This make Ramen an instance per dependency component, which means that the container will create a new instance of it each time when it needs to satisfy a dependency. There are three more options available:
- Single instance,
- Instance per container,
- Instance per named container.
- Next, we list all services provided by our component:
.as(Soup.self)
Here we tell the container that Ramen could be resolved as Soup or as Noodles.
- Finally, we have an option to instruct container to inject dependencies into Ramen properties of our choice:
.injectDependency(into: \.soySouce)
.injectOptionalDependency(into: \.miso)
After such set up, container will inject a required dependency into the soySouce property and an optional dependency into the miso property.
You can register a class or structure by simply specifying it’s type if it conforms to the Injectable protocol. The Injectable protocol has only one member: required init() method without parameters, which tells the container how to create an instance of conforming type when it needs to. If you want to use Initializer injection, you need to confirm your type to any of the InjectabeWithParameter protocols instead:
class Cocktail: InjectableWithFourParameters {
let fruitJuice: Juice
let lime: Lime
let sweetener: Sweetener
let water: Water
required init(_ fruitJuice: Juice,
_ lime: Lime,
_ sweetener: Sweetener,
_ water: Water) {
self.fruitJuice = fruitJuice
self.lime = lime
self.sweetener = sweetener
self.water = water
}
}
You can register a factory function or a closure, that will be responsible for creation of a component instance at runtime. This approach could also be used when conformance to the Injectable protocol is not possible. For example:
let container = try Container { builder in
builder.register(factory: {
Cocktail(fruitJuice: try $0.resolve(Juice.self),
lime: Lime(),
sweetener: Sugar(),
water: SodaWater())})
.singleInstance()
.asSelf()
}
class Cocktail {
let fruitJuice: Juice
let lime: Lime
let sweetener: Sweetener
let water: Water
required init(fruitJuice: Juice,
lime: Lime,
sweetener: Sweetener,
water: Water) {
self.fruitJuice = fruitJuice
self.lime = lime
self.sweetener = sweetener
self.water = water
}
}
A factory closure receives a single parameter: Scope that could be used to resolve required dependencies.
In order to register an existing instance of class you use register(instance:)
method:
let someExternalSingletonService = SingletonService.instance
let container = try Container { builder in
builder.register(instance: someExternalSingletonService)
.ownedExternally()
.asSelf()
The ownedExternally()
method tells the container to keep an unowned reference to the registered singleton. You may instead call ownedByContainer()
method, to instruct container to take the ownership and keep a strong reference to it.
You can register an instance of struct by using register(value:)
method:
let devConfiguration = DatabaseConfiguration(host: "localhost", port: 3306, user: "username", password: "s3cr3t")
let container = try Container { builder in
builder.register(value: devConfiguration)
.asSelf()
For each injectable component, as well as for all components created by factories, you has to explicitly specify how their instances will be scoped. You do it by calling one of four methods of component registration builder:
instancePerDependency()
singleInstance()
instancePerContainer()
instancePerContainer(withName:)
The container owns all single instance, instance per container and matching instance per named container components that were created during its lifetime and keeps a strong reference to them. It is supposed that they are deallocated together with the owning container.
]
All services that component provides has to be declared explicitly by calling either as()
or asSelf()
method of component registration builder:
let container = try Container { builder in
builder.register(injectable: Pear.self)
.singleInstance()
.asSelf()
.as(Fruit.self)
// Pear was registered with two services
}
You has to specify at least one service for each component registered in the container. A registration without services is considered incomplete and invalid. One component may be registered by several services. In contrast, you can not register two or more components by the same service in one container. This is not supported at the moment.
When the container is built and ready, you can start resolving services from it. For example:
let container = try Container { builder in
// ...
}
let appModule = try! container.resolve(AppModule.self)
appModule.bootstrap()
appModule.listen(atPort: 3000)
You can pass additional arguments, including specific dependencies, when resolving a component. For example:
let appleJuice = try container.resolve(Juice.self, withArguments: Argument<Fruit>(Apple.self))
All arguments are added to the CurrentScope of resolved component and are used for the Initializer injection and for the property injection.
For single instance, instance per container and instance per named container components only first call to container.resolve(:withArguments:)
actually has an effect. Subsequent calls will return existing instance, and all arguments will be ignored.
When a component confirms to one of InjectableWithParameters protocols, Juice resolves all parameters of theinit(...)
method and uses them to create an instance. When a component has too many dependencies, it can inject CurrentScope protocol and resolve everything needed from it:
class TeaBlend: InjectableWithParameter {
let tea: Tea
let fruit: Fruit
let berry: Berry
let flower: Flower
let herb: Herb
let spice: Spice
required init(_ scope: CurrentScope) throws {
self.tea = try scope.resolve(Tea.self)
self.fruit = try scope.resolve(Fruit.self)
self.berry = try scope.resolve(Berry.self)
self.flower = try scope.resolve(Flower.self)
self.herb = try scope.resolve(Herb.self)
self.spice = try scope.resolve(Spice.self)
}
}
A service is considered optional if it is a normal situation when no components providing this service are registered in the container. There are several way to resolve an optional service.
When using @Injectable property wrapper, you simple need to declare the property optional:
struct SushiRoll: Injectable {
// Required stuff
@Inject var tuna: Tuna
@Inject var cucumber: Cucumber
@Inject var mayo: Mayo
// Really optional
@Inject var omelette: Omelette?
}
When using CurrentScope or Container, you call resolveOptional()
method:
class SushiRoll: InjectableWithParameter {
required init(_ currentScope: CurrentScope) throws {
self.omelette = try currentScope.resolveOptional(Omlet.self)
// ...
}
// ...
}
You can also resolve optional service by passing optional type to resolve
method:
class SushiRoll: InjectableWithParameter {
required init(_ currentScope: CurrentScope) throws {
self.omelette = try currentScope.resolve(Omelette?.self)
// ...
}
// ...
}
Or specifying optional parameter in init method of Injectable component:
class SushiRoll: InjectableWith4Parameters {
required init(_ tuna: Tuna,
_ cucumber: Cucumber,
_ majo: Majo,
_ optionalOmelette: Omelette?) {
// ...
}
}
Either way, Juice will resolve a service if it is registered or will put/return nil if it is not.
Lazy<T> allows to postpone resolution of service until the moment when it is needed. For example:
class TripPlanningService: Injectable {
@Inject var hotelBookingService: Lazy<HotelBookingService>
required init() {
}
func planATrip(forDays days: Int) throws -> Trip {
if (days > 1) {
try hotelBookingService.getValue().makeBooking()
}
// more planing ...
}
}
Auto factories provider a convenient way to create multiple child components within a parent component.
class RobotFactory: Injectable {
@Inject var armFactory: FactoryWith2Parameters<Side, Equipment, Arm>
@Inject var legFactory: FactoryWith2Parameters<Side, Equipment, Leg>
required init() throws {
}
func makeRobot(withName name: String) throws -> Robot {
return Robot(name: name,
leftArm: try armFactory.create(.left, .machineGun),
rightArm: try armFactory.create(.right, .lazer),
leftLeg: try legFactory.create(.left, .jumpJet),
rightLeg: try legFactory.create(.right, .jumpJet))
}
}
There are several generic Factory types declared, depending on how many arguments you need to pass. There is no need to manually register Factory types in container. They are registered and created dynamically when needed.
Using Factory is the same as using resolve(_:withArguments:) method of CurrentScope, with only difference that parameters’ types are specify in factory class generic arguments, not when resolve method is called.
let arm = try armFactory.create(.left, .machineGun)
let sameArm = try currentScope.resolve(Arm.self, withArguments: Argument<Side>(.left), Argument<Equipment>(.machineGun))
Here arm and sameArm are equivalent.
Please note that Factory keeps a strong reference to CurrentScope of the component that it is used within and, thus, references all parameters (if any) that may present in it.
Modules helps to organize registration of components into structured and reusable units. In order to create a module, you need to conform to the Module protocol and define registerServices(into builder: ContainerBuilder). For example:
struct FruitModule : Module {
func registerServices(into builder: ContainerBuilder) {
builder.register(injectable: Apple.self)
.instancePerDependency()
.asSelf()
}
}
A child container keeps a reference to its parent and inherits all component registrations. When creating a child container you can use a container builder to register additional components or override inherited registrations. Parent container does not keep any reference to child container, and your code is fully responsible for managing its lifetime.
Thread safety is not implemented yet. All access to the container from multiple threads must be synchronized by calling code.
This project is licensed under MIT License.