TDD - 머니객체
package money;
public class Dollar {
public Dollar(int amount) {
super();
// TODO Auto-generated constructor stub
}
}
TDD
테스트 주도 개발을 잘하려면 훈련과 경험이 필요
수 - 규칙을 충실히 지키는 것
파 - 규칙을 깨고 자기만의 방식을 찾는 것
리 - 규칙이나 형에서 자유로워 지는 것이다.
수
- 간단한 쉬운 문제들은 TDD를 시도 , 전에 프로그래밍 해본 문제가 좋다.
- 초록 막대주기 : 처음 실행된 코드에서 다음코드까지의 시간
가능한 빨리 오류를 없애야됨오류의 주기를 정해놓고 그 이상의 시간이 지나면 새로 짜는 것이좋음
- 가짜로 구현하기를 적극사용하자
- 같은 문제를 여러번 풀어보자
- 초기에 리펙토링 툴을 사용하지 않는 것이 좋다.
파
- 여유를 가지자 학습과 개선의 필수적 요소는 자기를 돌아보기와 자기가 생각하는 것을 생각하는 메타인식이다.
자신이 하는 것을 관조 관찰 하고 기록하고 분석하자
프로토콜 분석 (생각한 것을 직접 말하도록 하면서 진행하고 이를 녹음한 것을 분석하는 것을 말한다.)
도 괜찮다.
- TDD로 해보지 않은 영역을 실험해 본다.
- 보폭을 조절 잘된다 싶으면 속도를 높이 고 안풀리다 싶으면 속도를 늦추는 훈련을 한다.
- 작은 애플리케이션 하나를 오로지 TDD로 완성해본다 일주일 걸리는 것이 좋다.
- 다른언어(다른 패러다임의 언어)로 TDD를 해본다 . 이전 풀어봤던 문제를 시도한다.
전혀 다른 해를 얻지 못하면 TDD에 문제가 있는 것
리
TDD를 사용하지 않고 같은 문제에 접근해보기 안써서 좋을때 있는가?
- TDD와 초기 설계를 섞는다 . 어떤방식의 디자인이 좋은가
- CRC카드(인덱스카드에 클래스, 책입, 협동대상을 기입)는 어떤가?
TDD로 달라진것을 확인하고 기록한다.
- TDD와 다른 방법을 시도한다. 의도에 의한 프로그래밍 , 단계적 계선 , 도메인 주도 설계
TDD에 수련에 도움이 되는 문제가
- ICPC 문제 등 알고리즘 중심의 문제
- 자동판매기 테스터(인터렉티브 셸(shell) 포함)
- 계산기(인터렉티브 셸 포함)
- 엘리베이터 시뮬리에션(Direct Event Simulation)
- 웹게시판
- 멀티 채팅 프로그램
테스트 주도 개발에서는
- 오직 자동화된 테스트가 실패할 경우에만 새로운 코드를 작성한다.
- 중복을 제거한다.
2가지 규칙을 따른다.
그러나 단순한 규칙이 가지는 기술적인 함의로 인해서 개인이나 집단 차원에서 복잡한 행동패턴이 만들어 진다.
- 매 결정사항에 대해 피드백을 제공하는 실행 가능한 코드를 기반으로 하는 유기적인 설계를 해야한다.
- 자동화된 테스트를 다른 사람이 만들어주길 기다릴수 없으므로 직접작성
- 개발환경은 작은변화에 빠르게 반응할 수 있어야한다.
- 테스트를 쉽게 만들려면 응집도는 높고 결합도는 낮은 컴포넌트들로 구성되게끔 설계해야한다.
1. 빨강 - 실패하는 작은 테스트작성 . 컴파일조차 되지 않을가능성 있음
2. 초록 - 빨리 테스트가 통과하게끔 만든다. 죄악을 저질러도됨(카피엔 페이스트, 특정상수를 반환하도록 구현하기)
3. 리팩토링 - 일단 테스트를 통과하게만 하는 와중에 생겨난 모든 중복을 제거한다.
- 결함 밀도를 충분히 감소 시킰 있다면, 품직보증을 수동적인 작업에서 능동적인 작업으로 전환할 수있다.
- 고약한 예외 상황의 숫자를 충분히 낮출 수있다면, 프로젝트 매니저가 정확히 추정할 수있다 고객을 매일 개발과정에 참여가능하다.
- 기술적 대화의 주제가 충분히 분명해 질 수있다면, 소프트웨어 엔지니어들은 일일 단위 혹은 주단위 협력대신에 분단위로 협력하면서 일 할 수있다.
- 결합 밀도가 낮아지면 새기능에 선적가능한 소프트웨어를 매일 갖게 되고 고객과 새로운 비즈니스 관계에 이를 수 있다.
용기
- 두려움은 여러분을 망설이게 만든다.
- 두려움은 여러분이 커뮤니케이션을 덜 하게 만든다.
- 두려움은 여러분이 피드백 받는 것을 피하도혹 만든다.
- 두려움은 여러분을 까다롭게 만든다.
이 중 어떠한 것도 프로그래밍에 도움이 되지 않는다.
- 불확신 상태로 있는 대신, 가능하면 재빨리 구체적인 학습을 하기 시작한다.
- 침묵을 지키는 대신, 좀더 분명하게 커뮤니케이션한다.
- 피드백을 회피하는 대신, 도움이 되고 구체적인 피드백ㅇ들 찾는다.
- (자신의 나쁜성깔을 직접 해결해야 한다.)
프로그래미을 우물에서 한 두레박의 물을 길어 올리기 위해 크랭크 같은것
TDD는 피드백사이의 간격을 인지하고, 또한 이 간격을 통제할 수있게 해주는 기술을 말한다.
"일주일간 종이에다 설계한 다음 코드를 테스트 주도로 개발한다면 이것도 TDD인가?"
결정과 그에 대한 피드백사이의 간격을 인지하고 의식적으로 제어했기때문에
미묘한 동시성 문제들을 코드실행으로 확실히 제현을 불가능
이 책을 끝까지 읽고 나면
- 단순하게 시작한고
- 자동화된 테스트를 만들고
- 새로운 셜계를 한번에 하나씩 도입하기 위해 리팩토링을 할 준비가 된것이다.
화폐 예제
TDD의 리듬
1. 재빨리 테스트를 하나 추가한다.
2. 모든 테스트를 실행하고 새로 추가한 것이 실패하는 지 확인한다.
3. 코드를 조금 바꾼다.
4. 리팩토링을 통해 중복을 제거한다.
놀라게 될 것
- 각각의 테스트가 기능의 작은 증가분을 어떻게 커버하는지
- 새 테스트를 돌아가게 하기 위해 얼마나 작고 못생긴 변화가 가능한지
- 얼마나 자주 테스트를 실행하는지
- 얼마나 수없이 작은 단계를 통해 리팩토링이 되어가는지
package money;
import org.junit.Test;
public class testMultiplication {
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10,five.amount);
}
}
이 코드에서는 4개의 컴파일에러가 존재한다.
1. Dollar 클래스가 없음
2. 생성자가 없음
3. times(int) 메서드가 없음
4. amount 필드가 없음
일단 컴파일에러를 없애자
package money;
public class Dollar {
public Dollar(int amount) {
super();
// TODO Auto-generated constructor stub
}
}
times() 스텁구현(테스트 코드가 컴파일 될수 있도록 껍데기만 만들어 두는 것)이 필요 , 컴파일만 되게 최소한의 구현만 하자
package money;
public class Dollar {
int amount=10;
public Dollar(int amount) {
super();
// TODO Auto-generated constructor stub
}
void times(int multiplier) {
}
}
@Test 를 추가 안하면 생기는 오류
java.lang.Exception: No tests found matching [{ExactMatcher:fDisplayName=testMultiplication], {ExactMatcher:fDisplayName=testMultiplication(money.testMultiplication)], {LeadingIdentifierMatcher:fClassName=money.testMultiplication,fLeadingIdentifier=testMultiplication]] from org.junit.internal.requests.ClassRequest@31cefde0 at org.junit.internal.requests.FilterRequest.getRunner(FilterRequest.java:40) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createFilteredTest(JUnit4TestLoader.java:80) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createTest(JUnit4TestLoader.java:71) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.loadTests(JUnit4TestLoader.java:46) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:522) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)
그냥 맞는 상수를 넣어서 테스트만 통과하게 만들었다.
package money;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class testMultiplication {
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10,five.amount);
}
}
계속진행하기 전에 일반화
1. 작은 테스트를 추가
2. 모든 테스트를 실행해서 테스트가 실패하는 것을 확인
3. 조금 수정한다.
4. 모든 테스트를 실행해서 테스트가 성공하는 것을 확인
5. 중복 제거하기 위해 리팩토링한다 -> 코드의 외적행위는 그대로 유지하면서 내부 구조를 변경하는 작업을 의미 자세한 내용은 마틴파울러의 리펙토링 참고
package money;
public class Dollar {
int amount;
public Dollar(int amount) {
super();
// TODO Auto-generated constructor stub
}
void times(int multiplier) {
amount = 5*2;
}
}
상수로 넣었던 걸 실제 메소드안으로 연산을 이동시킴
2의 값은 사실상 넘어오는 것이므로 중복된다 중복을 제거한다.
5의 값은 생성자에서 넘어오는 값이다 중복된다. 중복을 제거한다.
package money;
public class Dollar {
int amount;
public Dollar(int amount) {
this.amount = amount;
}
void times(int multiplier) {
amount =amount * multiplier;
}
}
지금까지 한 작업을 검토
- 우리가 알고 있는 작업해야 할 테스트 목록을 만들었다.
- 오퍼레이션이 외부에서 어떻게 보이길 원하는지 말해주는 이야기를 코드로 표현
- Junit의 상세한 사항은 잠시 무시
- 스텁 구현을 통해 테스트 파일을 컴파일
- 끔찍한 죄악으로 테스트를 통과 시킴
- 돌아가는 코드에서 상수를 변수로 변경하여 점진적으로 일반화
- 새로 할일들을 한번에 처리하는 대신 할일 목록에 추가하고 넘어 갔다.
타락한 객체
1. 테스트를 작성한다. -> 오퍼레이션이 코드에 어떤 식으로 나타나길 원하는지 생각하자.
- 원하는 인터페이스를 개발하라 . 올바른 답을 얻기 위해 필요한이야기의 모든 요소를 포함시켜라
2. 실행가능하게 만든다. 다른 무엇보다도 중요한 것은 빨리 초록 막대를 보는 것이다.
- 만약 깔끔하고 단순한 해법이 있지만 구현하는데 몇분정도 걸릴것 같으면 일단 적어 놓은 뒤에 원래 문제 (초록막대)로 돌아오자.
3. 올바르게 만든다 . 이제 시스템이 작동하므로 직전에 저질렀던 죄악을 수습하자. 좁고 올곧은 소프트웨어 정의의 길로 되돌아와서 중복을 제거하고 초록 막대로 되돌리자.
목적은 깔끔한 코드를 얻는것
그러나 달성하기 힘든 목표
나누어서 정복 -> '작동하는 깔끔한 코드'를 얻어야함
=일단 작동하는 에 해당하는 코드를 작성
그 이후에 깔끔한 코드 부분을 해결
배운 것들을 설계에 반영하드라 허둥거리는 아키텍쳐 주도 개발과 정반대
테스트를 통과 했지만 이상함
Dollar 에대해 연산을 수행후 Dollar 값이 바뀌는 것
package money;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class testMultiplication {
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);
five.times(2);
assertEquals(10,five.amount);
five.times(3);
assertEquals(15, five.amount);
}
}
times( )을 처음 호출한 이후에는 더 이상 5가 아니다.
times() 에서 새로운 객체를 반환하게 만들자
package money;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class testMultiplication {
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);
Dollar product = five.times(2);
assertEquals(10,product.amount);
product=five.times(3);
assertEquals(15, product.amount);
}
}
Dollar.time( )를 아래와 같이 수정하기 전엔 새 테스트는 컴파일 조차 되지 않을 것이다.
package money;
public class Dollar {
int amount;
public Dollar(int amount) {
this.amount = amount;
}
Dollar times(int multiplier) {
//아예 연산한 값을 넣은 객체를 생성함
return new Dollar(amount*multiplier);
}
}
빨리 초록색을 보기 위해 취할 수 있는 전략 2가지
1. 가짜로 구현하기 : 상수를 반환하게 만들고 진짜 코드를 얻을 때 까지 단계적으로 상수를 변수로 변화 시켜 간다.
2. 명백한 구현 사용하기 : 실제 구현을 입력한다.
잘 진행 되고 뭘 입력해야될지 알 때는 2번
예상치 못한 빨간 막대기를 만나면 가짜로 구현한방법을 리팩토링 하면서 다시 자신감을 가지면 명백한 구현 사용하기 모드로 돌아온다 .
- 설계상의 결함(Dollar 부작용)을 그 결함으로 실패하는 테스트로 변환
- 스텁 구현으로 빠르게 컴파일을 통과하게 만들었다.
- 올바르다고 생각하느 코드를 입력하여 테스트를 통과.
3. 모두를 위한 평등
지금 Dollar와 같이 객체를 값처럼 쓸수 있는 것을 객체 패턴이라고 한다.
값 객체에 대한 제약사항 중 하나는
객체의 인스턴스 변수가 생성자를 통해서 일단 설정된 후에는 결코 변하지 않는다는 것이다.
값 객체를 설정하면 별칭 문제에 대해 걱정할 필요가 없다는 장점이 존재한다.
똑같이 5$인 객체가 2개 있다고 치면
버그로 2번째 객체의 값을 변화 시키는 바람에 첫번째 값까지 변하게 되는 문제가 있는데 이 것이 별칭 문제이다.
값 객체를 사용하면 별칭에 대해 걱정할 필요가 없다 .
누군가 7$원한다면 새로운 객체를 만들어야 할 것이다.
값객체가 암시하는 것중 하나는 2장에서 같이 모든 연산을 새객체를 반환해야하는 것과
값 객체의 equals를 구현해 한다는 것이다 . (내용(이거나 어떤 기준)을 비교할 수있는 메소드를 만들어야 됨)
Dollar를 해시테이블의 키로 쓸 생각이면 equals( )를 구현 할때 hashCode( )를 같이 구현해야한다.
이것을 할일 목록에 적어놓고 문제가 생기면 그때 만지자
할일 목록
$5 + 10CHF = $10(환율이 2:1인경우) - > 정상적 동작 하는 기능의 목표
$5 * 2 = $10 -> 죄악으로 구현하는 코드를 만드는 것 (구현)
amount를 private으로 만들기
Dollar 부작용 ? ->어떤 값으로 새로운 객체를 생성하는 패턴으로 만들어서 별칭 문제를 해결
Money 반올림?
equals( )
hashCode( )
일단 이런식으로 가짜로 구현
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
}
@Override
public boolean equals(Object obj) {
return true;
}
삼각 측량 전략이 있는데
이를 사용하려면 예제가 2개 이상 필요하다.
테스트코드
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
}
equals() 메소드
@Override
public boolean equals(Object obj) {
Dollar dollar =(Dollar)obj;
return amount == dollar.amount;
}
일반적으로 해법이 존재하면 그대로 사용하지만
해법이 떠오르지 않을 때 삼각 측량을 사용한다.
- 맞아야하는 경우 틀려야하는 경우를 둘다 만족하는 equals() 구현
그러나 null이나 다른 객체들과 비교하면 어떻게 될까?
할일 목록에 적어두고 나중에 해결
할일 목록
$5 + 10CHF = $10(환율이 2:1인경우) - > 정상적 동작 하는 기능의 목표
$5 * 2 = $10 -> 죄악으로 구현하는 코드를 만드는 것 (구현)
amount를 private으로 만들기
Dollar 부작용 ? ->어떤 값으로 새로운 객체를 생성하는 패턴으로 만들어서 별칭 문제를 해결
Money 반올림?
equals( )
hashCode( )
Eqaul null
Eqaul object
동질성을 구현해서 Dollar 끼리 비교 가능하다 .
amount를 private로 만들 수 있게 되었음.
위 내용을 검토하면
- 우리의 디자인 패턴 (값 객체) 하나의 또 다른 오퍼레이션을 암시한다는 것을 알아 내었다.
- 해당 오퍼레이션을 테스트 하였다.
- 해당 오퍼레이션을 간단히 구현 했다.
- 곧장 리팩토리 대신에 테스트를 조금 더 했다.
- 두 경우를 모두 수용할 수 있도록 리팩토링 했다.
4. 프라이버시
동치성 문제가 정의햇으므로
테스트가 더 많은 이야기를 해줄 수 있도록 만들자 .
개념적으로 Dollar.times() 연산은 호출을 받은 객체의 값에 인자로 받은 곱수 만큼 곰한 값을 잦는 Dollar를 반환 해야한다.
하지만 테스트다 정확히 그것을 말하지는 않는다.
package money;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.omg.CORBA.DATA_CONVERSION;
public class testMultiplication {
@Test
public void testMultiplication() {
Dollar five = new Dollar(5);
assertEquals(new Dollar(10),five.times(2));//10달러를 가진 객체와 , 5*2객체 가 같냐
assertEquals(new Dollar(15), five.times(3));//15와 15는 같냐
}
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
}
}
이 테스트는 일련의 오퍼레이션이 아니라 참인 명제의 단언이므로 의도가 더 명확하다.
Dollar의 amount 멤버변수를 사용하는 코드는 Dallar 자신밖에 없으므로 변수를 private로 변경가능
할일 목록
$5 + 10CHF = $10(환율이 2:1인경우) - > 정상적 동작 하는 기능의 목표
$5 * 2 = $10 -> 죄악으로 구현하는 코드를 만드는 것 (구현)
amount를 private으로 만들기
Dollar 부작용 ? ->어떤 값으로 새로운 객체를 생성하는 패턴으로 만들어서 별칭 문제를 해결
Money 반올림?
equals( )
hashCode( )
Eqaul null
Eqaul object
동치성에 대한 코드가 정확히 검증되는것에 실패한다면
곱하기 테스트역시 코드가 정확하게 작동한다는 것에대한 것에 실패할 것이다.
이것은 TDD를 하면서 적극적으로 관리해야할 위험 요소다.
자신감을 가지고 전진할 수 있을 만큼만 결함의 정도를 낮추기를 희망할 뿐이다.
때때로 우리의 추론이 맞지 않아서 결함이 손가락사이로 빠져나가는 수가 있다.
그럴 때면 테스트를 어떻게 작성했느닞 교훈을 얻고 다시 앞으로 나아간다 .
그 이후에는 요감하게 펄럭이는 초록 막대 아래서 대담하게 앞으로 나아 갈 수 있다.
- 오직 테스트를 향상 시키기 위해서만 개발된 기능을 사용
- 두테스트가 동시에 실패하면 망한다.
- 위험 요소가 있음에도 계속 진행
- 테스트와 코드사이에 결합도를 낮추기위해 , 테스트 하는 객체의 새 기능을 사용
5장
1. 테스트 작성
2. 컴파일 되게 하기.
3. 실패하는지 확인하기 위해 실행.
4. 실행하게 만듦.
5. 중복 제거
처음 4단계는 빨리 진행
Fran이라는 클래스를 만듬
package money;
public class Franc {
private int amount;
public Franc(int amount) {
this.amount = amount;
}
Franc times(int multiplier) {
return new Franc(amount*multiplier);
}
@Override
public boolean equals(Object obj) {
Franc dollar =(Franc)obj;
return amount == dollar.amount;
}
}
코드 실행 시키기 단계 까지는 빨리 진행 가능하다 .
할일 목록
$5 + 10CHF = $10(환율이 2:1인경우) - > 정상적 동작 하는 기능의 목표
$5 * 2 = $10 -> 죄악으로 구현하는 코드를 만드는 것 (구현)
amount를 private으로 만들기
Dollar 부작용 ? ->어떤 값으로 새로운 객체를 생성하는 패턴으로 만들어서 별칭 문제를 해결
Money 반올림?
equals( )
hashCode( )
Eqaul null
Eqaul object
5CHF * 2 = 10CHF
Dollar/Franc 중복
공용 equals
공용 times
중복이 많기 때문에 다음 테스트를 작성하기 전까지
제거해야한다.
equals를 일반화부터 시작
- 큰 테스트를 바로 공략은 불가능 진전을 나타낼수 있는 자그만한 테스트를 만듬
- 중복을 만드고 조금 고쳐서 테스트를 작성
- 설상가상으로 모델 코드ㄷ까지 복사하고 수정해서 테스트를 통과
- 중복을 없앨때까지 집에 안가겠다고 약속
6장
두 클래스의 공통 상위 클래스
Dallar -----Franc
Money ----Dollar
L--- Franc
package money;
public class Money {
protected int amount;
}
package money;
public class Franc extends Money {
public Franc(int amount) {
this.amount = amount;
}
Franc times(int multiplier) {
return new Franc(amount*multiplier);
}
@Override
public boolean equals(Object obj) {
Franc dollar =(Franc)obj;
return amount == dollar.amount;
}
}
package money;
public class Dollar extends Money{
public Dollar(int amount) {
this.amount = amount;
}
Dollar times(int multiplier) {
//아예 연산한 값을 넣은 객체를 생성함
return new Dollar(amount*multiplier);
}
@Override
public boolean equals(Object obj) {
Dollar dollar =(Dollar)obj;
return amount == dollar.amount;
}
}
equals도 중복 되므로 위로 올려야 한다.
@Override
public boolean equals(Object obj) {
Money dollar =(Dollar)obj;
return amount == dollar.amount;
}
좀더 원활한 의사소통을 위해 변수이름을 변경
@Override
public boolean equals(Object obj) {
Money money =(Money)obj;
return amount == money.amount;
}
이제 Dollar에서 이 메서드를 Money로 옮긴다
Franc.equals를 제거해야한다 ,
Franc에 관해서 적절한 테스트가 없다.
있으면 좋을 거같은 테스트를 작성해라
리팩토링하기 전에 테스트를 작성하자 , Dollar 테스트를 복사하자 .
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
assertTrue(new Franc(5).equals(new Franc(5)));
assertFalse(new Franc(5).equals(new Franc(6)));
}
그리고 아까와 같이 Franc도 수정
할일 목록
$5 + 10CHF = $10(환율이 2:1인경우) - > 정상적 동작 하는 기능의 목표
$5 * 2 = $10 -> 죄악으로 구현하는 코드를 만드는 것 (구현)
amount를 private으로 만들기
Dollar 부작용 ? ->어떤 값으로 새로운 객체를 생성하는 패턴으로 만들어서 별칭 문제를 해결
Money 반올림?
equals( )
hashCode( )
Eqaul null
Eqaul object
5CHF * 2 = 10CHF
Dollar/Franc 중복
공용 equals
공용 times
검토
- 공통된 코드를 첫 번째 클래스에서 상위 클래스로 단계적으로 옮김
- 두번째 클래스도 Money의 하위클래스로 만듬
- 불필요한 구현을 제거 하기 전에 equals() 구현을 일치시켰음
7장 사과와 오렌지 (서로 다른걸 비교할 수 없다.)
할일 목록
$5 + 10CHF = $10(환율이 2:1인경우) - > 정상적 동작 하는 기능의 목표
$5 * 2 = $10 -> 죄악으로 구현하는 코드를 만드는 것 (구현)
amount를 private으로 만들기
Dollar 부작용 ? ->어떤 값으로 새로운 객체를 생성하는 패턴으로 만들어서 별칭 문제를 해결
Money 반올림?
equals( )
hashCode( )
Eqaul null
Eqaul object
5CHF * 2 = 10CHF
Dollar/Franc 중복
공용 equals
공용 times
Franc 과 Dollar 비교하기
@Test
public void testEquality() {
assertTrue(new Dollar(5).equals(new Dollar(5)));
assertFalse(new Dollar(5).equals(new Dollar(6)));
assertTrue(new Franc(5).equals(new Franc(5)));
assertFalse(new Franc(5).equals(new Franc(6)));
assertFalse(new Franc(5).equals(new Dollar(5)));
}
이 테스트 코드는 실패한다.
동치성 코드에서 Dollar가 Franc과 비교되지 않는지 검사해야한다.
오직 금액과 클래스가 서로 동일 할 때 만 두 Money가 같은 것이다.
public boolean equals(Object obj) {
Money money =(Money)obj;
return amount == money.amount && getClass().equals(money.getClass());
}
이제 공통코드 time를 처리해야할 때다 .
이번 장의 성과
- 우릴 괴롭히던 결함을 끄집어 내서 테스트에 담아냈다.
- 완벽하지 않지만 그럭저럭 봐줄 만한 방법 (getClass())으로 테스트를 통과하게 만듬
- 많은 설계를 도입하지 않기로 함
8장 객체 만들기
times의 두 코드가 거의 같다 .
반환을 Money로 만들면 더 비슷해진다.
Money times(int multiplier) {
//아예 연산한 값을 넣은 객체를 생성함
return new Dollar(amount*multiplier);
}
Money times(int multiplier) {
return new Franc(amount*multiplier);
}
}
Money 팩토리 메서드를 도입하자
@Test
public void testMultiplication() {
Dollar five = Money.dollar(5);
assertEquals(new Dollar(10),five.times(2));//10달러를 가진 객체와 , 5*2객체 가 같냐
assertEquals(new Dollar(15), five.times(3));//15와 15는 같냐
}
구현 코드는 Dollar를 생성하여 반환한다.
Money
static Dollar dollar (int amount) {
return new Dollar(amount);
}
Dollar에 대한 참조가 사라지므로 테스트 선언부를 수정
@Test
public void testMultiplication() {
Money five = Money.dollar(5);
assertEquals(new Dollar(10),five.times(2));//10달러를 가진 객체와 , 5*2객체 가 같냐
assertEquals(new Dollar(15), five.times(3));//15와 15는 같냐
}
Money 객체는 times가 없으므로 추상클래스로 변경후에 Money.times()를 선언하자
package money;
public abstract class Dollar extends Money{
abstract Money times(int multiplier);
public Dollar(int amount) {
this.amount = amount;
}
}
이제 팩토리 매서드의 선언을 바꿀수 있다.
package money;
public abstract class Money {
protected int amount;
abstract Money times(int muliplier);
public boolean equals(Object obj) {
Money money =(Money)obj;
return amount == money.amount && getClass().equals(money.getClass());
}
static Money dollar (int amount) {
return new Dollar(amount);
}
}
나머지 테스트코드에 사용 가능하다 .
package money;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.omg.CORBA.DATA_CONVERSION;
public class testMultiplication {
@Test
public void testMultiplication() {
Money five = Money.dollar(5);
assertEquals(Money.dollar(10),five.times(2));//10달러를 가진 객체와 , 5*2객체 가 같냐
assertEquals(Money.dollar(15), five.times(3));//15와 15는 같냐
}
@Test
public void testEquality() {
assertTrue(Money.dollar(5).equals(Money.dollar(5)));
assertFalse(Money.dollar(5).equals(Money.dollar(6)));
assertTrue(new Franc(5).equals(new Franc(5)));
assertFalse(new Franc(5).equals(new Franc(6)));
assertFalse(new Franc(5).equals(Money.dollar(5)));
}
}
package money;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.omg.CORBA.DATA_CONVERSION;
public class testMultiplication {
@Test
public void testMultiplication() {
Money five = Money.dollar(5);
assertEquals(Money.dollar(10),five.times(2));//10달러를 가진 객체와 , 5*2객체 가 같냐
assertEquals(Money.dollar(15), five.times(3));//15와 15는 같냐
}
@Test
public void testEquality() {
assertTrue(Money.dollar(5).equals(Money.dollar(5)));
assertFalse(Money.dollar(5).equals(Money.dollar(6)));
assertTrue(new Franc(5).equals(new Franc(5)));
assertFalse(new Franc(5).equals(new Franc(6)));
assertFalse(new Franc(5).equals(Money.dollar(5)));
}
@Test
public void testFrancMultiplication() {
Money five = Money.franc(5);
assertEquals(Money.franc(10),five.times(2));//10달러를 가진 객체와 , 5*2객체 가 같냐
assertEquals(Money.franc(15), five.times(3));//15와 15는 같냐
}
}
Franc비슷하게 만들어 준다.
검토
- 동일한 메서드 (times)의 두변이형 메서드 서명부를 통일 시킴으로써 중복제거를 한단계진행
- 최소한 메서드 선언부만이라도 공통 사우이 클래스로 옮김
- 팩토리 메서드를 도입하여 테스트 코드에서 콘크리트 하위 클래스의 존재 사실을 분리
- 하위 클래스가 사라지면 몇몇 테스트는 불필요한 여분에 것이 된다는 것을 인식 하지만 일단 냅둡
할일 목록에서 어떤 것을 하면 하위 클래스를 제거 할 수있을까?
통화개념을 도입하고 , 통화개념을 테스트 해야한다.
money
abstract String currency();
그 다음 두 하위 클래스에서 이를 구현하자 .
@Override
String currency() {
return "CHF";
}
@Override
String currency() {
return "USD";
}
package money;
public class Franc extends Money {
private String currency;
public Franc(int amount) {
this.amount = amount;
currency="CHF";
}
Money times(int multiplier) {
return new Franc(amount*multiplier);
}
@Override
String currency() {
return currency;
}
}
package money;
public class Dollar extends Money{
private String currency;
public Dollar(int amount) {
this.amount = amount;
currency = "USD";
}
Money times(int multiplier) {
return new Dollar(amount*multiplier);
}
@Override
String currency() {
return currency;
}
}
이제 두 currency() 가 동일 하므로 변수 선언과 currency() 구현을 둘다 위로 올릴 수 있다.
package money;
public abstract class Money {
protected int amount;
protected String currency;
abstract Money times(int muliplier);
public boolean equals(Object obj) {
Money money =(Money)obj;
return amount == money.amount && getClass().equals(money.getClass());
}
String currency() {
return currency;
};
static Money dollar (int amount) {
return new Dollar(amount);
}
static Money franc (int amount) {
return new Franc(amount);
}
}
문자열 USD 와 CHF 를 정적 팩토리 메서드로 옮긴다면 두 생성자가 동일해질 것이고, 그렇다면 공통 구현을 만들 수 있는 것이다.
우선 생성자에 인자를 추가하자.
package money;
public class Franc extends Money {
private String currency;
public Franc(int amount, String currency) {
this.amount = amount;
currency="CHF";
}
생성자를 호출하는 두 곳이 깨진다.
Money times(int multiplier) {
return new Franc(amount*multiplier,null);
}
static Money franc (int amount) {
return new Franc(amount,null);
}
지금 팩토리 메서드를 호출하지 않고 생성자를 호출해서 문제가 된다.
팩토리 메서드를 호출하면되지만 교리상은 기다리는 것이 맞음
지금 하는 일을 중단하지 않아야하니까
짧은 중단이 필요하면 중단한다.
하지만 짧은 중단만이다.
하던일을 중단하고 다른일을 하는 상태에서 그 일을 또 중단하지 않는다.
이것 짐 코플린이 알려준 큐직
진행하기 전에 times를 정리하자 .
Money times(int multiplier) {
return Money.franc(multiplier*amount);
}
이제 팩토리 메서드가 'CHF'를 전달할 수 있다.
static Money franc (int amount) {
return new Franc(amount,"CHF");
}
그리고 마지막으로 인자를 인스턴스 변수에 할당 할 수 있다.
public Franc(int amount, String currency) {
this.amount = amount;
this.currency=currency;
}
이런 작은 단계로도 업무를 수행할 수도 있어야한다.
머니도 비슷하게 고쳐본다.
package money;
public abstract class Money {
protected int amount;
protected String currency;
abstract Money times(int muliplier);
public boolean equals(Object obj) {
Money money =(Money)obj;
return amount == money.amount && getClass().equals(money.getClass());
}
String currency() {
return currency;
};
static Money dollar (int amount) {
return new Dollar(amount,"USD");
}
static Money franc (int amount) {
return new Franc(amount,"CHF");
}
}
package money;
public class Dollar extends Money{
private String currency;
public Dollar(int amount, String currency) {
this.amount = amount;
currency = currency;
}
Money times(int multiplier) {
return Money.dollar(amount*multiplier);
}
@Override
String currency() {
return currency;
}
}
TDD를 하는 동아 계속 해주어야 하는 일종의 조율이다.
종종걸음이 답답하며 보폭을 넓히고
성큼성큼이 불안하면 보폭을 줄인다.
TDD란 조종해 나가는 과정이다.
두 생성자가 동일하니 구현을 상위 클래스로 올리자.
public Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
package money;
public class Dollar extends Money{
public Dollar(int amount, String currency) {
super(amount,currency);
}
Money times(int multiplier) {
return Money.dollar(amount*multiplier);
}
@Override
String currency() {
return currency;
}
}
package money;
public class Franc extends Money {
public Franc(int amount, String currency) {
super(amount, currency);
}
Money times(int multiplier) {
return Money.franc(multiplier*amount);
}
@Override
String currency() {
return currency;
}
}
할일 목록
$5 + 10CHF = $10(환율이 2:1인경우) - > 정상적 동작 하는 기능의 목표
$5 * 2 = $10 -> 죄악으로 구현하는 코드를 만드는 것 (구현)
amount를 private으로 만들기
Dollar 부작용 ? ->어떤 값으로 새로운 객체를 생성하는 패턴으로 만들어서 별칭 문제를 해결
Money 반올림?
equals( )
hashCode( )
Eqaul null
Eqaul object
5CHF * 2 = 10CHF
Dollar/Franc 중복
공용 equals
공용 times
Franc 과 Dollar 비교하기
통화?
testFrancMultiplication 제거
times( ) 를 상위 클래스로 올리고 하위 클래스들을 제거 할 주비가 거의 다 됐다.
하지만 일단은 지금까지 한 것을 검토해 보자, 우리는
- 큰 설계를 하다 곤경에 빠지지만 . 그래서 좀전에 주목했던 더 작은 작업을 수행함
- 다른 부분들을 호출자( 팩토리 메서드)로 옮김으로써 두 생성자를 일치 시킴
- time()가 팩토리 메서드를 사용하도록 마들기 위해 리팩토링을 중단
- 비슷한 리팩토링 (Franc에서 했던일 을 Dollar에서도 적용)을 한번에 큰 단계로 처리
- 동일한 생성자들을 상위 클래스로 올림
10장 흥미로운 시간
Money times(int multiplier) {
return new Franc(amount*multiplier, currency);
}
------------------------------------------------------------------------------
Money times(int multiplier) {
return new Dollar(amount*multiplier, currency);
}
아까 팩토리 메서드를 쓰게 만들도록 했지만
다시 고친다 .