Skip to content

Instantly share code, notes, and snippets.

@CassiusPacheco
Last active March 17, 2020 10:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CassiusPacheco/951edf2fe4c68c08cce539867d3340fe to your computer and use it in GitHub Desktop.
Save CassiusPacheco/951edf2fe4c68c08cce539867d3340fe to your computer and use it in GitHub Desktop.
Dependency Container
//
import Foundation
extension NSLocking {
@discardableResult
public func sync<T>(action: () -> T) -> T {
lock()
defer { self.unlock() }
return action()
}
}
final public class DependencyContainer {
private var factories = [String: Any]()
private var singletonFactories = [String: Any]()
private let lock: NSLocking
public init(lock: NSLocking = NSRecursiveLock()) {
self.lock = lock
}
/// Returns whether the specified type has been registered. It returns `true` if there's a singleton instance
/// already created, or for a singleton factory or regular factory registered.
/// - Note: This method is thread safe.
public func contains<T>(_ type: T.Type) -> Bool {
return lock.sync {
let key = String(describing: type)
return singletonFactories[key] != nil || factories[key] != nil
}
}
}
public extension DependencyContainer {
// MARK: - Singleton registration
/// Registers a singleton factory closure. The instance will only be created once resolved.
/// Arguments are not supported for singleton registration.
/// - Note: This method is not thread safe and should only be used at the application start up.
func registerSingleton<T>(_ type: T.Type, factory: @escaping ((DependencyContainer) -> T)) {
singletonFactories[String(describing: type)] = factory
}
}
public extension DependencyContainer {
// MARK: - Factory methods with no arguments
/// Registers a factory closure. A new instance will be created every time it is resolved.
/// - Note: This method is not thread safe and should only be used at the application start up.
func register<T>(_ type: T.Type, factory: @escaping ((DependencyContainer) -> T)) {
factories[String(describing: type)] = factory
}
/// For singleton registration, it returns a previously created singleton instance if already created, otherwise
/// creates a new instance and caches it. For regular factory, it returns a new instance value.
/// - Note: This method is thread safe.
func resolve<T>(_ type: T.Type) -> T {
return lock.sync {
let key = String(describing: type)
if let singletonFactory = self.singletonFactories[key] as? ((DependencyContainer) -> T) {
let instance = singletonFactory(self)
self.singletonFactories[key] = { (di: DependencyContainer) -> T in return instance }
return instance
} else if let instanceFactory = self.factories[key] as? ((DependencyContainer) -> T) {
return instanceFactory(self)
} else {
fatalError("Instance of type `\(type)` hasn't been registered")
}
}
}
}
public extension DependencyContainer {
// MARK: - Factory methods with one argument
/// Registers a factory closure with one argument. A new instance will be created every time it is resolved.
/// - Note: This method is not thread safe and should only be used at the application start up.
func register<T, A>(_ type: T.Type, factory: @escaping ((DependencyContainer, A) -> T)) {
factories[String(describing: type)] = factory
}
/// Returns a newly created instance injecting one argument.
/// - Note: This method is thread safe.
func resolve<T, A>(_ type: T.Type, argument: A) -> T {
return lock.sync {
if let instanceFactory = self.factories[String(describing: type)] as? ((DependencyContainer, A) -> T) {
return instanceFactory(self, argument)
} else {
fatalError("Instance of type `\(type)` hasn't been registered")
}
}
}
}
public extension DependencyContainer {
// MARK: - Factory methods with two arguments
/// Registers a factory closure with two arguments. A new instance will be created every time it is resolved.
/// - Note: This method is not thread safe and should only be used at the application start up.
func register<T, A, B>(_ type: T.Type, factory: @escaping ((DependencyContainer, A, B) -> T)) {
factories[String(describing: type)] = factory
}
/// Returns a newly created instance injecting two arguments.
/// - Note: This method is thread safe.
func resolve<T, A, B>(_ type: T.Type, arguments arg1: A, _ arg2: B) -> T {
return lock.sync {
if let instanceFactory = self.factories[String(describing: type)] as? ((DependencyContainer, A, B) -> T) {
return instanceFactory(self, arg1, arg2)
} else {
fatalError("Instance of type `\(type)` hasn't been registered")
}
}
}
}
public extension DependencyContainer {
// MARK: - Factory methods with three arguments
/// Registers a factory closure with three arguments. A new instance will be created every time it is resolved.
/// - Note: This method is not thread safe and should only be used at the application start up.
func register<T, A, B, C>(_ type: T.Type, factory: @escaping ((DependencyContainer, A, B, C) -> T)) {
factories[String(describing: type)] = factory
}
/// Returns a newly created instance injecting three arguments.
/// - Note: This method is thread safe.
func resolve<T, A, B, C>(_ type: T.Type, arguments arg1: A, _ arg2: B, _ arg3: C) -> T {
return lock.sync {
if let instanceFactory = self.factories[String(describing: type)] as? ((DependencyContainer, A, B, C) -> T) {
return instanceFactory(self, arg1, arg2, arg3)
} else {
fatalError("Instance of type `\(type)` hasn't been registered")
}
}
}
}
public extension DependencyContainer {
// MARK: - Factory methods with four arguments
/// Registers a factory closure with four arguments. A new instance will be created every time it is resolved.
/// - Note: This method is not thread safe and should only be used at the application start up.
func register<T, A, B, C, D>(_ type: T.Type, factory: @escaping ((DependencyContainer, A, B, C, D) -> T)) {
factories[String(describing: type)] = factory
}
/// Returns a newly created instance injecting four arguments.
/// - Note: This method is thread safe.
func resolve<T, A, B, C, D>(_ type: T.Type, arguments arg1: A, _ arg2: B, _ arg3: C, _ arg4: D) -> T {
return lock.sync {
if let instanceFactory = self.factories[String(describing: type)] as? ((DependencyContainer, A, B, C, D) -> T) {
return instanceFactory(self, arg1, arg2, arg3, arg4)
} else {
fatalError("Instance of type `\(type)` hasn't been registered")
}
}
}
}
@CassiusPacheco
Copy link
Author

