当前位置:首页 > 科技  > 软件

编写更清晰代码:去掉所有多余的类型

来源: 责编: 时间:2023-10-13 14:34:36 190观看
导读最近,在 r/swift 子论坛上,我偶然发现了一篇介绍“整洁架构”项目示例的帖子。这引起了我的兴趣,于是我决定在 GitHub 上下载并仔细研究。帖子截图初看代码颇为复杂,让我感到迷惑。但在下载和深入研究后,我发现所有组件都

最近,在 r/swift 子论坛上,我偶然发现了一篇介绍“整洁架构”项目示例的帖子。这引起了我的兴趣,于是我决定在 GitHub 上下载并仔细研究。Qqr28资讯网——每日最新资讯28at.com

帖子截图帖子截图Qqr28资讯网——每日最新资讯28at.com

初看代码颇为复杂,让我感到迷惑。但在下载和深入研究后,我发现所有组件都整合在一起,项目实现了想要的功能。但我发现该项目的网络模块的复杂性较高。仅两个简单的网络查询操作竟涉及如此多的文件,让人难以理解,让我颇为惊讶。Qqr28资讯网——每日最新资讯28at.com

因此,我决定对网络层进行重构,使其更加模块化,并对整体组合和用户界面进行了小幅优化。为此,我创建了独立的项目对原始项目代码进行重构,你可以在文末找到原始项目和我重构后的项目链接。Qqr28资讯网——每日最新资讯28at.com

网络层——消除嵌套和多余类型

原项目的网络层通过协议和类型结构实现了高度模块化,每个协议和类型分别负责特定功能,大致结构如下:Qqr28资讯网——每日最新资讯28at.com

NetworkManager -> RequestManager -> RequestProtocol -> DataParser -> DataSource -> Repository -> UseCase

上述每一个类型都承担了网络过程的一部分职责,例如 DataParser 负责数据解析,如果想改变数据的解析方式,可以通过替换新的 DataParser 来实现,这种组合性是一项优点。Qqr28资讯网——每日最新资讯28at.com

但问题在于,由于这些类型相互嵌套,使人难以整体理解,且每个类型都存于单独的文件中。许多通过 Swinject 解析器进行注入,这使得整个网络层的工作流程变得难以追踪。正如 r/swift 中的一名评论者所言,这为代码增加了一层不必要的“中间层”。Qqr28资讯网——每日最新资讯28at.com

更令人费解的是,尽管作者增加了许多协议和类型来提高代码的灵活性,但其中存在很多硬编码的默认值。例如,DataParser 被直接编码在代码中,而 RequestProtocol.request() 的创建仅通过协议本身的扩展方法来实现。这种在增加了类型和复杂性后未充分利用它们的优势的做法,实在让人觉得可惜。Qqr28资讯网——每日最新资讯28at.com

为了消除冗余的嵌套以及不必要的类型和协议,我们可以引入一个全新的方法:modelFetcher。Qqr28资讯网——每日最新资讯28at.com

static func modelFetcher<T, U: Codable>(    createURLRequest: @escaping (T) throws -> URLRequest,    store: NetworkStore = .urlSession) -> (T) async -> Result<BaseResponseModel<PaginatedResponseModel<U>>, AppError> {    let networkFetcher = selfworkFetcher(store: store)    let mapper: (Data) throws -> BaseResponseModel<PaginatedResponseModel<U>> = jsonMapper()    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),而是通过直接注入操作行为来实现。需要说明的是,如果这样更方便,你还可以将其构造成一个带闭包的结构体,而不仅限于闭包。Qqr28资讯网——每日最新资讯28at.com

接下来,实际的请求获取闭包创建过程被大大简化,唯一会变化的是请求创建部分。Qqr28资讯网——每日最新资讯28at.com

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 负责实际将数据发送到网络,我们将其传递到构造函数中是为了方便后续的测试模拟(如果有需要的话)。Qqr28资讯网——每日最新资讯28at.com

下面的例子展示了如何通过使用行为替代类型,将原始项目中的协议和类型进行转换:Qqr28资讯网——每日最新资讯28at.com

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    }}

这段代码还可以继续优化变得更简洁:Qqr28资讯网——每日最新资讯28at.com

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)    }}

可以看出,我们在移除类型和协议的情况下实现了相同的功能。Qqr28资讯网——每日最新资讯28at.com

另一个案例是通过函数创建一个 JSON 映射器,并将其作为闭包返回,保留协议的灵活性,却不依赖协议。例如:Qqr28资讯网——每日最新资讯28at.com

static func jsonMapper<T: Decodable>() -> (Data) throws -> T {    let decoder = JSONDecoder()    decoder.keyDecodingStrategy = .convertFromSnakeCase    return { data in            try decoder.decode(T.self, from: data)           }}

在我看来,与基于协议/类型的方法相比,这种组合方式让网络层的实现变得更为直观和简洁。Qqr28资讯网——每日最新资讯28at.com

这并不意味着你不应使用协议,但在选择使用协议和类型时,应明确了解其用途,并思考是否真的需要为每 2-3 行代码创建一个完整的类型。Qqr28资讯网——每日最新资讯28at.com

项目模块划分

