TDD (8~13장)

Young Min Sim ·2021년 6월 15일
0

TDD

목록 보기
3/6

8장 객체 만들기

8 ~ 11장의 궁극적인 목표: Dollar, Franc 중복 제거

저번 시간에 이어 times() 의 중복을 없애면 Dollar, Franc 클래스는 아무 일도 하지 않게 된다.
다만, 이 두 하위 클래스를 없애는 큰 단계를 한 번에 밟기보다는 우선 작은 단계부터 시작한다.


우선 하위 클래스(Dollar, Franc)에 대한 직접적인 참조를 줄이는 작은 작업을 먼저 한다.

좀 더 나아가서 Money.dollar 및 Money.franc 의 리턴 타입을 Money 로 올려 구체 타입에 대한 참조를 좀 더 없앤다.
이제 하위 클래스의 존재를 테스트 코드로부터 분리하여 어떤 모델 코드에도 영향을 주지 않고 상속 구조를 맘대로 변경할 수 있게 됐다.

다만, Money 에는 아직 times() 가 없으므로 10장에서는 이 메서드를 구현한다.

이 장에서 한 것

  • 팩토리 메서드를 도입하여 테스트 코드에서 구체 하위 클래스의 존재 사실을 분리해냈다.

9장 우리가 사는 시간(times)

10장에서 각 자식 클래스에 있는 times() 를 상위 클래스인 Money 로 끌어올리기 전,
어떻게 하면 불필요한 하위 클래스(Dollar, Franc)를 제거하여 times() 를 상위 클래스로 끌어올릴 수 있을까 생각할 필요가 있다.
-> 클래스 대신 통화 개념(USD, CHF)을 이용하여 구분


이렇게 한 단계씩 과정을 밟아 나가다 보면 조금 답답할 수 있다.
그럴 때는 보폭을 넓혀 조금 큰 단계를 한 번에 진행하는 것도 좋은 방법이다.
반대로 너무 넓은 것 같으면 조금씩 줄여가며 진행할 수 있다.

마찬가지로 크게 중요하지 않다고 생각되는 부분은 건너 뛰면서 진행하는 중입니다.

이 장에서 한 것

  • times() 의 중복을 제거하는 큰 작업을 한 번에 하기보다는, 그 전에 필요한 작은 작업(통화 개념 도입)을 수행했다.

10장 흥미로운 시간(times)

통화 개념(USD, CHF)를 통해 구분하기 때문에 더 이상 하위 클래스(Dollar, Franc)가 필요 없다

이 장에서는 times, ==(equals) 메서드 2개를 리팩토링합니다.

이 상태에서 테스트를 돌려보면 잘 통과되던 테스트의 맨 마지막 줄에서 실패가 발생한다.


이는 타입으로 비교했던 이전 방식 때문이다.
currency (통화 개념)을 추가했으므로 이를 이용하는 방식으로 변경한다.

Money 에서 times 를 nil 을 반환하는 스텁으로 구현했으므로 테스트가 실패한다.

times 를 정상적으로 구현해준다.

이 장에서 한 것

  • 도입한 통화개념(USD, CHF)을 이용하여 구체 하위 클래스(Dollar, Franc)를 모두 제거하고 테스트가 통과하도록 리팩토링했다.

11장 모든 악의 근원



하위 클래스 2개에 대해 각각 존재했던 테스트 코드인데,
현재는 하위 클래스가 모두 사라졌으므로 중복되는 테스트 코드로 판단하여 제거한다.

이 장에서 한 것

  • 필요 없는 테스트코드 정리

12장 드디어 더하기

할일 목록

$5 + 10 CHF = $10 (환율이 2:1인 경우)
$5 + $5 = $10

테스트 함수 testSimpleAddition()

Money 클래스

이전과 같았으면 우선 테스트 통과를 위한 가짜 구현, 즉 plus 스텁 메서드를 만들었겠지만
앞으로는 이렇게 구현이 복잡하지 않고 명확한 경우에는 바로 구현한다.
이를 통해 TDD를 하면서 어떻게 단계의 크기를 조절할 수 있는지 배울 수 있다.

다중 통화 연산을 어떻게 표현하는게 좋을까 ?

$5 + 5CHF 와 같은 다른 통화를 더하는 연산을 구현하는게 이 1부의 최종 목표다.
해법은 두 Money의 합(Sum)을 나타내는 객체를 만드는 것이다.

($2 + 3CHF) * 5 와 같은 수식이 있다면 2, 3을 나타내는 건 Money 가 된다.
그리고 연산의 결과로 Expression(인터페이스)들이 생기는데, 그 중 하나는 Sum(구현 클래스)이 될 것이다.
Sum과 같은 연산이 완료되면, 환율을 이용하여 결과를 단일 통화로 축약(reduced)할 수 있다.

이 개념을 테스트로 작성해본다. 마지막 줄부터 작성한다.
이 장에서 할 일은 $5 + $5 였으므로 여기에 맞춰 작성해준다.

func testSimpleAddition() {
    ...
    XCTAssertEqual(Money.dollar(10), reduced)
}

여기서 reduced는 Expression에 환율을 적용함으로써 얻어진다.

여기서 Expression 은 '수식' 정도로 보시면 될 것 같습니다.

그러면 실세계에서 환율을 적용하는 곳은? -> 은행이다.

func testSimpleAddition() {
	...
    let bank = Bank()
    let reduced = bank.reduce(sum, "USD")
    XCTAssertEqual(Money.dollar(10), reduced)
}

