Skip to content

Instantly share code, notes, and snippets.

@Revolucent
Last active July 9, 2019 22:49
Show Gist options
  • Save Revolucent/25daa75bda879dd20bb2 to your computer and use it in GitHub Desktop.
Save Revolucent/25daa75bda879dd20bb2 to your computer and use it in GitHub Desktop.
DependencyResolver - a simple, elegant, flexible dependency resolver for Swift 2.x
//
// DependencyResolver.swift
//
// Created by Gregory Higley on 2/15/16.
// Copyright © 2016 Revolucent LLC.
//
// This file is released under the MIT license.
// https://gist.github.com/Revolucent/25daa75bda879dd20bb2
//
import Foundation
/**
A simple, elegant, flexible dependency resolver.
Registration uses a `type` and an optional `name`. The combination
of `type` and `name` must be unique. Registering the same `type`
and `name` **overwrites** the previous registration.
*/
public struct DependencyResolver {
private struct Key: Hashable {
let type: String
let name: String?
init(type: String, name: String? = nil) {
self.type = type
self.name = name
// djb2 hash algorithm: http://www.cse.yorku.ca/~oz/hash.html
// &+ operator handles Int overflow
var hash = 5381
hash = ((hash << 5) &+ hash) &+ type.hashValue
if let name = name {
hash = ((hash << 5) &+ hash) &+ name.hashValue
}
hashValue = hash
}
let hashValue: Int
}
private class Dependency {
private let create: Any -> Any
private let shared: Bool
private var instance: Any?
init<P, D>(shared: Bool, create: P -> D) {
self.shared = shared
self.create = { param in create(param as! P) }
}
func resolve<P, D>(parameters: P) -> D {
var result: D
if shared {
if instance == nil {
instance = create(parameters)
}
result = instance! as! D
} else {
result = create(parameters) as! D
}
return result
}
}
private static var dependencies = [Key: Dependency]()
private init() {}
/**
Registers `create` with the DependencyResolver.
- parameter type: Usually the type of `D`, but can be any string.
- parameter name: An optional name to disambiguate similar `type`s.
- parameter shared: Whether or not the instance is lazily created and then shared.
- parameter create: The lambda to register with the DependencyResolver.
*/
public static func register<P, D>(type type: String = String(D), name: String? = nil, shared: Bool = false, create: P -> D) {
let key = Key(type: type, name: name)
dependencies[key] = Dependency(shared: shared, create: create)
}
/**
Registers an existing instance with the DependencyResolver.
- note: This effectively creates a singleton. If you want your singleton created lazily,
register it with a lambda and set `shared` to true.
- parameter instance: The instance to register.
- parameter type: Usually the type of `D`, but can be any string.
- parameter name: An optional name to disambiguate similar `type`s.
*/
public static func register<D>(instance: D, type: String = String(D), name: String? = nil) {
register(type: type, name: name, shared: true) { instance }
}
/**
Resolves an instance of `D` in the DependencyResolver.
- parameter parameters: The parameters to pass to the registered lambda.
- parameter type: Usually the type of `D`, can be any string.
- parameter name: An optional name to disambiguate similar `type`s.
- returns: The result of the registered lambda, or nil if not registered.
*/
public static func resolve<P, D>(parameters: P, type: String = String(D), name: String? = nil) -> D? {
let key = Key(type: type, name: name)
guard let dependency = dependencies[key] else { return nil }
return (dependency.resolve(parameters) as D)
}
/**
Resolves an instance of `D` in the DependencyResolver.
- parameter parameters: The parameters to pass to the registered lambda.
- parameter type: Usually the type of `D`, can be any string.
- parameter name: An optional name to disambiguate similar `type`s.
- returns: The result of the registered lambda, or nil if not registered.
*/
public static func resolve<D>(type type: String = String(D), name: String? = nil) -> D? {
return resolve((), type: type, name: name)
}
}
private func ==(lhs: DependencyResolver.Key, rhs: DependencyResolver.Key) -> Bool {
if lhs.hashValue != rhs.hashValue { return false }
if lhs.type != rhs.type { return false }
if lhs.name != rhs.name { return false }
return true
}
@Revolucent
Copy link
Author

The basic way this works is that a lambda which describes how to create a particular type in registered with the resolver. The type (along with an optional string) is used to create a key with which we can later resolve that type. The type is ultimately resolved by calling the lambda and passing our parameters (if any) to it.

Examples:

protocol Bovine {}

class Watusi: Bovine {
  let name: String

  init(name: String) { self.name = name }
}

// Inject a Bovine into the resolver. Type inference tells the resolver which type we're talking about.
DependencyResolver.register { (name: String) in Watusi(name: name) as Bovine }

// Resolve a Bovine. Again, type inference is our friend. The "foo" is the parameter passed to our creation lambda above.
let bovine = DependencyResolver.resolve("foo")! as Bovine

What if we want more than one Bovine implementation?

class Ankole: Bovine {}

DependencyResolver.register(name: "watusi") { (name: String) in Watusi(name: name) as Bovine }
DependencyResolver.register(name: "ankole") { Ankole() as Bovine }
let watusi = DependencyResolver.resolve("foo", name: "watusi")! as Bovine
let ankole = DependencyResolver.resolve(name: "ankole")! as Bovine

What about shared instances? There are various ways, but the simplest is just this:

let watusi = Watusi(name: "shared")
DependencyResolver.register { watusi as Bovine }
let bovine = DependencyResolver.resolve()! as Bovine

Using names, you can have multiple different shared instances of the same type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment