Рефакторинг UIKit MVVM ProductViewModel на Combine
View model ниже использует свой Observable<T> и принимает context: UIViewController, который никогда не используется. Отрефакторите её так, чтобы состояние отдавалось через Combine, а зависимость от UIKit исчезла — тогда модель станет независимой от фреймворка и тестируемой.
Требования:
- Откройте
product,inCartCount,favouriteButtonTextкак Combine-publisher'ы (без своегоObservable). - Полностью уберите параметр
context: UIViewController. - Внедряйте репозитории как протоколы, а не создавайте их внутри init.
final class ProductViewModel {
let product: Observable<Product?>
let inCartCount: Observable<Int>
let favouriteButtonText: Observable<String>
init(productId: Int, context: UIViewController) {
// ваш код здесь — переработайте инициализатор и хранимое состояние
}
}
Перепишите реализацию.
Откройте состояние через @Published-свойства (или CurrentValueSubject) вместо своего Observable и уберите неиспользуемый context: UIViewController, чтобы модель не зависела от UIKit. Внедряйте репозитории как протоколы, а контроллер пусть подписывается и хранит свои AnyCancellable.
- ✗Подписываются, не сохраняя cancellable, из-за чего связывания сразу рвутся
- ✗Создают репозитории прямо в init вместо внедрения протоколов
- →Как внедрение репозиториев через протоколы сделает эту view model тестируемой?
- →Где должно формироваться название кнопки и почему не в замыкании контроллера?
Внедрите протоколы репозиториев, отдавайте состояние через @Published и уберите UIKit-параметр:
protocol ProductRepositoryProtocol { func getProduct(by id: Int, completion: @escaping (Product?) -> Void) }
protocol CartRepositoryProtocol { func inCartCount(for id: Int, completion: @escaping (Int) -> Void) }
protocol FavouriteRepositoryProtocol { func isFavourite(_ id: Int, completion: @escaping (Bool) -> Void) }
final class ProductViewModel {
@Published private(set) var product: Product?
@Published private(set) var inCartCount: Int = 0
@Published private(set) var favouriteButtonText: String = ""
init(productId: Int,
products: ProductRepositoryProtocol,
cart: CartRepositoryProtocol,
favourites: FavouriteRepositoryProtocol) {
products.getProduct(by: productId) { [weak self] in self?.product = $0 }
cart.inCartCount(for: productId) { [weak self] in self?.inCartCount = $0 }
favourites.isFavourite(productId) { [weak self] in
self?.favouriteButtonText = $0 ? "Remove from Favourites" : "Add to Favourites"
}
}
}
Контроллер подписывается на $inCartCount, $favouriteButtonText, $product и хранит возвращённые значения в Set<AnyCancellable>. Теперь во view model нет импорта UIKit, поэтому её можно тестировать с mock-репозиториями. Форматирование названия кнопки ("In Cart (n)") — во view model.