Reputation: 870
I have created a project that contains my networking code. I have another project that contains some services and view controllers related to a users profiles.
My networking layer has the following interface
public protocol HTTPClientTask {
func cancel()
}
public typealias HTTPClientResult = Result<(response: HTTPURLResponse, data: Data), Error>
public protocol HTTPClient {
/// The completion handler can be invoked in any thread.
/// Clients are responsible for dispatching to the appropriate thread, if needed.
@discardableResult
func execute(_ request: URLRequest, completion: @escaping (HTTPClientResult) -> Void) -> HTTPClientTask
}
My concrete class conforms to HTTPClient
and currently I am injecting this dependancy injection.
However this requires my profile module to be aware of my networking module. This creates a coupling between those modules I'd like to remove.
As HTTPClient
only has a single method that returns a task, I thought I could perhaps update my profile module to accept the function signature instead of the client.
This way only the composition root within my app will be aware of both modules which I believe is acceptable.
What I cannot quite get my head around is how to represent the HTTPClient
interface as a simple function.
I believe it is:
typealias ClientResult = Result<(response: HTTPURLResponse, data: Data), Error>
typealias ClientMethod = (URLRequest, @escaping (ClientResult) -> Void) -> HTTPClientTask
However I cannot represent the HTTPClientTask
property also.
This should be an object with a method on that returns void.
Essentially I'd like to pass the execute
method as an argument, but cannot workout how to represent it as a type without using the current protocols.
An example of how this interface is implemented can be found below:
public final class URLSessionHTTPClient: HTTPClient {
private let session: URLSession
private struct UnexpectedValuesRepresentationError: Error { }
private struct URLSessionTaskWrapper: HTTPClientTask {
let wrapped: URLSessionTask
func cancel() {
wrapped.cancel()
}
}
public init(session: URLSession = .shared) {
self.session = session
}
public func execute(_ request: URLRequest, completion: @escaping (HTTPClientResult) -> Void) -> HTTPClientTask {
let task = session.dataTask(with: request) { data, response, error in
completion(Result {
if let error = error {
throw error
} else if let data = data, let response = response as? HTTPURLResponse {
return (response, data)
} else {
throw UnexpectedValuesRepresentationError()
}
})
}
task.resume()
return URLSessionTaskWrapper(wrapped: task)
}
}
I have added a playground below, I hope makes it clearer.
import UIKit
public protocol HTTPClientTask {
func cancel()
}
public typealias HTTPClientResult = Result<(response: HTTPURLResponse, data: Data), Error>
public protocol HTTPClient {
/// The completion handler can be invoked in any thread.
/// Clients are responsible for dispatching to the appropriate thread, if needed.
@discardableResult
func execute(_ request: URLRequest, completion: @escaping (HTTPClientResult) -> Void) -> HTTPClientTask
}
public final class URLSessionHTTPClient: HTTPClient {
private let session: URLSession
private struct UnexpectedValuesRepresentationError: Error { }
private struct URLSessionTaskWrapper: HTTPClientTask {
let wrapped: URLSessionTask
func cancel() {
wrapped.cancel()
}
}
public init(session: URLSession = .shared) {
self.session = session
}
public func execute(_ request: URLRequest, completion: @escaping (HTTPClientResult) -> Void) -> HTTPClientTask {
let task = session.dataTask(with: request) { data, response, error in
completion(Result {
if let error = error {
throw error
} else if let data = data, let response = response as? HTTPURLResponse {
return (response, data)
} else {
throw UnexpectedValuesRepresentationError()
}
})
}
task.resume()
return URLSessionTaskWrapper(wrapped: task)
}
}
let client = URLSessionHTTPClient(session: .init(configuration: .ephemeral))
typealias ClientResult = Result<(response: HTTPURLResponse, data: Data), Error>
typealias ClientMethod = (URLRequest, @escaping (ClientResult) -> Void) -> HTTPClientTask // I want to remove the knowledge of `HTTPClientTask` from the typealias
class Loader {
private let load: ClientMethod
init(load: @escaping ClientMethod) {
self.load = load
}
func get() {
let url = URL(string: "https://google.com")!
load(.init(url: url)) { result in
print(result)
}
}
}
let loader = Loader(load: client.execute(_:completion:))
loader.get()
Upvotes: 2
Views: 72
Reputation: 2478
I believe URLSessionTaskWrapper
is probably making it difficult to represent this without the use of an abstract interface such as HTTPClientTask
.
You could perhaps return the URLSessionTask
itself.
public protocol HTTPClient {
@discardableResult
func execute(_ request: URLRequest, completion: @escaping (HTTPClientResult) -> Void) -> URLSessionTask
}
...
typealias ClientMethod = (URLRequest, @escaping (ClientResult) -> Void) -> URLSessionTask
Upvotes: 1