如何在视图与实现间构建协议/接口分离层?——基于SOLID与MVVM架构的多服务泛型协议问题求解
我太懂这种被泛型协议卡脖子的感觉了——带关联类型和Self约束的协议看起来完美契合依赖倒置的设计,但真要拿来当变量或者存进集合里,编译器分分钟给你甩错误。手动写每个服务对应的类型擦除类确实是个苦力活,尤其是服务多的时候。下面给你几个实用的思路,包括一个能简化类型擦除的通用方案,还有替代的架构方向。
一、通用型自动类型擦除方案
我们可以写一个万能的AnyService结构体,它能包装任何符合Service协议的实例,同时处理关联类型、Hashable和Codable这些带Self约束的要求。核心是把协议的关联类型转为通用参数,用闭包捕获原实例的行为:
struct AnyService<Config: ServerConfiguration>: Service, Hashable, Codable { private let wrappedConfiguration: Config private let performJobClosure: () -> Void private let hashClosure: (inout Hasher) -> Void private let encodeClosure: (Encoder) throws -> Void private let decodeFactory: (Config) throws -> any Service // 初始化方法:包装任意符合Service协议的实例 init<Wrapped: Service>(_ wrapped: Wrapped, decodeFactory: @escaping (Config) throws -> Wrapped) where Wrapped.Configuration == Config, Wrapped: Codable { self.wrappedConfiguration = wrapped.configuration self.performJobClosure = wrapped.performJob self.hashClosure = wrapped.hash(into:) self.encodeClosure = wrapped.encode(to:) self.decodeFactory = decodeFactory } // 实现Service协议要求 var configuration: Config { wrappedConfiguration } init(using configuration: Config) { fatalError("请使用init(_ wrapped:, decodeFactory:)创建实例") } func performJob() { performJobClosure() } // 实现Hashable func hash(into hasher: inout Hasher) { hashClosure(&hasher) } static func == (lhs: AnyService<Config>, rhs: AnyService<Config>) -> Bool { lhs.wrappedConfiguration == rhs.wrappedConfiguration } // 实现Codable func encode(to encoder: Encoder) throws { try encodeClosure(encoder) } init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.wrappedConfiguration = try container.decode(Config.self) self.performJobClosure = { try decodeFactory(self.wrappedConfiguration).performJob() } self.hashClosure = { hasher in try decodeFactory(self.wrappedConfiguration).hash(into: &hasher) } self.encodeClosure = { encoder in try decodeFactory(self.wrappedConfiguration).encode(to: encoder) } self.decodeFactory = { try self.decodeFactory($0) } } }
使用的时候只需要一行代码包装你的服务:
let config = SomeServerConfiguration(baseURL: URL(string: "https://example.com")!) let authService = SomeAuthenticationService(using: config) let anyAuthService = AnyService(authService, decodeFactory: SomeAuthenticationService.init(using:))
这样你就可以用AnyService<SomeServerConfiguration>作为统一类型,存储不同的服务实例了。
二、用服务注册器统一管理实例
如果觉得类型擦除还是麻烦,你可以搞一个服务注册器,把服务的创建逻辑统一托管,完全避免直接引用泛型协议:
class ServiceRegistry { static let shared = ServiceRegistry() private var serviceFactories: [ObjectIdentifier: (any ServerConfiguration) -> any Service] = [:] // 注册服务工厂 func register<ServiceType: Service>(factory: @escaping (ServiceType.Configuration) -> ServiceType) { let key = ObjectIdentifier(ServiceType.self) serviceFactories[key] = { config in guard let typedConfig = config as? ServiceType.Configuration else { fatalError("配置类型不匹配 \(ServiceType.self)") } return factory(typedConfig) } } // 获取服务实例 func getService<ServiceType: Service>(with config: ServiceType.Configuration) -> ServiceType { let key = ObjectIdentifier(ServiceType.self) guard let factory = serviceFactories[key], let service = factory(config) as? ServiceType else { fatalError("未注册 \(ServiceType.self) 服务或配置无效") } return service } }
使用流程很简单:先注册服务,再按需获取:
// 注册服务(建议在App启动时完成) ServiceRegistry.shared.register { config in SomeAuthenticationService(using: config) } // 在需要的地方获取服务 let config = SomeServerConfiguration(baseURL: URL(string: "https://example.com")!) let authService: SomeAuthenticationService = ServiceRegistry.shared.getService(with: config)
这个方案既保留了依赖倒置的优势,又不用处理泛型协议的类型问题,还能和依赖注入结合让代码更解耦。
三、调整协议设计,去掉关联类型
如果可以接受少量运行时类型检查,直接修改协议设计是最快捷的解决方式:把关联类型换成存在类型any,牺牲一点类型安全换编译器友好:
protocol Service: ObservableObject, Hashable, Codable { var configuration: any ServerConfiguration { get } init(using configuration: any ServerConfiguration) func performJob() }
对应的实现类调整为:
class SomeAuthenticationService: AuthenticationService { var configuration: any ServerConfiguration required init(using configuration: any ServerConfiguration) { guard let typedConfig = configuration as? SomeServerConfiguration else { fatalError("配置类型不匹配") } self.configuration = typedConfig } func performJob() {} }
这样你就可以直接用any Service作为类型来引用实例,完全不用处理类型擦除。
总结
- 想要严格类型安全又不想重复写擦除代码:通用
AnyService+ 服务注册器的组合是最优解,注册器还能帮你统一管理解码逻辑。 - 追求最快解决问题:调整协议去掉关联类型,虽然有运行时类型检查,但开发成本最低。
- 服务注册器的方式还能很好地适配依赖注入,让你的架构扩展性更强。
内容的提问来源于stack exchange,提问作者zombie
相关产品推荐
相关产品推荐

