How can we make default implementations safer?

I usually take a somewhat different approach here and try to always deal with concrete types in these scenarios. So I'd keep the original protocols:

public protocol ServerResponse {
}

public protocol CacheableResponse {
    var cacheKey: String { get }
}

And try to write code so every function that returns a response actually returns some ServerResponse.

Then, for the consumers of ServerResponse, I'd just maintain a separate path for types that conform to both ServerResponse and CacheableResponse:

struct ResponseHandler {
   func handleResponse(_ response: some ServerResponse) {
        // Common function for all paths
        _handleResponse(response)
    }
    
    func handleResponse<Response: ServerResponse & CacheableResponse>(
        _ response: Response
    ) {
        // Do whatever you need with the cacheKey here...
        print(response.cacheKey)
        // Common function for all paths
        _handleResponse(response)
    }
    
    private func _handleResponse(_ response: some ServerResponse) {
        // Actually handle the response...
    }
}
2 Likes