Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- swift documentation
- clean architecture
- Protocol
- Xcode
- swiftUI
- RxCocoa
- SWIFT
- Refactoring
- 애니메이션
- HIG
- Human interface guide
- combine
- uitableview
- 스위프트
- uiscrollview
- Clean Code
- 리펙터링
- MVVM
- 클린 코드
- tableView
- UICollectionView
- UITextView
- ribs
- rxswift
- map
- collectionview
- 리팩토링
- ios
- 리펙토링
- Observable
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[Clean Architecture] 2. 코드로 알아보는 SOLID - OCP(Open Close Principle) 개방 폐쇄 원칙 본문
Clean Architecture/Clean Architecture 코드
[Clean Architecture] 2. 코드로 알아보는 SOLID - OCP(Open Close Principle) 개방 폐쇄 원칙
jake-kim 2021. 9. 18. 00:520. 코드로 알아보는 SOLID - 클래스 다이어그램 필수 표현
1. 코드로 알아보는 SOLID - SRP(Single Responsibility Principle) 단일 책임 원칙
2. 코드로 알아보는 SOLID - OCP(Open Close Principle) 개방 폐쇄 원칙
3. 코드로 알아보는 SOLID - LSP(Liskov Substitution Principle) 리스코프 치환 원칙
4. 코드로 알아보는 SOLID - ISP(Interface Segregation Principle) 인터페이스 분리 원칙
5. 코드로 알아보는 SOLID - DIP(Dependency Inversion Principle, testable) 의존성 역전 원칙
6. 코드로 알아보는 SOLID - Coordinator 패턴 화면전환
7. 코드로 알아보는 SOLID - Network, REST (URLSession, URLRequest, URLSessionDataTask)
8. 코드로 알아보는 SOLID - 캐싱 Disk Cache (UserDefeaults, CoreData)
OCP
- Open Close Principle
- 개방 폐쇄 원칙
- 요구사항을 받았을 때 "확장에는 열려있어야 하고 변경에는 닫혀있어야 하는 원칙"
OCP를 준수하는 방법
- 우선순위가 높은 것은 다른것에 의존하지 않도록 컴포넌트 계층구조를 설계
- Interactor가 가장 우선순위가 높은 형태
코드에서의 OCP
- 실제 코드에서는 Interactor에서도 결국 financial 데이터에 관해서 주입 받아서 처리해야하는데, 이 때 결국 Database영역을 사용하는 꼴이 되므로, 참조는 Interface를 통해서 참조하도록 설계 (타입은 protocol이고 외부에서 주입해주는 것은 구현체를 주입)
예제) 사용자가 color버튼을 탭하는 횟수 계산하는 앱
- Actor의 요구사항: 한쪽 버튼이 다른쪽 버튼보다 2배 이상 많이 누른 경우, 작은쪽 버튼을 2배한 다음에 +1을 하라는 요구사항 존재
- 설계
- Interactor인 UseCase들이 가장 중요하므로, ColorUseCase는 그 누구에게도 의존하지 않는 상태
- Entity(business logic에서 바로 사용되는 모델)인 ColorModel 정의
struct ColorModel {
enum ColorType: String {
case blue = "blue"
case red = "red"
}
let color: ColorType
var currentBlueColor: Int = 0
var currentRedColor: Int = 0
}
- Keychain을 사용할 수 있는 KeychainRepository 정의
- KeychainAccess 프레임워크 사용
import KeychainAccess
public protocol KeychainRepository {
func save(_ key: String, _ value: String)
func get(_ key: String) -> String?
func delete(_ key: String)
func removeAll()
}
class KeychainRepositoryImpl: KeychainRepository {
public static let shared = KeychainRepositoryImpl()
private init() {}
let keychain = Keychain()
func save(_ key: String, _ value: String) {
do {
try keychain.set(value, key: key)
} catch {
print(error.localizedDescription)
return
}
}
func get(_ key: String) -> String? {
do {
guard let key = try keychain.get(key) else { return nil }
return key
} catch {
print(error.localizedDescription)
return nil
}
}
func delete(_ key: String) {
do {
try keychain.remove(key)
} catch {
print(error.localizedDescription)
}
}
func removeAll() {
do {
try keychain.removeAll()
} catch {
print(error.localizedDescription)
}
}
}
- KeychinService와 KeychainServiceImpl 정의
import KeychainAccess
fileprivate struct KeychainKey {
static let blueCount = "blueCount"
static let redCount = "redCount"
}
protocol KeychainService {
func saveBlueCount(_ count: Int)
func saveRedCount(_ count: Int)
func getBlueCount() -> Int?
func getRedCount() -> Int?
}
class KeychainServiceImpl: KeychainService {
private let keychainRepository: KeychainRepository
init(keychainRepository: KeychainRepository) {
self.keychainRepository = keychainRepository
}
// Save
public func saveBlueCount(_ count: Int) {
keychainRepository.save(KeychainKey.blueCount, String(count))
}
public func saveRedCount(_ count: Int) {
keychainRepository.save(KeychainKey.redCount, String(count))
}
// Get
public func getBlueCount() -> Int? {
return Int(keychainRepository.get(KeychainKey.blueCount) ?? "0")
}
public func getRedCount() -> Int? {
return Int(keychainRepository.get(KeychainKey.redCount) ?? "0")
}
}
- ColorUseCase와 ColorUseCaseImpl 정의
protocol ColorUseCase {
func updateColor(with: ColorModel, completion: (ColorModel) -> Void)
func getCurrentColorCount(completion: (ColorModel) -> Void)
}
final class ColorUseCaseImpl: ColorUseCase {
private let keychainService: KeychainService
init(keychainService: KeychainService) {
self.keychainService = keychainService
}
func updateColor(with colorModel: ColorModel, completion: (ColorModel) -> Void) {
var currentBlueCnt = keychainService.getBlueCount() ?? 0
var currentRedCnt = keychainService.getRedCount() ?? 0
// Business logic: 서로 두 배 이상 차이나면 적은 수를 *2 후 추가하는 로직
switch colorModel.color {
case .blue:
if currentBlueCnt * 2 < currentRedCnt {
currentBlueCnt = currentBlueCnt * 2 + 1
} else {
currentBlueCnt += 1
}
case .red:
if currentRedCnt * 2 < currentBlueCnt {
currentRedCnt = currentRedCnt * 2 + 1
} else {
currentRedCnt += 1
}
}
updateColor(currentBlueCnt, currentRedCnt)
let colorModel = ColorModel(color: colorModel.color,
currentBlueColor: currentBlueCnt,
currentRedColor: currentRedCnt)
completion(colorModel)
}
func getCurrentColorCount(completion: (ColorModel) -> Void) {
let currentBlueCnt = keychainService.getBlueCount() ?? 0
let currentRedCnt = keychainService.getRedCount() ?? 0
let colorModel = ColorModel(color: .blue,
currentBlueColor: currentBlueCnt,
currentRedColor: currentRedCnt)
completion(colorModel)
}
private func updateColor(_ blueColorCount: Int, _ redColorCount: Int) {
keychainService.saveBlueCount(blueColorCount)
keychainService.saveRedCount(redColorCount)
}
}
- ViewModel 정의
import RxSwift
import RxCocoa
protocol ColorViewModelInput {
func viewDidLoad()
func didTapBlueButton()
func didTapRedButton()
}
protocol ColorViewModelOutput {
var buttonInfo: BehaviorRelay<String> { get }
}
protocol ColorViewModel: ColorViewModelInput, ColorViewModelOutput {}
final class ColorViewModelImpl: ColorViewModel {
// Output
var buttonInfo: BehaviorRelay<String> = .init(value: "")
private var colorUseCase: ColorUseCase?
init(colorUseCase: ColorUseCase) {
self.colorUseCase = colorUseCase
}
// Input
func viewDidLoad() {
loadColorInfo()
}
func didTapBlueButton() {
colorUseCase?.updateColor(with: ColorModel(color: .blue), completion: { colorModel in
buttonInfo.accept("파란 버튼 누적 카운트 = \(colorModel.currentBlueColor),\n빨간 버튼 누적 카운트 = \(colorModel.currentRedColor)")
})
}
func didTapRedButton() {
colorUseCase?.updateColor(with: ColorModel(color: .red), completion: { colorModel in
buttonInfo.accept("파란 버튼 누적 카운트 = \(colorModel.currentBlueColor),\n빨간 버튼 누적 카운트 = \(colorModel.currentRedColor)")
})
}
// Private
private func loadColorInfo() {
colorUseCase?.getCurrentColorCount(completion: { colorModel in
buttonInfo.accept("파란 버튼 누적 카운트 = \(colorModel.currentBlueColor),\n빨간 버튼 누적 카운트 = \(colorModel.currentRedColor)")
})
}
}
- ColorViewController 정의
import SnapKit
import RxSwift
class ColorViewController: UIViewController {
private let disposeBag = DisposeBag()
var viewModel: ColorViewModel!
lazy var buttonContainerStackView: UIStackView = {
let view = UIStackView()
view.spacing = 16.0
return view
}()
lazy var leftButton: UIButton = {
let button = UIButton()
button.backgroundColor = .blue
button.setTitle("파란색 버튼", for: .normal)
return button
}()
lazy var rightButton: UIButton = {
let button = UIButton()
button.backgroundColor = .red
button.setTitle("빨간색 버튼", for: .normal)
return button
}()
lazy var infoLabel: UILabel = {
let label = UILabel()
label.textColor = .black
label.font = .systemFont(ofSize: 20)
label.numberOfLines = 0
return label
}()
init(viewModel: ColorViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init?(coder: NSCoder) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
addSubviews()
makeConstraints()
bindInput()
bindOutput()
}
private func setupViews() {
view.backgroundColor = .white
}
private func addSubviews() {
view.addSubview(buttonContainerStackView)
[leftButton, rightButton].forEach { buttonContainerStackView.addArrangedSubview($0) }
view.addSubview(infoLabel)
}
private func makeConstraints() {
buttonContainerStackView.snp.makeConstraints { maker in
maker.centerX.centerY.equalToSuperview()
}
infoLabel.snp.makeConstraints { maker in
maker.top.equalTo(buttonContainerStackView.snp.bottom).offset(16)
maker.centerX.equalTo(view.snp.centerX)
}
}
// Input
private func bindInput() {
leftButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.viewModel.didTapBlueButton()
}).disposed(by: disposeBag)
rightButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.viewModel.didTapRedButton()
}).disposed(by: disposeBag)
}
// Output
private func bindOutput() {
viewModel.buttonInfo.subscribe(onNext: { [weak self] in self?.updateInfoLabel(to: $0) }).disposed(by: disposeBag)
}
private func updateInfoLabel(to contents: String) {
infoLabel.text = contents
}
}
* 전체 소스 코드 `OCP` 그룹 참고: https://github.com/JK0369/SOLID
'Clean Architecture > Clean Architecture 코드' 카테고리의 다른 글
Comments