Notice
Recent Posts
Recent Comments
Link
관리 메뉴

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

[Refactoring] 9-4 조건부 로직 최소화 (조건부 로직을 다형성으로 바꾸기) 본문

Refactoring (리펙토링)

[Refactoring] 9-4 조건부 로직 최소화 (조건부 로직을 다형성으로 바꾸기)

jake-kim 2023. 6. 3. 01:51

조건부 로직을 다형성으로 바꾸기

  • 조건부 로직이 복잡해지면 수정하기가 굉장히 어려워지므로, 더 높은 수준의 개념인 다형성을 사용하여 조건들을 분리하는 방법
  • switch 문 안의 case가 여러개 있을 때 case별로 클래스를 하나씩 만들어서 공통 switch 로직의 중복을 제거하는 것이 목표
    • 특히 switch case로 나누어진 분기 부분들에서 조건부 로직을 자신만의 방식으로 처리하도록 구성할 때, 다형성을 사용하여 각 클래스에서 처리하도록하면 SRP(Single Responsibility Principle) 원칙도 지키는 효과

조건부 로직을 다형성으로 바꾸기

조건부 로직을 다형성으로 바꾸기 예시

  • 리펙토링 전) 
    • 사용자가 맥주를 고르고 이 맥주의 가격을 측정하는 프로그램
    • 맥주의 종류는 enum으로 정의된 상태
    • 각 맥주의 가격이 각각 다름
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
  • 현재 문제점
    • switch의 가장 단점인, 하나의 코드가 여러개의 case들을 책임지는 상태이므로 단일 책임 원칙(SRP)을 지키지 못하고 있음
    • 조건식 안에가 길어지고 있기 때문에 코드가 쉽게 읽히지 않는 상태
  • 다형성으로 리펙토링
    • 수퍼 클래스가 될 MyBeer 구현
    • MyBeer를 서브 클래싱하여 getPrice(), getTax()만 오버라이딩하고 사용하는쪽에서는 total()을 호출하여 원하는 값을 획득하도록 구현
class MyBeer {
    func getPrice() -> Int {
        0
    }
    func getTax() -> Int {
        0
    }
    func total() -> Int {
        getPrice() + getTax()
    }
}
  • Hoganda, Hite, Heineken모두 MyBeer를 서브클래싱하여 구현
class MyBeer {
    func getPrice() -> Int {
        0
    }
    func getTax() -> Int {
        0
    }
    func total() -> Int {
        getPrice() + getTax()
    }
}

class Hoganda: MyBeer {
    init?(beer: Beer) {
        guard beer == .hoganda else { return nil }
        print("this is hoganda")
    }
    
    override func getPrice() -> Int {
        1000
    }
    override func getTax() -> Int {
        300
    }
}

class Hite: MyBeer {
    init?(beer: Beer) {
        guard beer == .hite else { return nil }
        print("this is hite")
    }
    
    override func getPrice() -> Int {
        500
    }
}

class Heineken: MyBeer {
    private let premium: Bool
    
    init?(beer: Beer, premium: Bool) {
        guard beer == .heineken else { return nil }
        print("this is heineken")
        self.premium = premium
    }
    
    override func getPrice() -> Int {
        2000
    }
    override func getTax() -> Int {
        premium ? 1000 : 500
    }
}
  • switch문도 사용하는쪽에서는 상관하지 않아도 되므로, switch만 신경쓰는 컴포넌트를 생성
    • 생성자가 필요 없기 때문에 enum타입의 빌더를 정의
enum BeerBuilder {
    static func createBeer(_ beerType: Beer) -> MyBeer? {
        switch beerType {
        case .hoganda:
            return Hoganda(beer: beer)
        case .hite:
            return Hite(beer: beer)
        case .heineken:
            return Heineken(beer: beer)
        default:
            return nil
        }
    }
}
  • 사용하는쪽코드
    • 사용하는쪽에서는 switch문도 신경쓰지 않아도 되고 Builder에서 각각의 케이스 문에 대한 일을 MyBeer 서브클래싱에서 처리하기 때문에 코드를 수정할 때 개발자는 고려해야하는 범위를 줄여주는 장점이 있는 좋은 코드로 탄생
let result2 = BeerBuilder.createBeer(beer)?.total()
print("result:", result2) // 500

* 조건부 로직을 상속을 사용하여 리펙토링 했지만 더욱 리펙토링을 잘 하려면 상속보다는 Interface인 protocol을 사용하는게 더욱 적합한데, 이 글은 다음 포스팅 글, 조건부 로직을 protocol로 리펙토링하기에서 계속

 

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

* 참고

- Refactoring (Martin Flowler)

Comments