객체지향
객체지향적으로 리팩토링하며 이해해보자
리팩토링 전 코드
사용자가 상점에서 상품을 구매하는 코드이다.
사용자와 상점은 돈을 가지고 있고, 거래가 일어나면 돈을 주고 받는다.
상점 코드
import Foundation
class SoiStore {
var money: Int = 0
var name: String = "쏘이네 가게"
var products: [Int: [String: Any]]
init() {
self.money = 0
self.products = {
[
1: ["name": "키보드", "price": 30000],
2: ["name": "모니터", "price": 50000]
]
}()
}
func setMoney(_ money: Int) {
self.money = money
}
func setProducts(_ products: [Int: [String: Any]]) {
self.products = products
}
func getMoney() -> Int {
return self.money
}
func getProducts() -> [Int: [String: Any]] {
return self.products
}
}
손님 코드
import Foundation
class User {
var money: Int
var store = SoiStore()
var belongs: [Int: [String: Any]]
init() {
self.money = 0
self.belongs = [:]
}
func setMoney(_ money: Int) {
self.money = money
}
func setBelongs(belongs: [Int: [String: Any]]) {
self.belongs = belongs
}
func getMoney() -> Int {
self.money
}
func getBelongs() -> [Int: [String: Any]]{
self.belongs
}
func getStore() -> SoiStore {
self.store
}
func seeProduct(_ productID: Int) -> [String: Any]? {
let products = self.store.getProducts()
return products[productID]
}
func purchaseProduct(_ productID: Int) throws -> [String: Any] {
guard let product = self.seeProduct(productID) else {
throw NSError(domain: "상품이 없습니다", code: 0)
}
if self.money >= product["price"] as? Int ?? 0 {
self.store.products.removeValue(forKey: productID)
self.money -= product["price"] as? Int ?? 0
self.store.money += product["price"] as? Int ?? 0
self.belongs[productID] = product
return product
} else {
throw NSError(domain: "잔돈이 부족합니다", code: 0)
}
}
}
실행 코드
let user = User()
user.setMoney(100000)
do {
try user.purchaseProduct(1)
} catch {
print(error.localizedDescription)
}
print(user.belongs)
print(user.store.getProducts())
리팩토링 1 - 추상화
지금은 SoiStore하나만 사용하고 있다. 그런데 JiwonStore, AppleStore 등 상점이 여러개가 될 수 있다.
상점이 여러 개가 생긴다면, 추상화와 의존성 주입이 필요하다.
추상화
protocol Store {
var money: Int { get set }
var products: [Int: [String: Any]] { get set }
func setMoney(_ money: Int)
func setProducts(_ products: [Int: [String: Any]])
func getMoney() -> Int
func getProducts() -> [Int: [String: Any]]
}
상점 코드
변경사항 없으며, JiwonStore가 새로 생겼다. (형채는 SoiStore와 동일)
사용자 코드
class User {
var money: Int
var store: Store // 의존성 주입
var belongs: [Int: [String: Any]]
init(store: Store) {
self.money = 0
self.store = store
self.belongs = [:]
}
func setMoney(_ money: Int) {
self.money = money
}
func setBelongs(belongs: [Int: [String: Any]]) {
self.belongs = belongs
}
func getMoney() -> Int {
self.money
}
func getBelongs() -> [Int: [String: Any]]{
self.belongs
}
func getStore() -> Store {
self.store
}
func seeProduct(_ productID: Int) -> [String: Any]? {
let products = self.store.getProducts()
return products[productID]
}
func purchaseProduct(_ productID: Int) throws -> [String: Any] {
guard let product = self.seeProduct(productID) else {
throw NSError(domain: "상품이 없습니다", code: 0)
}
if self.money >= product["price"] as? Int ?? 0 {
self.store.products.removeValue(forKey: productID)
self.money -= product["price"] as? Int ?? 0
self.store.money += product["price"] as? Int ?? 0
self.belongs[productID] = product
return product
} else {
throw NSError(domain: "잔돈이 부족합니다", code: 0)
}
}
}
실행코드
let user = User(store: SoiStore())
user.setMoney(100000)
do {
try user.purchaseProduct(1)
} catch {
print(error.localizedDescription)
}
print(user.belongs)
print(user.store.getProducts())
let user2 = User(store: JiwonStore())
리팩토링 2 - 캡슐화
Store의 책임 재정의
Store는 사용자가 요구한 제품을 보여줄 수 있어야한다.
Store는 사용자에게 물건을 줘야한다.
Store는 사용자에게 돈을 받아야한다.
상점 프로토콜
// 추상화
protocol Store {
func showProduct(_ productID: Int) -> [String: Any]?
func giveProduct(_ productID: Int)
func takeMoney(_ money: Int)
}
상점 코드
class SoiStore: Store {
private var money: Int = 0
var name: String = "쏘이네 가게"
private var products: [Int: [String: Any]] // 의존성 주입
init(products: [Int: [String: Any]]) {
self.money = 0
self.products = products
}
func showProduct(_ productID: Int) -> [String : Any]? {
products[productID]
}
func giveProduct(_ productID: Int) {
if let _ = products[productID] {
products.removeValue(forKey: productID)
}
}
func takeMoney(_ money: Int) {
self.money += money
}
// Money, Product는 외부에서 수정할 수 없어야한다. -> private으로 선언하며, protocol에는 기재하지 않는다.
}
사용자 코드
class User {
private var money: Int
private var store: Store // 의존성 주입
private var belongs: [Int: [String: Any]]
init(money: Int, store: Store) {
self.money = money
self.store = store
self.belongs = [:]
}
func getStore() -> Store {
self.store
}
func seeProduct(_ productID: Int) -> [String: Any]? {
return store.showProduct(productID)
}
func purchaseProduct(_ productID: Int) throws {
guard let product = store.showProduct(productID) else {
throw NSError(domain: "재고가 없습니다.", code: 0)
}
guard let price = product["price"] as? Int, price <= self.money else {
throw NSError(domain: "돈이 부족합니다", code: 0)
}
self.money -= price
store.takeMoney(price)
store.giveProduct(productID)
self.belongs[productID] = product
}
func getBelongs() -> [Int: [String: Any]] {
self.belongs
}
}
실행코드
let user = User(money: 100000,
store: SoiStore(products: [
1: ["name": "키보드", "price": 30000],
2: ["name": "모니터", "price": 50000]
]))
do {
try user.purchaseProduct(1)
} catch {
print(error.localizedDescription)
}
print(user.getBelongs())
헷갈렸던 부분
프로토콜 : 객체의 기본 규약
setMoney라는 함수가 있다고 가정한다면 이것은 프로토콜에 추가해서는 안 된다.
객체의 기본 규약을 정의하는 것이기 때문에 당연히 적용되어야된다고 생각했었다. 하지만 프로토콜로 선언하면 private을 쓸 수 없다. 또한 외부 객체가 알 필요 없는 내부 로직이나 구현 방식에 대한 정보는 포함시키지 않는 것이 맞다. (캡슐화와 정보 은닉)
리팩토링 3 - 단일 책임
현재 User의 purchaseProduct에서 많은 행위를 책임지고 있다.
물건 판매 책임을 Store로 위임하여 결합도를 낮춰보자.
이에 상품을 판매하는 행위를 추상화하고, 구체적인 로직을 작성해야한다.
상점 프로토콜
// store의 역할은 물건을 보여주는 것과 판매하는 것이다.
protocol Store {
func showProduct(_ productID: Int) throws -> [String: Any]
func sellProduct(_ productID: Int, money: Int) throws
}
상점
class SoiStore: Store {
private var money: Int = 0
var name: String = "쏘이네 가게"
private var products: [Int: [String: Any]]
init(products: [Int: [String: Any]]) {
self.money = 0
self.products = products
}
// 물건을 보여주는 역할
func showProduct(_ productID: Int) throws -> [String : Any] {
guard let product = products[productID] else {
throw NSError(domain: "재고가 없습니다", code: 0)
}
return product
}
// 물건을 판매하는 역할
func sellProduct(_ productID: Int, money: Int) throws {
guard products[productID] != nil else {
throw NSError(domain: "재고가 없습니다.", code: 0)
}
takeMoney(money)
takeOutProduct(productID)
}
private func takeMoney(_ money: Int) {
self.money += money
}
private func takeOutProduct(_ productID: Int) {
products.removeValue(forKey: productID)
}
}
손님
class User {
private var money: Int
private var store: Store
private var belongs: [Int: [String: Any]]
init(money: Int, store: Store) {
self.money = money
self.store = store
self.belongs = [:]
}
func getStore() -> Store {
self.store
}
func getBelongs() -> [Int: [String: Any]] {
self.belongs
}
func seeProduct(_ productID: Int) -> [String: Any]? {
try? store.showProduct(productID)
}
// 판매의 역할은 store에서 처리, 구매만 user가 수행
func purchaseProduct(_ productID: Int) throws {
guard let product = try? store.showProduct(productID),
let price = product["price"] as? Int
else { return }
guard money >= price else {
throw NSError(domain: "잔액이 부족합니다", code: 0)
}
do {
giveMoney(price)
try store.sellProduct(productID, money: price)
addBelong(productID, product: product)
} catch {
takeMoney(price)
}
}
private func giveMoney(_ money: Int) {
self.money -= money
}
private func takeMoney(_ money: Int) {
self.money += money
}
private func addBelong(_ id: Int, product: [String: Any]) {
self.belongs[id] = product
}
}
리팩토링 4 - Product의 객체화
Product가 Dictionary로 선언되어있어서 복잡해보일 뿐만 아니라 추후 Product 역시 특정 책임을 갖게 될 수 있으므로 구조체 또는 클래스를 이용해야한다.
또한 함수명으로 코드를 이해할 수 있도록 money>=price 등을 함수로 만드는 것이 좋다.
프로토콜
protocol Store {
func showProduct(_ productID: Int) throws -> Product
func sellProduct(_ productID: Int, money: Int) throws
}
Product 구조체
struct Product {
let name: String
let price: Int
}
Store
class SoiStore: Store {
private var money: Int = 0
var name: String = "쏘이네 가게"
private var products: [Int: Product] // 의존성 주입
init(products: [Int: Product]) {
self.money = 0
self.products = products
}
func showProduct(_ productID: Int) throws -> Product {
guard let product = products[productID] else {
throw NSError(domain: "재고가 없습니다", code: 0)
}
return product
}
func sellProduct(_ productID: Int, money: Int) throws {
guard products[productID] != nil else {
throw NSError(domain: "재고가 없습니다.", code: 0)
}
takeMoney(money)
takeOutProduct(productID)
}
private func returnMoney(_ money: Int) {
self.money -= money
}
private func takeMoney(_ money: Int) {
self.money += money
}
private func takeOutProduct(_ productID: Int) {
products.removeValue(forKey: productID)
}
}
User
class User {
private var money: Int
private var store: Store
private var belongs: [Int: Product]
init(money: Int, store: Store) {
self.money = money
self.store = store
self.belongs = [:]
}
func getStore() -> Store {
self.store
}
func seeProduct(_ productID: Int) -> Product? {
try? store.showProduct(productID)
}
func purchaseProduct(_ productID: Int) throws {
guard let product = try? store.showProduct(productID)
else { return }
guard checkMoney(product.price) else {
throw NSError(domain: "잔액이 부족합니다", code: 0)
}
do {
giveMoney(product.price)
try store.sellProduct(productID, money: product.price)
addBelong(productID, product: product)
} catch {
takeMoney(product.price)
}
}
private func checkMoney(_ price: Int) -> Bool {
self.money >= price
}
private func giveMoney(_ money: Int) {
self.money -= money
}
private func takeMoney(_ money: Int) {
self.money += money
}
private func addBelong(_ id: Int, product: Product) {
self.belongs[id] = product
}
func getBelongs() -> [Int: Product] {
self.belongs
}
}
main
let user = User(money: 100000, store: SoiStore(products: [
1: Product(name: "키보드", price: 30000),
2: Product(name: "모니터", price: 100000)
]))
do {
try user.purchaseProduct(1)
} catch {
print(error.localizedDescription)
}
print(user.getBelongs())
Last updated