-
Hello there, this is a question from a beginner, but I'm not sure why I would want to use this - and have my code needing to use a library, am I tying myself into the use of the library to structure my code? I don't quite understand what benefits this is giving me over passing dependencies in initialisers, and then I can see what the code depends on by it needing it to initialise itself? And if there's something, A that depends on B, should there not be something else that gives A the B it needs to work, without A knowing how it gets given it? Apologies if this question is a bit dumb or silly. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 7 replies
-
Hi @Pikuseru, this isn't a silly question, and probably something we could cover better in the docs somewhere. Though hard to know if people would read it! Your question really goes to the root of why one would use a dependency management library, period. Not just this one, but any library. The benefits don't really show themselves when considering a feature A that creates a feature B. In such a situation passing dependencies explicitly isn't really that big of a deal. The real benefit comes when having dozens or even hundreds of features. Suppose you have feature 1 through 4, where feature 1 can navigate to 2, 2 can navigate to 3, and 3 can navigate to 4. And further suppose that feature 4 really needs a date generator dependency so that it can keep its logic testable. Then feature 1-4 all need the date generator, even though only feature 4 needs the dependency: struct Feature1 {
let date: () -> Date
init(@escaping date: () -> Date) {
self.date = date
}
func goToFeature2() {
… = Feature2(date: date)
}
}
struct Feature2 {
let date: () -> Date
init(@escaping date: () -> Date) {
self.date = date
}
func goToFeature3() {
… = Feature3(date: date)
}
}
struct Feature3 {
let date: () -> Date
init(@escaping date: () -> Date) {
self.date = date
}
func goToFeature4() {
… = Feature4(date: date)
}
}
struct Feature4 {
let date: () -> Date
init(@escaping date: () -> Date) {
self.date = date
}
} And one could argue that it's good that feature 1-3 are forced to have a But things really start to breakdown when a leaf feature suddenly needs access to a new dependency, like an API client: struct Feature4 {
+ let apiClient: APIClient
let date: () -> Date
init(
+ apiClient: APIClient,
@escaping date: () -> Date
) {
+ self.apiClient = apiClient
self.date = date
}
} That unfortunately reverberates throughout your entire app, forcing you to fix every feature that touches struct Feature1 {
+ let apiClient: APIClient
let date: () -> Date
init(
+ apiClient: APIClient,
@escaping date: () -> Date
) {
+ self.apiClient = apiClient
self.date = date
}
func goToFeature2() {
… = Feature2(
+ apiClient: apiClient,
date: date
)
}
}
struct Feature2 {
+ let apiClient: APIClient
let date: () -> Date
init(
+ apiClient: APIClient,
@escaping date: () -> Date
) {
+ self.apiClient = apiClient
self.date = date
}
func goToFeature3() {
… = Feature3(
+ apiClient: apiClient,
date: date
)
}
}
struct Feature4 {
+ let apiClient: APIClient
let date: () -> Date
init(
+ apiClient: APIClient,
@escaping date: () -> Date
) {
+ self.apiClient = apiClient
self.date = date
}
func goToFeature4() {
… = Feature4(
+ apiClient: apiClient,
date: date
)
}
} This makes it incredibly difficult to add dependencies to features when they need them, and you may find yourself taking shortcuts because it is such a pain. Perhaps the most common shortcut is to use default values in initializers so that you are not forced to always pass along dependencies: init(
apiClient: APIClient = LiveAPIClient(),
@escaping date: () -> Date = { Date() }
) {
self.apiClient = apiClient
self.date = date
} This now means you can do However, this breaks down when testing the integration of features. If you were to follow this pattern, and try to write a test for Compare all of this to just using struct Feature4 {
@Dependency(\.apiClient) var apiClient
@Dependency(\.date) var date
…
} And if a feature doesn't use a dependency it does not need to declare it: struct Feature1 {
// No @Dependency if not needed
…
}
struct Feature2 {
// No @Dependency if not needed
…
}
struct Feature3 {
// No @Dependency if not needed
…
} And if a leaf feature needs to add a new dependency, it can do so without touching a single other feature in the app: struct Feature4 {
+ @Dependency(\.locationManager) var locationManager
…
} And further, in tests if a feature even so much as touches a And this is really just scratching the surface. I recommend reading the various docs and articles in the repo, and I also gave a talk on this subject last year that you might be interested in. |
Beta Was this translation helpful? Give feedback.
Hi @Pikuseru, there's no better way to compare multiple approaches to a problem than to build the same thing multiple times, once for each approach. 🙂
We have a moderately complex app called SyncUps that was built using
@Dependency
. Perhaps you could fork it and then demonstrate the technique you are describing? That would help you see any pros/cons, and help me understand your idea.