Now that we have a more robust MVVM codebase that uses a Service, and we’ve ensured its testability, it’s time to address the elephant in the room.
This is the second part of a two-part blogpost. Follow this link if you were looking for the first part.
If you’re a visual learner I also provide some presentation slides here.
If you just want to fiddle around with the code, you can find it here.
And now, on with the show 🎩
# What are the pitfalls of our DI implementations? 🚯
The keen eyed developers might have been screaming at the screen this whole time. “You’re using the singleton pattern!”. The singleton pattern should be avoided yes, but it’s a cardinal sin if used properly (which I’d argue is the case). The real danger is that it nudges into using too many static
properties too easily, particularly for our Services.
But why is that a problem? What’s the lifecycle of a static
property? Well, for Swift
Stored type properties (such as
static
ones) are lazily initialized on their first access. They’re guaranteed to be initialized only once, even when accessed by multiple threads simultaneously, and they don’t need to be marked with the lazy modifier.
Ok sure, but why does this matter?
Well, they never get cleaned up after being accessed the first time, at least not until you terminate the app! So if we have many or huge services being allocated as static properties… 💣⏲️💥 We might just run out of memory from keeping too much stuff around. Is there a fix for this?
# Service Locators
What we want is some way to:
- Hold all of the services that should be deallocated at some point (stored in a collection, e.g. Dictionary key-value)
- Have some way to manage (add and remove) and search for services in that collection
We call this a Service Locator, and its pseudo-code implementation ServiceLocator.swift
should look something like this:
- 🗂️
var services = [ServiceName : Any]()
- 🆔
register(Service)
- 🔍
get(Service)
- 🚮
unregister(Service)
On top of that we have to consider a couple of scenarios, what should happen if:
- we try to re-register a service? (duplicate entries)
- we try to get an unregistered service? (nil checking)
- we fetch and try to assign the wrong type of service? (type safety)
Before you proceed any further, how many lines of code do you think it will take to implement this Service Locator?
# Simple implementation
A fleshed-out implementation can look something akin to this:
final class ServiceLocator {
enum Error: Swift.Error {
case duplicateService(ServiceName)
case inexistentService(ServiceName)
case typeMismatch(ServiceName)
}
typealias ServiceName = String
static let shared = ServiceLocator()
private var services = [ServiceName : Any]()
// MARK: - Public Methods
@discardableResult
func register<Service>(
service: Service,
name serviceName: ServiceName? = nil
) throws -> ServiceName {
let name = buildName(for: Service.self, serviceName)
guard services[name] == nil
else { throw Error.duplicateService(name) }
services[name] = service
return name
}
func get<Service>(
name serviceName: ServiceName? = nil
) throws -> Service {
let name = buildName(for: Service.self, serviceName)
guard let registeredService = services[name]
else { throw Error.inexistentService(name) }
guard let service = registeredService as? Service
else { throw Error.typeMismatch(name) }
return service
}
func unregister<Service>(
_ type: Service.Type,
name serviceName: ServiceName? = nil
) throws {
let name = buildName(for: type, serviceName)
guard let _ = services[name]
else { throw Error.inexistentService(name) }
services[name] = nil
}
// MARK: - Private Methods
private func buildName<Service>(
for _: Service.Type,
_ serviceName: ServiceName? = nil
) -> ServiceName {
return serviceName ?? "\(Service.self)"
}
}
# Sample Usage
And with this powerful pattern we can improve our Service once again 💪.
protocol ServiceProtocol {
func toggle(bool: Bool) -> Bool
}
class ServiceImplementation: ServiceProtocol {
static let singleton = ServiceImplementation() // 👈
func toggle(bool: Bool) -> Bool {
!bool
}
}
Let’s get rid of that singleton and replace it with a proper registration through our shiny Service Locator.
protocol ServiceProtocol {
func toggle(bool: Bool) -> Bool
}
class LocatableServiceImplementation: ServiceProtocol {
// 👀 Notice how we no longer need a singleton ref. here!
func toggle(bool: Bool) -> Bool {
!bool
}
}
// MARK: Usage example:
let serviceLocator = ServiceLocator.shared
let service: LocatableServiceImplementation
// get or register the service
do {
service = try serviceLocator.get(LocatableServiceImplementation.self)
} catch ServiceLocator.Error.inexistentService {
service = .init()
try? serviceLocator.register(service: instance)
} catch {
// handle other unexpected errors
}
// use the service as we would normally
let viewModel: ViewModel = .init(
model: .init(boolProperty: true),
service: service
)
// and when we no longer need the service, we just get rid of it 🚮
serviceLocator.unregister(LocatableServiceImplementation.self)
# Putting it all together 🏁
All in all, here’s what we’ve accomplished for our codebase in one snippet of code.
// MARK: Service
protocol ServiceProtocol {
func toggle(bool: Bool) -> Bool
}
class LocatableServiceImplementation: ServiceProtocol {
func toggle(bool: Bool) -> Bool {
!bool
}
}
// MARK: ViewModel
protocol ViewModelRepresentable {
var model: Model { get }
var service: ServiceProtocol { get }
func getUpdatedValue() -> Bool
}
struct ViewModel: ViewModelRepresentable {
private(set) var model: Model
private let service: ServiceProtocol
init(
model: Model,
service: ServiceProtocol
) {
self.model = model
self.service = service
}
func getUpdatedValue() -> Bool {
service.toggle(bool: model.boolProperty)
}
}
// MARK: View
class View: UIView {
private(set) var viewModel: ViewModelRepresentable
required init(viewModel: ViewModelRepresentable) {
self.viewModel = viewModel
setupViewVisibility()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViewVisibility() {
isHidden = viewModel.getUpdatedValue()
}
}
# Unit Tests
My hope is that by now you’re thinking to yourself “What about testing the Service Locator and its usage?”, and I’ve got you covered! Take a peek at the tests I used to develop the implementation I showcased above here. And a massive shoutout to the guys at Mindera for open-sourcing Alicerce’s ServiceLocator and the tests for it as well 💛
# Going the extra mile 🏃♂️
Before you leave, if earlier you found that whole Service Locator sample usage a bit bloated 🤬, rest assured you are not alone. Luckily, with a bit of ingenuity 👷♂️🪄 we can even begin expanding our simple Service Locator with a cool getOrRegister
method that does the searching and error handling for us, delegating that awkward try-catch
dance that we’ve seen before into the implementation details of the ServiceLocator.
// Conforming to this abstraction is the linchpin
protocol Serviceable {
init() // 👈
}
final class ServiceLocator {
// ...
@discardableResult
func register<Service>( /* ... */ ) throws -> ServiceName {
// ...
}
func get<Service>( /* ... */ ) throws -> Service {
// ...
}
// ...
func getOrRegister<Service: Serviceable>( // 👈
_ type: Service.Type
) -> Service {
// perform the same try-catch dance as before, but with abstractions!
do {
return try get()
} catch Error.inexistentService {
let instance = Service.init() // 👈
do {
try register(service: instance)
} catch {
// 🔨 not thread safe... yet! 🔨
}
return instance
} catch {
// 🔨 not exhaustive... yet? 🔨
}
}
// ...
}
And now, we can just do the following 💆♂️
let viewModel = ViewModel(
model: .init(boolProperty: true),
service: serviceLocator.getOrRegister(ServiceImplementation.self)
)
Thanks for sticking around, and be sure to check out part 1 if you haven’t already!
Hopefully this two-part blogpost helped you improve the testability of your codebase. 🤞 Until next time 👋
# Sources
Definitions:
Stored properties:
Service Locators:
- https://github.com/Mindera/Alicerce/blob/408a3015dc578f2598c14645b942f04c9042d7ce/Sources/Utils/ServiceLocator.swift
- https://github.com/Mindera/Alicerce/blob/408a3015dc578f2598c14645b942f04c9042d7ce/Tests/AlicerceTests/Utils/ServiceLocatorTests.swift
- https://quickbirdstudios.com/blog/swift-dependency-injection-service-locators/
- https://stevenpcurtis.medium.com/the-service-locator-pattern-in-swift-5db2c770bcc
- https://www.oracle.com/java/technologies/service-locator.html
- https://www.baeldung.com/java-service-locator-pattern