Dependency Injection in Pure Swift
There is Cake Design Pattern in Scala to do Dependency Injection (DI), and a minimal version called Minimal Cake Design Pattern.
We don’t really need a framework to do DI in Swift. With pure Swift code only, I will introduce you how to use Minimal Cake Design Pattern to do DI at production and test code.
With every class, I will create an Interface using protocol
, which name started withUses...
, and an Implementation using class
which name started with MixIn...
.
Interface only declares what the protocol
can do, and Implementation declares how.
protocol UserRepository {
func findById(id: Int) -> User?
}
// Interface
protocol UsesUserRepository {
var userRepository: UserRepository { get }
}
// Implementation
class MixInUserRepository: UserRepository {
func findById(id: Int) -> User? {
return User(id: id, name: "orakaro", role: "member")
}
}
When instance of UserRepository
is used by another class, for example UserService
, I will use above Interface to declare a new pair protocol
/extension
, and of course another Interface and Implementation for UserService
itself.
Sounds more complicated than it is. Let’s look at the code.
protocol UserService: UsesUserRepository {
func promote(asigneeId: Int) -> User?
}
extension UserService {
func promote(asigneeId: Int) -> User? {
guard var asignee = userRepository.findById(id: asigneeId) else {return nil}
asignee.role = "leader"
return asignee
}
}
// Interface
protocol UsesUserService {
var userService: UserService { get }
}
// Implementation
class MixInUserService: UserService {
let userRepository: UserRepository = MixInUserRepository()
}
What if UserService
is used by another TeamService
again? Well the same logic is applied, UsesUserService
is used to declare a pair of protocol
/extension
, and there will be Interface/Implementation for the new service also.
protocol TeamService: UsesUserService {
func buildTeam(leader: User) -> [User?]
}
extension TeamService {
func buildTeam(leader: User) -> [User?] {
return [userService.promote(asigneeId: leader.id)]
}
}
// Interface
protocol UsesTeamServvice {
var teamService: TeamService { get }
}
// Implementation
class MixInTeamService: TeamService {
let userService: UserService = MixInUserService()
}
All this kind of wiring is statically typed. If we have a dependency declaration missing or something is misspelled then we get a compilation error. Furthermore implementation is immutable (declared as let
).
For example our application using TeamService
can be like this
let applicationLive = MixInTeamService()
let team = applicationLive.buildTeam(leader: User(id: 1, name: "orakaro", role: "member"))
print(team) // [Optional(User(id: 1, name: "orakaro", role: Optional("leader")))]
At this time we already can inject a mock repository into UserService
to test the promote
method even without mocking framework.
We are not merely creating mocks but the mocks we create are wired in as the declared dependencies wherever defined.
class MockUserRepository: UserRepository {
func findById(id: Int) -> User? { return nil }
}
class UserServiceForTest: UserService {
let userRepository: UserRepository = MockUserRepository()
}
let testService = UserServiceForTest()
print(testService.promote(asigneeId: 1)) // nil
The same way can be used to inject TeamService
to test buildTeam
method.
class TeamServiceForTest: TeamService {
let userService: UserService = UserServiceForTest()
}
let applicationTest = TeamServiceForTest()
let testTeam = applicationTest.buildTeam(leader: User(id: 1, name: "orakaro", role: "member"))
print(testTeam) // [nil]
The big picture is now:
For more detail see playground in this repo.
I found this solution to be a simple, clear way to structure Swift code and create the object graph. It uses only pure Swift protocol
/extension
, does not depend on any frameworks or libraries, and provides compile-time checking that everything is defined properly.