CassiusPacheco commented Feb 16, 2020

Unit Tests:

import Foundation
import XCTest

final class DependencyInjectorTests: XCTestCase {
    private class DummyTest: Equatable {
        let name: String
        let id: String = UUID().uuidString

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

        static func == (lhs: DependencyInjectorTests.DummyTest, rhs: DependencyInjectorTests.DummyTest) -> Bool {
            guard lhs.id == rhs.id else { return false }
            guard lhs.name == rhs.name else { return false }
            return true
        }
    }

    private class MultipleArgsDummyTest: Equatable {
        let name: String
        let city: String
        let state: String
        let country: String

        init(name: String, city: String, state: String, country: String) {
            self.name = name
            self.city = city
            self.state = state
            self.country = country
        }

        static func == (lhs: DependencyInjectorTests.MultipleArgsDummyTest, rhs: DependencyInjectorTests.MultipleArgsDummyTest) -> Bool {
            guard lhs.name == rhs.name else { return false }
            guard lhs.city == rhs.city else { return false }
            guard lhs.state == rhs.state else { return false }
            guard lhs.country == rhs.country else { return false }
            return true
        }
    }

    func testContainerResolvesRegisteredClasses() {
        let container = DependencyInjector()

        container.register(DummyTest.self) { (di) -> DummyTest in
            return DummyTest(name: "Test")
        }

        let resolvedTest1 = container.resolve(DummyTest.self)
        let resolvedTest2 = container.resolve(DummyTest.self)

        XCTAssertNotEqual(resolvedTest1, resolvedTest2, "Factory creates a new instance for every instance resolved")
    }

    func testContainerResolvesSingletonRegisteredClasses() {
        let container = DependencyInjector()

        container.registerSingleton(DummyTest.self) { (di) -> DummyTest in
            return DummyTest(name: "Test")
        }

        let resolvedTest1 = container.resolve(DummyTest.self)
        let resolvedTest2 = container.resolve(DummyTest.self)

        XCTAssertEqual(resolvedTest1, resolvedTest2, "Singleton factories returns the same instance for every instance resolved")
        XCTAssertTrue(resolvedTest1 === resolvedTest2, "Resolve singleton should return the same instance every time")
    }
}

extension DependencyInjectorTests {
    // MARK: - Test Contains

    func testContainerContainsRegisteredSingletonClass() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(DummyTest.self))

        container.registerSingleton(DummyTest.self) { di -> DummyTest in
            return DummyTest(name: "Test")
        }

        XCTAssertTrue(container.contains(DummyTest.self), "container should contain the registered singleton")
    }

    func testContainerContainsInstantiatedSingletonClass() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(DummyTest.self))

        container.registerSingleton(DummyTest.self) { di -> DummyTest in
            return DummyTest(name: "Test")
        }

        let singleton = container.resolve(DummyTest.self)

        XCTAssertTrue(singleton === container.resolve(DummyTest.self), "Singletons should have the same memory address")
        XCTAssertTrue(container.contains(DummyTest.self), "container should contain the registered singleton")
    }

    func testContainerContainsRegisteredFactoryClass() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(DummyTest.self))

        container.register(DummyTest.self) { di -> DummyTest in
            return DummyTest(name: "Test")
        }

        // The singleton instance hasn't been created yet because it has never been resolved.
        XCTAssertTrue(container.contains(DummyTest.self), "container should contain the registered factory")
    }
}

extension DependencyInjectorTests {
    // MARK: - Test One Argument

