Architecture (swift)/MVVM (맛보기)
[iOS - swift] clean architecture - DI (필요한 곳에서 protocol에 선언하는 방법)
jake-kim
2021. 7. 13. 01:01
1. DI패턴 (필요한 곳에서 protocol에 선언하는 방법)
DI 패턴
- ADIContainer와 AViewModel이 있고 DIContainer에서 AViewModel를 만들 때, AViewModel에 필요한 값을 정의하는 방법
- DIP와 테스트에 용이하기 위하여 protocol을 통해 설계는 2. 테스트 구조를 고려한 DI패턴 참고
- 1. DI패턴 (필요한 곳에서 protocol에 선언하는 방법)의 목적
- DIContainer자체가 구현체가 되는 패턴 파악
- DIContainer가 구현체가 되는 '패턴'에 대해서 보며, 이 방법은 의미없다는것을 알고 Usecase위치는 ViewModel에 있어야한다는 것을 깨닫는 목적
- 테스트시에 DIContainer 구현체를 변경하는 일은 적으므로 ViewModel에서 Usecase를 init값에 받는것이 이상적
일차원적인 DI 방법
- A -> B화면전환 시, B의 Dependencies에 protocol이 아닌 class를 바로 참조하는 것
- 구성
- BViewController
- BViewModel
- BDIContainer
// BViewController.swift
class BViewController: UIViewController {
private var viewModel: BViewModel!
static func create(with viewModel: BViewModel) -> BViewController {
let view = BViewController()
view.viewModel = viewModel
return view
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
}
}
- ViewModel에 Dependencies가 있고 BDIContainer에서 해당 코드를 바로 참조하는 형태
// BViewModel.swift
protocol BInput {
}
protocol BOutput {
}
protocol BViewModel: BInput, BOutput {}
class DefaultBViewModel: BViewModel {
let dependencies: Dependencies
struct Dependencies {
let countNumber: Int
let someUseCase: SomeUseCase
}
init(dependencies: Dependencies) {
self.dependencies = dependencies
}
}
- BDIContainer: BViewModel의 Dependencies를 참조
// BDIContainer.swift
class BDIContainer {
func makeBViewController(countNumber: Int) -> BViewController {
let vc = BViewController.create(with: makeBViewModel(countNumber: countNumber))
return vc
}
func makeBViewModel(countNumber: Int) -> BViewModel {
return DefaultBViewModel(dependencies: .init(countNumber: countNumber,
someUseCase: SomeUseCase()))
}
}
* source code: 일차원적인 DI방법
필요한 곳에서 protocol에 선언하고 주입하는 쪽에서 구현하여 사용 방법
- BViewModel에 Protocol로 Dependencies 정의
// BViewModel.swift
protocol BInput {
}
protocol BOutput {
}
protocol BViewModel: BInput, BOutput {}
protocol BDependencies { // 추가
func getSomeUseCase() -> SomeUseCase
}
class DefaultBViewModel: BViewModel {
let dependencies: BDependencies // 추가
let countNumber: Int
struct Dependencies {
let someUseCase: SomeUseCase
}
init(countNumber: Int, dependencies: BDependencies) {
self.countNumber = countNumber
self.dependencies = dependencies
}
func executeSomeUsecase() { // usecase는 주입받은 usecase이용
let usecase = dependencies.getSomeUseCase()
print("execute usecase")
}
}
- BDIContainer에서 protocol을 구현하여 self로 주입
- 구현체가 BDIContainer가 되는 개념
// BDIContainer.swift
class BDIContainer {
func makeBViewController(countNumber: Int) -> BViewController {
let vc = BViewController.create(with: makeBViewModel(countNumber: countNumber))
return vc
}
func makeBViewModel(countNumber: Int) -> BViewModel {
return DefaultBViewModel(countNumber: countNumber, dependencies: self) // 추가: self로 주입
}
}
extension BDIContainer: BDependencies { // 추가
func getSomeUseCase() -> SomeUseCase {
return SomeUseCase()
}
}
* source code: 필요한 곳에서 protocol에 선언하고 주입하는 쪽에서 구현하여 사용 방법
테스트 관점에서 의미있는 코드인지 판단
- Test 시에 BDIContainer를 사용 > UseCase를 갈아 치울 수 없고 이미 extension으로 정의되어 있으므로 usecase를 갈아치우며 테스트하는 로직에 어려움이 존재
- UseCase는 ViewModel의 init에서 받는 구조가 되어야, 테스트 로직 시 usecase만 mock으로 만들고 viewMoel에 주입하여 테스트 하는 코드가 이상적