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

