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