두 Money 의 합(Sum)Expression 이어야 한다.
아래와 같은 테스트코드가 완성된다.

func testSimpleAddition() {
    let five = Money.dollar(5)
    let sum: Expression = five.plus(five)
    let bank = Bank()
    let reduced = bank.reduce(sum, "USD")
    XCTAssertEqual(Money.dollar(10), reduced)
}

컴파일 및 테스트를 통과시킬 차례다.

Expression, Bank 추가

이때 Bank 의 reduce 는 테스트만 통과하도록 가짜 구현

Money 클래스의 plus 메서드 반환 타입 수정

이 장에서 한 것

  • 큰 테스트($5 + 10CHF) 전, 작은 테스트($5 + $5) 단계를 먼저 진행했다.

13장. 진짜로 만들기

할일 목록

$5 + 10 CHF = $10 (환율이 2:1인 경우)
$5 + $5 = $10
$5 + $5에서 Money 반환하기

앞장에서 말했던 대로 Money 의 합을 객체로 즉, Sum으로 표현한다.
Money 의 plus() 에서 Money 대신 Sum 을 리턴하도록 수정

환율을 담당하는 Bank 와 마찬가지로 Money 를 더하는 작업을 담당하는 책임을 분리하기 위해 Sum 을 추가한게 아닐까 싶네요.
아직까지는 과한게 아닌가? 라는 생각도 들지만 일단 끝까지 읽어봐야 할 것 같습니다.


Money 클래스

테스트가 정상적으로 통과된다.


이제 Bank.reduce() 는 Sum 을 전달 받으므로 이를 테스트할 필요가 있다.

Bank 가 Sum 의 상세 구현에 대해 너무 많이 알고 있음

위와 같이 Sum 의 상세 구현, 변수를 직접 참조하지 않고 reduce 를 통해 값을 받아올 수 있음



또한 Bank 의 reduce 메서드에 Money 를 인자로 넘겼을 때도 테스트할 필요가 있음

그리고 bank 의 reduce 도 수정

타입을 검사하는 코드 때문에 다소 지저분해진다.
이럴 때는 다형성을 사용하도록 바꾸는게 좋다.

지저분한 캐스팅, 클래스 검사 코드 등을 제거할 수 있다.

이 장에서 한 것

  • 앞으로 필요할 것으로 예상되는 객체(Sum)의 생성을 강요하기 위한 테스트를 작성했다.
  • 명시적인 클래스 검사를 제거하기 위해 다형성을 사용했다.
class testTests: XCTestCase {
    
    override func setUpWithError() throws {
    }

    override func tearDownWithError() throws {
    }
    
    func testMultiplication() {
        let five = Money.dollar(5)
        XCTAssertEqual(Money.dollar(10), five.times(2))
        XCTAssertEqual(Money.dollar(15), five.times(3))
    }
    
    func testEquality() {
        XCTAssertTrue(Money.dollar(5) == Money.dollar(5))
        XCTAssertFalse(Money.dollar(5) == Money.dollar(6))
        XCTAssertFalse(Money.dollar(5) == Money.franc(5))
    }
    
    func testCurrency() {
        XCTAssertEqual("USD", Money.dollar(1).currency)
        XCTAssertEqual("CHF", Money.franc(1).currency)
    }
  
    func testSimpleAddition() {
        let five = Money.dollar(5)
        let sum: Expression = five.plus(five)
        let bank = Bank()
        let reduced = bank.reduce(sum, "USD")
        XCTAssertEqual(Money.dollar(10), reduced)
    }
    
    func testPlusReturnsSum() {
        let five = Money.dollar(5)
        let result: Expression = five.plus(five)
        let sum = result as! Sum
        XCTAssertEqual(five, sum.augend)
        XCTAssertEqual(five, sum.addend)
    }
    
    func testReduceSum() {
        let sum: Expression = Sum(augend: Money.dollar(3), addend: Money.dollar(4))
        let bank = Bank()
        let result = bank.reduce(sum, "USD")
        XCTAssertEqual(Money.dollar(7), result)
    }
    
    func testReduceMoney() {
        let bank = Bank()
        let result = bank.reduce(Money.dollar(1), "USD")
        XCTAssertEqual(Money.dollar(1), result)
    }
}

class Bank {
    
    func reduce(_ source: Expression, _ to: String) -> Money {
        return source.reduce(to)
    }
}

protocol Expression {
    func reduce(_ to: String) -> Money
}

struct Sum: Expression {
    let augend: Money
    let addend: Money
    
    func reduce(_ to: String) -> Money {
        let amount = augend.amount + addend.amount
        return Money(amount, to)
    }
}

class Money: Equatable, Expression {
    fileprivate let amount: Int
    let currency: String
    
    init(_ amount: Int, _ currency: String) {
        self.amount = amount
        self.currency = currency
    }
    
    static func dollar(_ amount: Int) -> Money {
        return Money(amount, "USD")
    }
    
    static func franc(_ amount: Int) -> Money {
        return Money(amount, "CHF")
    }
    
    func times(_ multiplier: Int) -> Money {
        return Money(amount * multiplier, currency)
    }
    
    func plus(_ addend: Money) -> Expression {
        return Sum(augend: self, addend: addend)
    }
    
    func reduce(_ to: String) -> Money {
        return self
    }
    
    static func ==(lhs: Money, rhs: Money) -> Bool {
        return lhs.amount == rhs.amount && lhs.currency == rhs.currency
    }
}

0개의 댓글