    func testContainerContainsRegisteredFactoryClassWithSingleArgument() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(DummyTest.self))

        container.register(DummyTest.self) { di, name -> DummyTest in
            return DummyTest(name: name)
        }

        XCTAssertTrue(container.contains(DummyTest.self), "container should contain the registered factory")
    }

    func testContainerResolveFactoryClassWithSingleArgument() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(DummyTest.self))

        container.register(DummyTest.self) { di, name -> DummyTest in
            return DummyTest(name: name)
        }

        let resolved = container.resolve(DummyTest.self, argument: "Some name")
        XCTAssertEqual(resolved.name, "Some name")
        XCTAssertFalse(resolved === container.resolve(DummyTest.self, argument: "Some name"), "Resolve factory should create new instances every time")
    }
}

extension DependencyInjectorTests {
    // MARK: - Test Two Argument

    func testContainerContainsRegisteredFactoryClassWithTwoArguments() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(MultipleArgsDummyTest.self))

        container.register(MultipleArgsDummyTest.self) { di, name, city -> MultipleArgsDummyTest in
            return MultipleArgsDummyTest(name: name, city: city, state: "NSW", country: "Australia")
        }

        XCTAssertTrue(container.contains(MultipleArgsDummyTest.self), "container should contain the registered factory")
    }

    func testContainerResolveFactoryClassWithTwoArguments() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(DummyTest.self))

        container.register(MultipleArgsDummyTest.self) { di, name, city -> MultipleArgsDummyTest in
            return MultipleArgsDummyTest(name: name, city: city, state: "NSW", country: "Australia")
        }

        let resolved = container.resolve(MultipleArgsDummyTest.self, arguments: "Some name", "Sydney")
        XCTAssertEqual(resolved.name, "Some name")
        XCTAssertEqual(resolved.city, "Sydney")
        XCTAssertEqual(resolved.state, "NSW")
        XCTAssertEqual(resolved.country, "Australia")
        XCTAssertFalse(resolved === container.resolve(MultipleArgsDummyTest.self, arguments: "Some name", "Sydney"), "Resolve factory should create new instances every time")
    }
}

extension DependencyInjectorTests {
    // MARK: - Test Three Argument

    func testContainerContainsRegisteredFactoryClassWithThreeArguments() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(MultipleArgsDummyTest.self))

        container.register(MultipleArgsDummyTest.self) { di, name, city, state -> MultipleArgsDummyTest in
            return MultipleArgsDummyTest(name: name, city: city, state: state, country: "Australia")
        }

        XCTAssertTrue(container.contains(MultipleArgsDummyTest.self), "container should contain the registered factory")
    }

    func testContainerResolveFactoryClassWithThreeArguments() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(DummyTest.self))

        container.register(MultipleArgsDummyTest.self) { di, name, city, state -> MultipleArgsDummyTest in
            return MultipleArgsDummyTest(name: name, city: city, state: state, country: "Australia")
        }

        let resolved = container.resolve(MultipleArgsDummyTest.self, arguments: "Some name", "Sydney", "NSW")
        XCTAssertEqual(resolved.name, "Some name")
        XCTAssertEqual(resolved.city, "Sydney")
        XCTAssertEqual(resolved.state, "NSW")
        XCTAssertEqual(resolved.country, "Australia")
        XCTAssertFalse(resolved === container.resolve(MultipleArgsDummyTest.self, arguments: "Some name", "Sydney", "NSW"), "Resolve factory should create new instances every time")
    }
}

extension DependencyInjectorTests {
    // MARK: - Test Four Argument

    func testContainerContainsRegisteredFactoryClassWithFourArguments() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(MultipleArgsDummyTest.self))

        container.register(MultipleArgsDummyTest.self) { di, name, city, state, country -> MultipleArgsDummyTest in
            return MultipleArgsDummyTest(name: name, city: city, state: state, country: country)
        }

        XCTAssertTrue(container.contains(MultipleArgsDummyTest.self), "container should contain the registered factory")
    }

    func testContainerResolveFactoryClassWithFourArguments() {
        let container = DependencyInjector()

        XCTAssertFalse(container.contains(DummyTest.self))

        container.register(MultipleArgsDummyTest.self) { di, name, city, state, country -> MultipleArgsDummyTest in
            return MultipleArgsDummyTest(name: name, city: city, state: state, country: country)
        }

        let resolved = container.resolve(MultipleArgsDummyTest.self, arguments: "Some name", "Sydney", "NSW", "Australia")
        XCTAssertEqual(resolved.name, "Some name")
        XCTAssertEqual(resolved.city, "Sydney")
        XCTAssertEqual(resolved.state, "NSW")
        XCTAssertEqual(resolved.country, "Australia")
        XCTAssertFalse(resolved === container.resolve(MultipleArgsDummyTest.self, arguments: "Some name", "Sydney", "NSW", "Australia"), "Resolve factory should create new instances every time")
    }
}

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