2021. 9. 8. 22:54ㆍ오브젝트 : 코드로 이해하는 객체지향 설계
1. 협력과 메시지
클라이언트-서버 모델
- 클라이언트 : 협력 안에서 메시지를 전송하는 객체
- 서버 : 협력 안에서 메시지를 수신하는 객체
클라이언트 ⇒ Screening / 서버 ⇒ Movie
클라이언트 ⇒ Movie / 서버 ⇒ DiscountPolicy
객체는 협력에 참여하는 동안 클라이언트와 서버의 역할을 동시에 수행한다.
- 다른 객체와 협력을 가능하게 해주는 매개체 ⇒ 메시지
협력과 관련된 용어
메시지
- 메시지 : 객체들이 협력하기 위해 사용할 수 있는 의사소통 수단.
- 메시지는 오퍼레이션명(operation name)과 인자(argument)로 구성된다.
- 메시지 전송 / 메시지 패싱 : 한 객체가 다른 객체에게 도움을 요청하는 것.
- 메시지 전송은 메시지에 메시지 수신자를 추가한 것
- 메시지 전송자 : 메시지를 전송하는 객체
메시지 전송자 ⇒ 클라이언트 / 메시지 수신자 ⇒ 서버
※ 메시지 전송은 메시지 수신자, 오퍼레이션명, 인자의 조합
오퍼레이션
- 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션이라고 한다.
- 구현 코드를 제외한 단순히 메시지와 관련된 시그니처를 말한다.
- 수행 가능한 어떤 행동에 대한 추상화다.
메서드
- 메시지에 응답하기 위해 실행되는 코드 블록
- 오퍼레이션의 구현을 메서드라고 한다.
- 동일한 오퍼레이션이라고 해도 메서드는 다를 수 있다.
퍼블릭 인터페이스
- 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합
시그니처
- 오퍼레이션의 이름과 파라미터의 목록을 합친 것.
2. 인터페이스와 설계 품질
좋은 인터페이스란 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 만족한 것
퍼블릭 인터페이스의 품질에 영향을 미치는 원칙과 기법
- 디미터 법칙
- 묻지 말고 시켜라
- 의도를 드러내는 인터페이스
- 명령-쿼리 분리
디미터 법칙
협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙 ⇒ 디미터 법칙(Law of Demeter)
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
Movie movie = screening.getMovie();
boolean discountable = false;
for(DiscountCondition condition: movie.getDiscountConditions()) {
if(condition.getType() == DiscountConditionType.PERIOD) {
discountable = screening.getWhenScreened().getDayOfWeek().equals(condition.getDayOfWeek()) &&
condition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
condition.getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
} else {
discountable = condition.getSequence() == screening.getSequence();
}
if(discountable) {
break;
}
}
Money fee;
if(discountable) {
Money discountAmount = Money.ZERO;
switch(movie.getMovieType()) {
case AMOUNT_DISCOUNT:
discountAmount = movie.getDiscountAmount();
break;
case PERCENT_DISCOUNT:
discountAmount = movie.getFee().times(movie.getDiscountPercent());
break;
case NONE_DISCOUNT:
discountAmount = Money.ZERO;
break;
}
fee = movie.getFee().minus(discountAmount).times(audienceCount);
} else {
fee = movie.getFee();
}
return new Reservation(customer, screening, fee, audienceCount);
}
}
ReservationAgency와 파라미터로 전달된 Screening사이의 결합도가 너무 높아서 변경이 쉽지 않고 Screening뿐만 아니라 Movie와 DiscountCondition에도 직접 접근한다.
- 디미터 법칙의 조건
- this 객체
- 메서드의 매개변수
- this의 속성
- this의 속성인 컬렉션의 요소
- 메서드 내에서 생성된 지역 객체
public class ReservationAgency { public Reservation reserve(Screening screening, Customer customer, int audienceCount) { Money fee = screening.calculateFee(audienceCount); return new Reservation(customer, screening, fee, audienceCount); } }
※ 부끄럼타는 코드(shy code) : 불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드
※ 디미터 법칙을 위반하는 코드
//기차 충돌(train wreck) 코드
screening.getMovie().getDiscountConditions();
묻지 말고 시켜라
객체의 상태에 관해 묻지 말고 원하는 것을 시켜라
메시지 전송자는 메시지 수신자의 상태를 기반으로 결정을 내린 후 메시지 수신자의 상태를 바꿔서는 안 된다. 오직 메시지 수신자가 담당해야 할 책임이다.
객체 자신이 보유하고 있는 정보나 메시지 전송 결과로 얻게 되는 정보만 사용해서 의사결정을 내리면 된다.
의도를 드러내는 인터페이스
- 메서드를 명명하는 두 가지 방법(켄트 백)
- 메서드가 작업을 어떻게 수행하는지를 나타낸다.public class PeriodCondition { public boolean isSatisfiedByPeriod(Screening screening) {...} } public class SequenceCondition { public boolean isSatisfiedBySequence(Screening screening) {...} }⇒ 메서드에 대해 제대로 커뮤니케이션하지 못한다.
- ⇒ 메서드 수준에서 캡슐화를 위반한다.
- 어떻게가 아닌 무엇을 하는지 드러낸다. 어떻게를 드러내는 이름이란 메서드의 내부 구현을 설명하는 이름이다. 무엇을 드러내는 이름은 객체가 협력 안에서 수행해야하는 책임에 관해 고민해야 한다.public class PeriodCondition { public boolean isSatisfiedBy(Screening screening) {...} } public class SequenceCondition { public boolean isSatisfiedBy(Screening screening) {...} } public interface DiscountCondition { boolean isSatisfiedBy(Screening screening); }
의도를 드러내는 선택자(Intention Revealing Selector) : 어떻게가 아닌 무엇을 하느냐에 따라 메서드의 이름을 짓는 패턴
⇒ 이 패턴을 인터페이스 레벨로 확장한 것을 의도를 드러내는 인터페이스라고 한다.
3. 원칙의 함정
⇒ 법칙에는 예외가 없지만 원칙에는 예외가 넘쳐난다.
디미터 법칙은 하나의 도트(.)를 강제하는 규칙이 아니다.
IntStream.of(1, 15, 20, 3, 9 ).filter( x -> x > 10).distinct( ).count();
결합도와 응집도의 충돌
public class PeriodCondition implements DiscountCondition {
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
}
}
public class Screening {
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
return whenScreened.getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(whenScreened.toLocalTime()) <= 0 &&
endTime.compareTo(whenScreened.toLocalTime()) >= 0;
}
}
public class PeriodCondition implements DiscountCondition {
public boolean isSatisfiedBy(Screening screening) {
return screening.isDiscountable(dayOfWeek, startTime, endTime);
}
}
4. 명령-쿼리 분리 원칙
- 루틴 : 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈
- 프로시저 : 부수효과를 발생시키지만 값을 반환할 수 없다.
- 명령 : 객체의 상태를 수정하는 오퍼레이션
- 함수 : 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.
- 쿼리 : 객체와 관련된 정보를 반환하는 오퍼레이션
- 프로시저 : 부수효과를 발생시키지만 값을 반환할 수 없다.
오퍼레이션은 명령과 쿼리 중 하나여야 한다.
명령과 쿼리를 분리하기 위한 두 가지 규칙
- 객체의 상태를 변경하는 명령은 반환값을 가질 수 없다.
- 객체의 정보를 반환하는 쿼리는 상태를 변경할 수 없다.
명령-쿼리 분리와 참조 투명성
명령과 쿼리를 분리함으로써 참조 투명성의 장점을 제한적이지만 누릴 수 있습니다.
참조 투명성
"어떤 표현식 e가 있을 때 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성"
f(1) + f(1) = 6
f(1) * 2 = 6
f(1) - 1 = 2
일 때, f(1)을 3으로 바꾸더라도 식의 결과는 변하지 않는다.
3 + 3 = 6
3 * 2 = 6
3 - 1 = 2
책임에 초점을 맞춰라
'오브젝트 : 코드로 이해하는 객체지향 설계' 카테고리의 다른 글
[오브젝트 : 코드로 이해하는 객체지향 설계] 5. 책임 할당하기 (0) | 2021.09.02 |
---|---|
[오브젝트 : 코드로 이해하는 객체지향 설계] 3. 역할, 책임, 협력 (0) | 2021.08.24 |
[오브젝트 : 코드로 이해하는 객체지향 설계] 2. 객체지향 프로그래밍 (0) | 2021.08.17 |
[오브젝트 : 코드로 이해하는 객체지향 설계] 1. 객체, 설계 (0) | 2021.08.11 |