You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中实现BaseViewModel遵循DRY原则的方案优化咨询

Swift中实现BaseViewModel遵循DRY原则的方案优化咨询

从Android生态转到Swift开发,你想通过BaseViewModel复用refresh逻辑的思路非常棒,完全贴合**DRY(Don't Repeat Yourself)**原则!不过你的现有实现还有几个可以优化的点,既能更贴合Swift的语言特性,也能提升代码的健壮性和可维护性,我来逐一分析:

一、现有方案的小隐患:返回nil的占位方法

你在基类中用返回nil的getSuccessResult()作为占位,虽然能让代码运行,但存在静默失败的风险:如果某个子类忘记重写这个方法,基类返回的nil会导致refresh方法中的if let successResult = ...分支不执行,页面会一直停留在loading状态,却没有任何报错信息,调试起来非常麻烦。

优化建议:把基类的占位方法改成抛出致命错误,这样子类一旦忘记实现,运行时会直接崩溃并给出明确提示:

func getSuccessResult() async throws -> T? {
    fatalError("⚠️ Subclasses must override getSuccessResult() method!")
}

这样能快速发现未实现的子类,避免静默失败的调试噩梦。

二、更贴合Swift特性:用协议+默认实现替代单一基类

Swift没有抽象类,但**协议(Protocol)+ 扩展(Extension)**的组合可以实现更灵活的"抽象"能力,而且避免了类继承的单继承限制。你可以把ViewModel的核心逻辑抽成协议,并通过扩展提供refresh的默认实现:

1. 定义核心协议与通用枚举

先把通用的Resource枚举抽成顶层类型,再定义ViewModel协议:

// 通用状态枚举,可全局复用
enum Resource<DataType> {
    case loading
    case success(DataType)
    case error(String)
}

// 定义ViewModel的核心协议
protocol BaseViewModelProtocol: ObservableObject {
    associatedtype DataType
    @Published var result: Resource<DataType> { get set }
    var apiService: APIService { get }
    
    func getSuccessResult() async throws -> DataType
    func refresh()
}

2. 给协议添加refresh的默认实现

通过扩展协议,把复用的refresh逻辑统一实现:

extension BaseViewModelProtocol {
    func refresh() {
        result = .loading
        Task { @MainActor in
            do {
                // 这里直接返回非可选类型,避免额外的可选绑定
                let successData = try await getSuccessResult()
                result = .success(successData)
            } catch APIService.GHError.invalidURL {
                result = .error("Invalid URL")
            } catch APIService.GHError.invalidResponse {
                result = .error("Invalid response")
            } catch APIService.GHError.invalidData {
                result = .error("Invalid data")
            } catch {
                result = .error("Unexpected error")
            }
        }
    }
}

3. 子类实现更简洁

子类只需遵守协议,实现getSuccessResult()即可,无需继承基类:

class GithubViewModel: BaseViewModelProtocol {
    @Published var result: Resource<[UserWrapper]> = .loading
    let apiService = APIService()
    
    func getSuccessResult() async throws -> [UserWrapper] {
        async let following = apiService.getUsers(endPoint: Constants.followingEndPoint)
        async let followers = apiService.getUsers(endPoint: Constants.followersEndPoint)
        return try await UserWrapper.createUsers(following: following, followers: followers)
    }
    
    private struct Constants {
        private static let endPoint = "https://api.github.com/users/alirezaeiii/"
        static let followingEndPoint = endPoint + "following"
        static let followersEndPoint = endPoint + "followers"
    }
}

这种方式的优势在于:

  • 摆脱了类继承的单继承限制,子类可以同时遵守其他协议
  • 协议的关联类型比类泛型更灵活,适配场景更多
  • 核心逻辑与实现分离,代码结构更清晰

三、额外优化点:提升代码可测试性

基类中直接初始化APIService的方式,会导致单元测试时无法替换成Mock服务。可以通过构造注入的方式,让APIService的实例可配置:

// 基类版的注入改造
class BaseViewModel<T>: ObservableObject {
    typealias GHError = APIService.GHError
    
    @Published var result = Resource<T>.loading
    let apiService: APIService // 改为外部注入
    
    // 默认使用真实的APIService,测试时可传入Mock实例
    init(apiService: APIService = APIService()) {
        self.apiService = apiService
        refresh()
    }
    
    // ... 其他代码保持不变
}

// 子类初始化时也可以沿用注入逻辑
class GithubViewModel : BaseViewModel<[UserWrapper]> {
    init(apiService: APIService = APIService()) {
        super.init(apiService: apiService)
    }
    
    // ... 其他代码保持不变
}

这样在单元测试中,你可以轻松传入一个模拟的APIService,验证ViewModel的状态变化,无需依赖真实网络请求。

总结

你的初始方案已经抓住了复用的核心,以上优化主要是让代码更贴合Swift的语言习惯,同时提升健壮性和可测试性。如果团队更习惯类继承的方式,用fatalError替代返回nil是最低成本的优化;如果想追求更灵活的架构,协议+默认实现的方案会更合适。

备注:内容来源于stack exchange,提问作者Ali

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 09:55:37