Skip to content

A swift package enabling decentralised dependency resolution

License

Notifications You must be signed in to change notification settings

nicholascross/Resolve

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Resolve

A swift package to support dependency resolution with property wrapper support for ease of use.

NOTE: Whilst convenient for prototyping using this with non throw away code could be a maintainence issue if used without restrictions; since this obscures property ownership and at worst could be encouraging the action at a distance anti-pattern.

Usage Example

This is how you would do something similar to Swinject basic usage

let resolver = DependencyResolver()

resolver.register { Cat(name: "Mimi") as Animal }
resolver.register { PetOwner() as Person }
let petOwner: Person = resolver.resolve()
petOwner.play()
// print: I'm playing with Mimi.

Where the types are defined as follows.

protocol Animal {
    var name: String? { get }
}

class Cat: Animal {
    let name: String?

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

protocol Person {
    func play()
}

class PetOwner: Person {
    @Resolve var pet: Animal

    func play() {
        let name = pet.name ?? "someone"
        print("I'm playing with \(name).")
    }
}

What can Resolve do?

@Resolve property wrapper

Resolving registered dependencies is simple just add the @Resolve property wrapper in front of your property.

@Resolve var pet: Animal

This will find the first DependencyResolver that registered this type to resolve the value.

If using a single DependencyResolver per type variant is not appropriate for your use case you can include this as part of your property declaration.

@Resolve(resolver: someContext) var pet: Animal

Type registration

Before the above will work there must be a defined way to resolve the object that will be returned. This is done by registering a closure that returns the type to be resolved.

Note the casting to Animal, this is allows registration of a new Cat instance any time we resolve the Animal type.

let resolver = DependencyResolver()
resolver.register { Cat(name: "Mimi") as Animal }

There can be only a single registration for a given type variant this allows the default registrations to be ignored which might be useful for testing purposes. Earlier registration of mock/stub objects will take precedence allowing you to provide alternate implementation for testing purposes.

If an alternate registration is truly required the old registration can be removed and a new one registered.

resolver.removeResolver(for: Person.self)

Variant resolution

There can be multiple variants registered for a single type.

resolver.register(variant: "long_date") { () -> DateFormatter in
    let formatter = DateFormatter()
    formatter.dateFormat = "MMM yyyy"
    return formatter
}

resolver.register(variant: "short_date") { () -> DateFormatter in
    let formatter = DateFormatter()
    formatter.dateFormat = "dd/MM/yyyy"
    return formatter
}
// This will resolve expected date formatter
@Resolve(variant:"long_date") var formatter: DateFormatter

Object lifetime management

Resolve does not require explicit management of the lifetime of any resolved objects. The above date example would need to be modified in order to prevent a new date formatter being created everytime it was resolved.

let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "MMM yyyy"
    return formatter
}()

resolver.register(variant: "long_date") { dateFormatter }

Objects lifetime can be specified explictly using the convenience functions persistent, transient, ephemeral.

// persistent life time will always resolve the same object
resolver.persistent { Example() }

// transient life time will resolve the same object provided there is a strong reference to it elsewhere
resolver.transient { Example2() }

// ephemeral life time will always resolve a new object
resolver.ephemeral { Example3() }

Optional property storage

Assigning to the formatter property would currently be a no-op but backing storage can be updated by implementing a storer closure.

var dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "MMM yyyy"
    return formatter
}()

resolver.register(variant: "long_date", resolver: { dateFormatter }, storer: { f in dateFormatter = f })

The above registration will allow the following to property to be used as a setter as well.

// This will resolve expected date formatter
@Resolve(variant:"long_date") var formatter: DateFormatter

The property can be set directly or via calling the store function on the DependencyResolver.

self.formatter = someOtherFormatter
// OR
resolver.store(object: someOtherFormatter, variant: "long_date")

Type variants registered with the persistent or transient functions may have thier stored values replaced.

let petOwner = resolver.register { PetOwner() }
let mimi = resolver.transient(variant: "Mimi") { Cat(name: "Mimi") as Animal }

petOwner.play()
// print: I'm playing with Mimi.

petOwner.pet = Cat(name: "Franky")
petOwner.play()
// print: I'm playing with Franky.

Hierarchical registration

When registering a type conforming to DependencyRegister protocol the registerDependencies function will be called giving you an opportunity to register any further dependencies.

This allows the distribution of dependency registration through out the application in hierarchical manner, as one register may register another whilst in turn registering its own dependencies.

final class ExampleRegister: DependencyRegister {
    func registerDependencies(resolver: Resolver) {
        resolver.register(variant: "Mimi") { Cat(name: "Mimi") as Animal }
        resolver.register { PetOwner() as Person }
    }
}

let resolver = DependencyResolver()
resolver.register { ExampleObject() }

let petOwner: Person = resolver.resolve()
petOwner.play()
// print: I'm playing with Mimi.