[오브젝트 : 코드로 이해하는 객체지향 설계] 6. 메시지와 인터페이스

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에도 직접 접근한다.

  • 디미터 법칙의 조건
    1. this 객체
    2. 메서드의 매개변수
    3. this의 속성
    4. this의 속성인 컬렉션의 요소
    5. 메서드 내에서 생성된 지역 객체
    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();

묻지 말고 시켜라

객체의 상태에 관해 묻지 말고 원하는 것을 시켜라

메시지 전송자는 메시지 수신자의 상태를 기반으로 결정을 내린 후 메시지 수신자의 상태를 바꿔서는 안 된다. 오직 메시지 수신자가 담당해야 할 책임이다.

객체 자신이 보유하고 있는 정보나 메시지 전송 결과로 얻게 되는 정보만 사용해서 의사결정을 내리면 된다.

의도를 드러내는 인터페이스

  • 메서드를 명명하는 두 가지 방법(켄트 백)
    1. 메서드가 작업을 어떻게 수행하는지를 나타낸다.public class PeriodCondition { public boolean isSatisfiedByPeriod(Screening screening) {...} } public class SequenceCondition { public boolean isSatisfiedBySequence(Screening screening) {...} }⇒ 메서드에 대해 제대로 커뮤니케이션하지 못한다.
    2. ⇒ 메서드 수준에서 캡슐화를 위반한다.
    3. 어떻게가 아닌 무엇을 하는지 드러낸다. 어떻게를 드러내는 이름이란 메서드의 내부 구현을 설명하는 이름이다. 무엇을 드러내는 이름은 객체가 협력 안에서 수행해야하는 책임에 관해 고민해야 한다.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

책임에 초점을 맞춰라

반응형