总体上,应用程序的模块划分还算理想。然而,我觉得可以进一步完善项目,方法是对网络模块进行明确的划分。让我们思考一下:应用程序真的需要了解它将使用哪个 JSON 映射器作为网络特性吗?我们是否可以更改网络特性的JSON映射器而不破坏整个结构?如果网络模块能够自主处理这些内容,那就更好了,这样我们可以专注于使用它的主要目的:获取超级英雄数据。Qqr28资讯网——每日最新资讯28at.com

我们应该限制网络模块接收的内容,仅限于有意识地改变的部分,如用于测试的输入,而不过多暴露。此外,我们可以只公开实际使用的部分,例如fetcher功能,而不是整个NetworkStore模块的所有底层特性,并将其设为public。Qqr28资讯网——每日最新资讯28at.com

值得注意的是,网络模块不应涉及域的内容,最好将ArkanaKeys依赖从整个项目中独立出来,单独置于网络模块中。拥有一个完全隔离的网络模块,可以让我们在制作任何关于漫威超级英雄的应用时,轻松地复用所有的网络逻辑。Qqr28资讯网——每日最新资讯28at.com

在提供的示例代码中,我仅进行了“虚拟模块化”操作,没有为网络模块创建独立的框架,也没有将ArkanaKeys的依赖关系转移到那里。相反,我创建了一个文件夹并加入了访问控制,模拟了完全独立框架的情形。这样做是为了使演示项目简洁,实际上,你只需创建一个框架并添加到项目中即可。Qqr28资讯网——每日最新资讯28at.com

另一个更远大的目标是将 UI 和演示逻辑进行分离。目前这两者相当耦合,我觉得这并不是问题。我删除了 Presentation 文件夹,并把它们和 UI 层放在一起,因为在这一点上,很难想象使用 HomeViewModel来做除了 HomeView以外的事情,但这是一个组织代码的个人喜好问题。Qqr28资讯网——每日最新资讯28at.com

我最终使用了一个简单的 Container类来代替 Swinject,但这也是个人喜好的问题。无论如何,解析器/容器应该避免尝试解析太多具体的网络类型,比如 NetworkManager, DataSource, Repositories和UseCases。在这种情况下,让我们注入 NetworkStore(我用来替换 NetworkManager的类型)并直接解析UseCase 的依赖。Qqr28资讯网——每日最新资讯28at.com

UI 层的优化更新

以下是关于 UI 层的一些优化更新,通过减少缩进和删除 AnyView类型来提高可读性和性能。将 View从 body中提取出来以提高可读性,在我看来,尽可能减少缩进到只有几个级别是有帮助的。原始应用程序在 HomeView中达到了 13 个缩进级别!而且,它是应用程序的根视图,所以从一开始就尽可能地使其可读是一个好主意。通过将 homeView提取为一个计算属性,我们可以很容易地将缩进减少到只有五个级别。
示例如下:Qqr28资讯网——每日最新资讯28at.com

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,比如成功/空/创建/加载,而不是让它们在构造函数中自动为我们完成。
示例如下:Qqr28资讯网——每日最新资讯28at.com

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等名称。Qqr28资讯网——每日最新资讯28at.com

在 SwiftUI 的上下文中,使用单一基础控制器/视图的方法可能不太符合习惯。与直接将所有这些状态处理器添加到基础视图上相比,以 ViewModifiers的形式将它们组合起来并添加感觉更为自然。不过,每种方法都有其优劣之处。如果你想强调构造函数的使用,并且想通过减少 ZStacks 的使用来实现,那么这种方法也是可取的。Qqr28资讯网——每日最新资讯28at.com

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)            }        }    }}

结论

衷心感谢 mohaned_y98 提供的启发和出色的示例项目!本文基于清晰的架构原则,采用了与原始项目不同的风格进行探索。相较于我所重构的项目,原始项目有其独特的优势,你可根据项目需求选择适合的设计方案。Qqr28资讯网——每日最新资讯28at.com

在尽量保留初衷的同时,我对项目进行了重构,增强了其人体工程学和可读性。鉴于用户界面或展示层已经构建得非常稳固,我未在这些方面投入过多精力。如果从头开始,我可能会选择不同的编码方式,但现有的代码编写得恰到好处,且运作正常。Qqr28资讯网——每日最新资讯28at.com

原始项目和我重构后的项目链接放在下方,欢迎下载阅读我重构后的项目。你认为我忽略了哪些方面?你会有哪些不同的实现方法?Qqr28资讯网——每日最新资讯28at.com

原始项目: https://github.com/Mohanedy98/swifty-marvel我重构后的项目:https://github.com/terranisaur/Demo-SwiftyMarvelousQqr28资讯网——每日最新资讯28at.com

译者介绍

刘汪洋,51CTO社区编辑,昵称:明明如月,一个拥有 5 年开发经验的某大厂高级 Java 工程师,拥有多个主流技术博客平台博客专家称号。Qqr28资讯网——每日最新资讯28at.com

原文标题:Clean Code Review: Removing All the Extra Types,作者:Alex ThurstonQqr28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-13252-0.html编写更清晰代码:去掉所有多余的类型

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: Tailwind CSS 真有那么好吗?讨厌它的前六大原因

下一篇: 如何更优雅的编程?面向接口编程四大法宝!

标签:
  • 热门焦点
Top