Why are structs with closure vars are used instead of protocols? #522
-
I have just started learning TCA and I am going through the examples. I am struggling to understand why structs are used instead of protocols. Apologies if this is already explained. For example in AudioRecorderClient.swift, following declaration can be replaced: struct AudioRecorderClient { .. }
struct AudioRecorderClient {
var currentTime: (AnyHashable) -> Effect<TimeInterval?, Never>
var requestRecordPermission: () -> Effect<Bool, Never>
var startRecording: (AnyHashable, URL) -> Effect<Action, Failure>
var stopRecording: (AnyHashable) -> Effect<Never, Never>
enum Action: Equatable {
case didFinishRecording(successfully: Bool)
}
enum Failure: Equatable, Error {
case couldntCreateAudioRecorder
case couldntActivateAudioSession
case couldntSetAudioSessionCategory
case encodeErrorDidOccur
}
} by a protocol and associated implementations. protocol AudioRecorderClient { .. }
protocol AudioRecorderClient {
func currentTime(id: AnyHashable) -> Effect<TimeInterval?, Never>
func requestRecordPermission() -> Effect<Bool, Never>
func startRecording(id: AnyHashable, into filePath: URL) -> Effect<Action, Failure>
func stopRecording(id: AnyHashable) -> Effect<Never, Never>
enum Action: Equatable {
case didFinishRecording(successfully: Bool)
}
enum Failure: Equatable, Error {
case couldntCreateAudioRecorder
case couldntActivateAudioSession
case couldntSetAudioSessionCategory
case encodeErrorDidOccur
}
} Why is protocol based approach not used for external effects? I find it more readable than the struct+vars approach. Thanks, |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 1 reply
-
Hi Suraj |
Beta Was this translation helpful? Give feedback.
-
In addition to what @mstfy posted, there's a full collection of material on the subject here and here. There are many reasons to use a struct over a protocol (if possible), but I can distill one example for you. The main reason one puts an interface in front of a dependency (whether it be via structs or protocols) is so that you can create multiple instances of the dependency that serve different purposes. The most common are:
Whether you use structs or protocols for the interface of the dependency, creating the "live" and "mock" instances looks about the same: struct Dependency {
var fetch: (Int) -> Effect<User, Error>
var save: (User) -> Effect<Bool, Error>
static let live = Self(
fetch: { id in /* load user from disk/database/network */ },
save: { user in /* save user to disk/database/network */ }
)
static let mock = Self(
fetch: { _ in .init(value: User(name: "Blob") },
save: { _ in .init(value: true) }
)
}
// vs
protocol Dependency {
func fetch(id: Int) -> Effect<User, Error>
func save(user: User) -> Effect<Bool, Error>
}
struct LiveDependency: Dependency {
func fetch(id: Int) -> Effect<User, Error> {
// load user from disk/database/network
}
func save(user: User) -> Effect<Bool, Error> {
// save user to disk/database/network
}
}
struct MockDependency: Dependency {
func fetch(id: Int) -> Effect<User, Error> {
.init(value: User(name: "Blob"))
}
func save(user: User) -> Effect<Bool, Error> {
.init(value: true)
}
} However, for the mock instance you probably do not want to hard code the data directly into the instance. You may want an instance that successfully returns data for all endpoints to test the happy path, and then an instance that fails for all endpoints to test the unhappy path. Now you could certainly create For the struct style of the dependency you can simply override any endpoint to supply the data you want: var dependency = Dependency.mock
// Make fetch fail
dependency.fetch = { _ in .init(error: CouldNotLoad()) }
// Make save return false instead of true
dependency.save = { _ in .init(value: false) } But doing this for the protocol style is not possible. Because the protocol requirements are implemented with methods you cannot override them. You could enhance struct MockDependency: Dependency {
var fetchResult: Result<User, Error>
var saveResult: Result<Bool, Error>
func fetch(id: Int) -> Effect<User, Error> {
.result { self.fetchResult }
}
func save(user: User) -> Effect<Bool, Error> {
.result { self.saveResult }
}
}
var dependency = MockDependency()
dependency.fetchResult = .failure(CouldNotLoad())
dependency.saveResult = .success(false) But this also means can't provide custom logic for each endpoint. It just pinned to that one value. For example, say you wanted to simulate an instance of the dependency that successfully fetches a user for var dependency = Dependency.mock
dependency.fetchUser = { id in
id <= 100
? .init(value: User(name: "Blob \(id)"))
: .init(error: CouldNotLoad())
} So, to recover this with struct MockDependency: Dependency {
var fetch: (Int) -> Effect<User, Error>
var save: (User) -> Effect<Bool, Error>
func fetch(id: Int) -> Effect<User, Error> {
self.fetch(id)
}
func save(user: User) -> Effect<Bool, Error> {
self.save(user)
}
}
var dependency = MockDependency()
dependency.fetchUser = { id in
id <= 100
? .init(value: User(name: "Blob \(id)"))
: .init(error: CouldNotLoad())
} At this point That's why we decide to just start there. Start with a struct that has Now, having said all that, if you prefer to use protocols then that is totally fine. Protocols work great, they will continue to get better in the future, and I don't know of any reason why they won't work for a TCA application. |
Beta Was this translation helpful? Give feedback.
-
I know this is an old discussion and @mbrandonw explained quite well the advantages of the struct based interface. One way how you can take advantage of the interface using Protocols is to use a Mock generator, like
The generate mock will look like this.
To provide custom logic you can use the creation closure.
To conclude you can use both in the same way. One thing that the |
Beta Was this translation helpful? Give feedback.
In addition to what @mstfy posted, there's a full collection of material on the subject here and here.
There are many reasons to use a struct over a protocol (if possible), but I can distill one example for you. The main reason one puts an interface in front of a dependency (whether it be via structs or protocols) is so that you can create multiple instances of the dependency that serve different purposes. The most common are: