闽公网安备 35020302035485号

NetworkManager -> RequestManager -> RequestProtocol -> DataParser -> DataSource -> Repository -> UseCase上述每一个类型都承担了网络过程的一部分职责,例如 DataParser 负责数据解析,如果想改变数据的解析方式,可以通过替换新的 DataParser 来实现,这种组合性是一项优点。
static func modelFetcher<T, U: Codable>(
createURLRequest: @escaping (T) throws -> URLRequest,
store: NetworkStore = .urlSession
) -> (T) async -> Result<BaseResponseModel<PaginatedResponseModel<U>>, AppError> {
let networkFetcher = self.networkFetcher(store: store)
let mapper: (Data) throws -> BaseResponseModel<PaginatedResponseModel<U>> = jsonMapper()
// 堆代码 duidaima.com
let fetcher = self.fetcher(
createURLRequest: createURLRequest,
fetch: { request -> (Data, URLResponse) in
try await networkFetcher(request)
}, mapper: { data -> BaseResponseModel<PaginatedResponseModel<U>> in
try mapper(data)
})
return { params in
await fetcher(params)
}
}
此函数的设计旨在保持与原代码相同的组合功能,但未采用协议(protocols)和类型(types),而是通过直接注入操作行为来实现。需要说明的是,如果这样更方便,你还可以将其构造成一个带闭包的结构体,而不仅限于闭包。static func characterFetcher(
store: NetworkStore = .urlSession
) -> (CharacterFetchData) async -> Result<BaseResponseModel<PaginatedResponseModel<CharacterModel>>, AppError> {
let createURLRequest = { (data: CharacterFetchData) -> URLRequest in
var urlParams = ["offset": "\(data.offset)", "limit": "\(APIConstants.defaultLimit)"]
if let searchKey = data.searchKey {
urlParams["nameStartsWith"] = searchKey
}
return try createRequest(
requestType: .GET,
path: "/v1/public/characters",
urlParams: urlParams
)
}
return self.modelFetcher(createURLRequest: createURLRequest)
}
优化后,我们无需深入到许多不同的文件中,也无需理解众多的协议和类型,因为我们可以通过直接注入闭包来实现相同的行为。NetworkStore 负责实际将数据发送到网络,我们将其传递到构造函数中是为了方便后续的测试模拟(如果有需要的话)。protocol NetworkManager {
func makeRequest(with requestData: RequestProtocol) async throws -> Data
}
class DefaultNetworkManager: NetworkManager {
private let urlSession: URLSession
init(urlSession: URLSession = URLSession.shared) {
self.urlSession = urlSession
}
func makeRequest(with requestData: RequestProtocol) async throws -> Data {
let (data, response) = try await urlSession.data(for: requestData.request())
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else { throw NetworkError.invalidServerResponse }
return data
}
}
这段代码还可以继续优化变得更简洁:static func networkFetcher(
store: NetworkStore
) -> (URLRequest) async throws -> (Data, URLResponse) {
{ request in
let (data, response) = try await store.fetchData(request)
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode != 200 {
throw NetworkError.invalidServerResponse
}
return (data, response)
}
}
可以看出,我们在移除类型和协议的情况下实现了相同的功能。另一个案例是通过函数创建一个 JSON 映射器,并将其作为闭包返回,保留协议的灵活性,却不依赖协议。例如:static func jsonMapper<T: Decodable>() -> (Data) throws -> T {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return { data in
try decoder.decode(T.self, from: data)
}
}
在我看来,与基于协议/类型的方法相比,这种组合方式让网络层的实现变得更为直观和简洁。这并不意味着你不应使用协议,但在选择使用协议和类型时,应明确了解其用途,并思考是否真的需要为每 2-3 行代码创建一个完整的类型。public var body: some View {
NavigationStack {
ZStack {
BaseStateView(
viewModel: viewModel,
successView: homeView,
emptyView: BaseStateDefaultEmptyView(),
createErrorView: { errorMessage in
BaseStateDefaultErrorView(errorMessage: errorMessage)
},
loadingView: BaseStateDefaultLoadingView()
)
}
}
.task {
await viewModel.loadCharacters()
}
}
我想最后提一下的是,这个应用使用了一个 BaseStateView,它接受四个不同的 AnyView来表示应用的不同状态,比如成功、空、错误等。BaseStateView使用泛型来代替 AnyView会更合适,因为 AnyView对于 SwiftUI 来说并不总是性能很好。这样会提高性能,但是一个缺点是,它让我们必须传入我们想要的具体的 View,比如成功/空/创建/加载,而不是让它们在构造函数中自动为我们完成。struct BaseStateView<S: View, EM: View, ER: View, L: View>: View {
@ObservedObject var viewModel: ViewModel
let successView: S
let emptyView: EM?
let createErrorView: (_ errorMessage: String?) -> ER?
let loadingView: L?
...
}
为了提高可读性,你可以使用如SuccessView、EmptyView等名称。struct ErrorStateViewModifier<ErrorView: View>: ViewModifier {
@ObservedObject var viewModel: ViewModel
let errorView: (String) -> ErrorView
func body(content: Content) -> some View {
ZStack {
content
if case .error(let message) = viewModel.state {
errorView(message)
}
}
}
}
在尽量保留初衷的同时,我对项目进行了重构,增强了其人体工程学和可读性。鉴于用户界面或展示层已经构建得非常稳固,我未在这些方面投入过多精力。如果从头开始,我可能会选择不同的编码方式,但现有的代码编写得恰到好处,且运作正常。
原始项目和我重构后的项目链接放在下方,欢迎下载阅读我重构后的项目。你认为我忽略了哪些方面?你会有哪些不同的实现方法?
原始项目: https://github.com/Mohanedy98/swifty-marvel
我重构后的项目:https://github.com/terranisaur/Demo-SwiftyMarvelous