관리 메뉴

김종권의 iOS 앱 개발 알아가기

[Refactoring] 9-5 조건부 로직 최소화 (조건부 로직을 protocol로 리펙토링하기) 본문

Refactoring (리펙토링)

[Refactoring] 9-5 조건부 로직 최소화 (조건부 로직을 protocol로 리펙토링하기)

jake-kim 2023. 6. 4. 01:40

조건부 로직을 protocol로 바꾸기

  • 이전 포스팅 글에서는 switch case 조건부 로직을 다형성으로 바꿨었는데, 상속을 사용하게되면 코드를 파악하기 어렵고 수정하기 어려운 반면에 protocol을 사용하면 이전 포스팅 글에서 알아본 대로 여러 이점이 존재 (아래 "Protocol 프로그래밍을 지향해야하는 이유"요약 참고)

Protocol 프로그래밍을 지향해야하는 이유

*protocol = 자바의 Interface

  • DIP (Dependency Inversion Principle): 소스코드 의존성이 구현체에 의존하지 않고 추상(protocol)에 의존하는 것
  • 기능제공(=확장성)
    • 확장성이라는 의미는 개발자가 코드를 작성할 때 매우 자연스럽게 사용이 가능
    • 사용하는쪽에서 매우 자연스러움 -> 프로토콜을 준수한다 -> 기능을 준수한다
  • protocol을 적절히 이용하면 코드를 사용하는쪽에서 관심사의 범위를 좁혀서 의존성을 낮추기가 가능 (ISP 원칙 - 이전 포스팅 글 참고)
  • 상속을 사용할때보다 더욱 변경에 유리한 코드

예시)

  • 이전 포스팅 글인 9-4 조건부 로직을 다형성으로 바꾸기에서 알아보았듯이 switch문의 조건문 후 로직에서 각각의 역할을 따로 정의하고 그 내부에서 역할을 수행하기 위해 별도의 class로 만들고, Builder를 따로 만들어서 리펙토링한 상태

(리펙토링 전)

  • 하나의 switch문에서 여러개의 case들에 대해서 책임지는 상태
enum Beer {
    case hoganda
    case hite
    case heineken
}

let beer = Beer.hite
var price = 0
var taxFee = 0
var premium = false

switch beer {
case .hoganda:
    print("this is hoganda")
    price = 1000
    taxFee = 300
case .hite:
    print("this is hite")
    price = 500
case .heineken:
    print("this is heineken")
    price = 2000
    if premium {
        taxFee = 1000
    } else {
        taxFee = 500
    }
}

let result = price + taxFee
print("result:", result) // 500

(위 코드를 클래스로 만들어서 리펙토링한 내용은 이전 포스팅글 참고)

  • protocol을 사용하여 리펙토링하면?
    • protocol 정의
    • Beearable 프로토콜을 준수하는 쪽에서는 price, taxFee, premium만 세팅해주면 되므로 아래와 같이 정의 후 default implementation으로 미리 정의
protocol Beerable {
    var price: Int { get }
    var taxFee: Int { get }
    var premium: Bool { get }
    var total: Int { get }
    
    init?(beer: Beer)
    init?(beer: Beer, premium: Bool)
}

extension Beerable {
    var premium: Bool { false }
    var total: Int { price + taxFee }
    
    init?(beer: Beer) { nil }
    init?(beer: Beer, premium: Bool) { nil }
}
  • 위 프로토콜을 준수하는 hoganda, hite, heineken 모두 정의
struct Hoganda: Beerable {
    let price = 1000
    let taxFee = 300
    
    init?(beer: Beer) {
        guard beer == .hoganda else { return nil }
        print("this is Hoganda")
    }
}

struct Hite: Beerable {
    let price = 500
    let taxFee = 0
    
    init?(beer: Beer) {
        guard beer == .hite else { return nil }
        print("this is Hite")
    }
}

struct Heineken: Beerable {
    let price = 2000
    let premium: Bool
    var taxFee: Int {
        premium ? 1000 : 500
    }
    
    init?(beer: Beer, premium: Bool) {
        guard beer == .heineken else { return nil }
        self.premium = premium
        print("this is Heineken")
    }
}
  • builder를 만들어서 사용하면 완료
enum BeerBuilder {
    static func createBeer(beer: Beer, premium: Bool = false) -> Beerable? {
        switch beer {
        case .hoganda:
            return Hoganda(beer: beer)
        case .hite:
            return Hite(beer: beer)
        case .heineken:
            return Heineken(beer: beer, premium: premium)
        default:
            return nil
        }
    }
}

let resultBeer = BeerBuilder.createBeer(beer: beer, premium: premium)
print(resultBeer?.total) // 500

결과

  • 사용하는쪽에서 구현체에 의존하지 않고 인터페이스에 의존하는 형태 (DIP, testable)
  • switch문 안에서 처리하는 복잡한 처리 로직들이 각각의 프로토콜 안에서 처리되어 나중에 특정 로직을 수정할 때 개발자가 확인해야할 범위를 줄여주기 때문에 수정하기 쉬움

* 전체 코드: https://github.com/JK0369/ExRefactoring9_5

 

 

Comments