코드스피츠 2강 오브젝트 - 2회차(1)
책 오브젝트 13장를 기반으로 하는 코드스피츠 강의 오브젝트2 - 2회차 (1) 를 정리한 내용입니다.
계약 (Contract)
프로그램에서는 메시지를 통해서 계약을 성립하게 된다.
전달받은 메시지의 규격을 지키고 있어야 한다. (precondition)
Receiver 가 메시지를 검증하는 것을 기본적으로 정책으로 삼고있다.
전달할 메시지의 규격 (postcondition)
Receiver 가 값을 돌려주기 전에 한다.
객체 자신의 규격 (class invariant)
예를 들면 네트워크 Bridge 를 들수있다.
메시지를 받을 수 있는 필드가 선언되어 있는가?
위임된 책임의 컨텍스트
컨텍스트를 잘 맞춰주고 있는가?
객체간의 서로 계약을 하기 위해선 위 4가지가 만족되어 있어야 한다.
Invariant
객체 초기 상태, 객체의 메시지 수신전 상태라고 볼 수 있다.
메시지와 무관한 객체의 상태 (필드)
일반적으로 피드값의 상태 점검
DI 에게 위임하거나 초기화 할당으로 처리
public class Plan {
// Calculator calc; 는 초기값이 Null 이다.
private Calculator calc;
private Set<Call> calls = new HashSet<>();
public final void addCall(Call call){
calls.add(call);
}
public final void setCalculator(Calculator calc){
this.calc = calc;
}
public final Money calculateFee(){
// calc 은 항상 Null 이 아닌지 ?
return calc.calcCallFee(calls, Money.ZERO);
}
}
Calculator 생성자 조건이 없기 때문에 Null을 방지하기 위해서 기본 객체를 생성해 줄수 있다.
Precondition
일반적으로 validation
메시지로 받는 값을 스스로 검증함
검증이 확정된 형으로 갈음할 수 있음
Null Precondition
빨간색 박스는 Null 문제가 발생할 수 있다.
@NonNull Annotation
은 Compile 타임에서는 검증할 수 있으나 Runtime 타임에서는 알 수 없다.
if 로 Null 이 아닌 경우를 체크한다.
이 경우에는 return 이 void 가 아니라 boolean 이어야 한다.
return true = 성공
return false = 실패
Context 상에 부드럽게 넘어도가도 되는 것인지 알아야 한다.
throw
하나를 throw 로 던지면 모든 것들은 다 throw 던지는 정책을 팀내 정책, 회사 정책을 세워라!
추가적인 상태 검사
calc 가 비어있는지도 체크할 수 있다.
Postcondition
일반적으로 결과값 검증이라 함
보내줄 값이 올바름을 검증함
검증이 확정된 형으로 갈음할 수 있음
사후 검증 대상
0원 혹시 음수가 나오면 정상이 아니다.
public class Plan{
private Calculator calc = new Calculator();
private Set<Call> calls = new HashSet<>();
public final void addCall(Call call){...}
public final void setCalculator(Calculator calc){...}
public final Money calculateFee(){
Money result = calc.calcCallFee(calls, Money.ZERO);
if(calls.size() > 0 && (
result.equals(Money.ZERO) ||
result.isLessThan(Money.ZERO)
)) throw new RuntimeException("calculate error");
return result;
}
}
Call 이 최소 1건이 있었는데 요금이 0원이거나 음수인지 아닌지를 검증한다.
계약별 책임할당
Null 체크와 같은 것은 어쩔수없이 본인이 해야한다.
isEmpty 는 getter 나 마찬가지 역할을 한다.
이것은 캡슐화하는데 실패했기 때문에 일어난다.
헐리우드 원칙 위반 (묻지 말고 시켜라) 하고 있다.
Plan은 Calculator에게 calls 의 계산 로직의 책임을 위임하고 있기 때문에
Calculator 에게 시키는 것이 바람직하다. (헐리우드 원칙)
사후계약조건 책임할당
Plan 은 Money인 result를 검사해야할 이유가 전혀 없다.
this 가 없기 때문에 빨간색 박스는 Plan 의 메소드가 아니다.
Calculator 는 calc 계산하는 로직을 위임받은 객체이므로 Calculator 검증로직을 맡게된다.
Plan 의 빨간색 박스에 있는 calc.calcCallFee 를 수행하기 전 후로 검증하지 않다는 것은
Plan 과 Calculator 가 성실히 메시지를 주고 받는다는 계약은 맺은 것이다.
이것이 바로 진짜 계약이다.
협력을 통한 책임분할
일반적으로 precondition 은 Receiver(Plan) 이 가지고 있지만
postcondition 은 Receiver(Plan) 으로 부터 위임된 객체 (Calculator) 가 책임을 수행한다.
그러므로 우리는 precondition 과 postcondition 은 일관성있게 하자!
한가지 생각해보자!
Calculator 는 내부에 calls 를 가지고 있지 않고 외부로 부터 전달받는다.
그런데 왜? 이걸 검증해야 하지? 라는 의문점이 생긴다.
Calculator 와 Plan 의 계약 관계가 바뀌었다.
calls 가 비어있는지 아닌지는 Plan 가 하도록 계약이 변경되었다.
Calculator 는 더이상 0건이 아닌 calls 를 받도록 계약이 바뀌었다.
런타임에 의한 계약조건
위에 빨간색 박스 호출 시점이 다르다는 것을 인지하여야 한다.
즉, 두 개의 시점차이가 발생한다.
Calculator check 는 잘못된 함수이다.
Plan 에서는 setCalculator 를 호출하는 시점에 check 를 하고 있다.
근본적으로 check는 하는 이유는 Plan 이 요금을 "계산할 떄" 하기 때문에 시점 차이가 발생할 수 있다.
그러므로 실제로 "계산할 때" check 검증 로직을 넣어주어야 한다.
Plan 의 setCalculator 에서 chekc 함수를 제거한다.
이제 정말 Calculator 가 "계산할 때" 검증 로직을 추가한다.
이제보니 Plan 의 precondition 으로 보였지만 Calculator의 invariant 이었다.
즉, 요금을 "계산할 때" 무조건 필수 조건이 충족되어야 한다라는 의미로 invariant 로 이야기할 수 있다.