이번장에서 이 원칙, 기법들을 알아보자!
isSatisfiedBy(screening)
condition.isSatisfiedBy(screening)
by. Larman04
- 오퍼레이션
- 실행하기 위해 객체가 호출될 수 있는 변환이나 정의에 관한 명세
- 인터페이스의 각 요소
- 구현이 아닌 추상화
※ 퍼블릭 인터페이스 관점에서 '메서드 호출'보다는 '오퍼레이션 호출'이라는 용어가 더 적절하다.
퍼블릭 인터페이스의 품질에 영향을 미치는 원칙(기법)
- 디미터 법칙
- 묻지말고 시켜라
- 의도를 드러내는 인터페이스
- 명령-쿼리 분리
- 디미터 법칙
협력하는 객체의 내부 구조에 대한 결합으로 발생하는 설계 문제를 해결하기 위해 제안된 원칙
M의 인자로 전달된 클래스 (C 자체를 포함)
C의 인스턴스 변수의 클래스
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);
}
Screening
인스턴스에게만 메시지 전송디미터 법칙과 캡슐화
- 디미터 법칙; 캡슐화의 또 다른 관점
- 캡술화와 디미터 법칙; 클래스의 캡슐화를 위한 구체적 지침 제공
- 캡슐화; 내부 구현의 은폐
- 디미터의 법칙; (협력간의 캡슐화 보존을 위해) 외부에서 접근하는 요소를 제한
- 디미터의 법칙; 협력과 구현의 유기적 통합
- 내부구현을 채우는 동시에,
- 현재 협력하는 클래스에 관해서도 고민하도록 주의를 환기.
publc class PeriodCondition {
public boolean isSatisfiedByPeriod(Screening screening) {...}
}
public class SequenceCondition {
public boolean isSatisfiedBySequence(Screening screening) {...}
}
- cf. 어떻게 수행하는지를 보여주는 메서드 명명 방식; 메서드의 내부 구현을 설명
- 내부 구현현을 협력 설계 초기에 너무 이르게 고민하게 한다.
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
public class PeriodCondition implements DiscountCondition {
public boolean isSatisfiedBy(Screening screening) {...}
}
public class SequenceCondition impelements DiscountCondition {
public boolean isSatisfiedBy(Screening screening) {...}
}
켄트 벡은 메서드의 목적을 효과적으로 전달하고자 의도를 드러내는 선택자를 사용해 메서드의 이름을 짓는 것에 관해 글을 쓴 적이 있다. 설계에 포함된 모든 공개 요소가 조화를 이뤄 인터페이스를 구성하고, 인터페이스를 구성하는 각 요소의 이름을 토대로 설계 의도를 드러낼 수 있는 기회를 얻게 된다. 타입 이름, 메서드 이름, 인자 이름이 모두 결합되어 의도를 드러내는 인터페이스를 형성한다. 그러므로 수행 방법에 관해서는 언급하지 말고 결과와 목적만을 포함하도록 클래스와 오퍼레이션의 이름을 부여하라. 이렇게 하면 클라이언트 개발자가 내부를 이해해야 할 필요성이 줄어든다.
방법이 아닌 의도를 표현하는 추상적인 인터페이스 뒤로 모든 까다로운 매커니즘을 캡슐화해야 한다. 도메인의 퍼블릭 인터페이스에서는 관계와 규칙을 시행하는 방법이 아닌 이벤트와 규칙 그 자체만을 명시한다.
방정식을 푸는 방법을 제시하지말고 이를 공식으로 표현해라. 문제를 내라. 하지만 문제를 푸는 방법을 표현해서는 안된다.[Evans03].
Theater
가 인자로 전달된 audience
와 인스턴스 변수인 ticketSeller
외에도, 내부에 포함된 객체에게 직접 접근하고 있다.Ticket ticket = ticketSeller.getTicketOffice().getTicet();
audience.getBag().minusAmount(ticket.getFee());
디미터 법칙을 위반한 코드를 수정하는 일반적인 방법은, 내부 구조를 묻는 것이 아니라, 직접 자신의 책임을 수행하도록 시키는 것이다.
public lass Theater {
private TicketSeller ticketSeller;
public Theater(TicketSeller ticketSeller) {
this.ticketSeller = ticketSeller;
}
public void enter(Audience audience) {
if (audience.getBag().hasInvitation()) {
Ticket ticket = ticketSeller.getTicketOffice().getTicket();
audience.getBag().setTicket(ticket);
} else {
Ticket ticket = ticketSeller.getTicketOffice().getTicket();
audience.getBag().minusAmount(ticket.getFee());
ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
audience.getBag().tetTicket(ticket);
}
}
}
묻지 말고 시켜라
적용내부 구조를 묻는 것이 아니라 원하는 작업을 시켜야 한다.
Audience
와 TicketSeller
가 내부 구조에 관해 묻지 말고 시켜라
스타일을 따르는 퍼블릭 인터페이스를 갖도록 수정하자.
Theater
(클라이언트)가 TicketSeller
에게 시키고 싶은 일Audience
가 Ticket
을 가지도록 만드는 것.public class TicketSeller {
public void setTicket(Audience audience) {
if (audience.getBag().hasInvitation()) {
Ticket ticket = ticketOffice.getTicket();
audience.getBag().setTicket(ticket);
} else {
Ticket ticket = ticketOffice.getTicket();
audience.getBag().minusAmount(ticket.getFee());
ticketOffice.plusAmount(ticket.getFee());
}
}
}
// Theater가 디미터의 법칙을 준수하도록 수정
public class Theater {
public void enter(Audience audience) {
ticketSeller.setTicket(audience);
}
}
TicketSeller
(클라이언트)가 Audience
에게 시키고 싶은 일Ticket
을 보유하는 것.public class Audience {
public Long setTicket(Ticket ticket) {
if (bag.hasInvitation()) {
bag.setTicket(ticket);
return 0L;
} else {
bag.setTicket(ticket);
bag.minusAmount(ticket.getFee());
return ticket.getFee();
}
}
}
// TicketSeller가 디미터의 법칙을 준수하도록 수정
public class TicketSeller {
public void setTicket(Audience audience) {
ticketOffice.plusAmount {
audience.setTicket(ticketOffice.getTicket());
}
}
}
Audience
(클라이언트)가 hasInvitation
을 이용해 Bag
의 상태를 확인하고 있다.Bag
이 스스로 상태를 확인하고 행동하도록, 관련 로직을 Bag
내부로 이동시키자.public class Bag {
public Long setTicket(Ticket ticket) {
if (hasInvation()) {
this.ticket = ticket;
return 0L;
} else {
this.ticket = ticket;
minsuAmount(ticket.getFee());
return ticket.getFee();
}
}
private boolean hasInvation() {
return invation != null;
}
private void minusAmount(Long amount) {
this.amoun -= amount;
}
}
// Audience가 디미터의 법칙을 준수하도록 수정
public class Audience {
public Long setTicket(Ticket ticket) {
return bag.setTicket(ticket);
}
}
인터페이스에 의도를 드러내자
적용응용이 어렵게 느껴지는 부분. 어떤식으로 의도를 드러내야 하는지, 예시를 통한 학습 필요.
TicketSeller
의 setTicket
은 클라이언트의 의도를 명확하게 전달하는가?Audience
의 setTicket
메서드의 의도는 무엇인가?Bag
의 setTicket
메서드는, 앞의 두 메서드와 동일한 의도인가?TicketSeller
의 setTicket()
; sellTo()
로 변경Audienc
의 setTicket()
;buy()
로 변경Bag
의 setTicket()
;hold()
로 변경public class TicketSeller {
public void sellTo(Audience audience) {...}
}
public class Audience {
public Long buy(Ticket ticket) {...}
}
public class Bag {
public Long hold(Ticket ticket) {...}
}
소프트웨어 설계에 절대적인 법칙은 없다.
설계는 트레이드 오프의 산물이므로, 유연한 적용에 대한 판단력이 필요하다.
소프트웨어 개발자는 원칙을 이해하고 따르되, 상황에 맞게 유연하게 적용할 줄 알아야한다.
원칙이 현재 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시해라.
원칙을 아는 것보다 더 중요한 것은 언제 원칙이 유용하고 언제 유용하지 않은지 판단할 수 있는 능력을 기르는 것이다.
원칙 적용 시 고려해야 할 예외 상황들을 알아보자!
IntStream.of(1, 15, 20, 3, 9).filter(x -> x > 10).distinct().count();
of
, filter
, distinct
메서드는 객체를 다른 객체로 변환시키면서 IntStream
이라는 동일한 인스턴스를 반환하고 있다.묻지말고 시켜라
& 디미터의 법칙
의 무조건적인 준수디미터의 법칙
과 묻지말고 시켜라
원칙의 준수는 낮은 응집도를 초래한다.public class PeriodCondition implements DisocuntCondition {
public boolean isStatisfiedBy(Screeing screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
Screening
의 내부 상태를 가져와 사용; 캡슐화 위반으로 보일 수 있다.Screening
으로 이동 & PeriodConditino
에서 호출하도록 변경해보자!묻지말고 시켜라
법칙의 준수로 여겨질 수 있다.public class Screening {
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
Screening
이 기간에 따른 할인 조건을 판단하는 책임을 떠안게 된다.영화 예매
가 아닌 다른 책임을 담당해 응집도가 낮아진다.할인 조건 판단
책임은 PeriodCondition
의 것이다.Screening
이 PeriodCondition
인스턴스 변수를 인자로 받아, 둘 사이의 결합도는 높아진다.개발하다 보면 원칙 주순에 대한 딜레마를 경험할 때가 많다.
중요한 건 원칙을 맹목적으로 따르는 것이 아니라, 클래스의 응집도에 초점을 맞춰야 한다는 조언을 얻을 수 있었다.
Movie
에게 묻지 않고 movies
컬렉션의 전체 영화 가격을 계산할 방법이 있을까?for (Movie each: movies) {
total += each.getFee();
}
"질문이 답변을 수정해서는 안 된다."
public class Event {
public boolean isStaitsfied(RecurringSchedule schedule) {
if (from.getDayOfWeek() != schedule.getDayOfWeek() ||
!from.toLocalTime().equals(schjedule.getFrom()) ||
!duration.equals(schedule.getDuration())) {
reschedule(schedule);
return false;
}
return true;
}
private void reschedule(RecurringSchedule schedule) {
from = LocalDateTime.of(from.toLocalDate().plusDays(dayDistance(schedule)),
schedule.getFrom());
duration = schedule.getDuration();
}
private long dayDistance(RecurringSchedule schedule) {
return schedule.getDayOfWeek().getValue() - from.getDayOfWeek().getValue();
}
}
reschedule
메서드는 Event
의 일정을 인자로 전달된 RecurringSchedule
의 조건에 맞게 변경한다.rechedule
를 호출하는 isSatisfied
메서드는 Event
가 조건을 만족하지 못하는 경우, Event
의 상태를 조건을 만족시키도록 변경한 후 false
를 반환한다.isSatisfied
는 명령과 쿼리, 2가지 역할을 동시에 수행하고 있다.isSatisfied
는 조건의 부합 여부를 판단한 후 부합할 경우 true
를, 부합하지 않는 경우 flase
를 반환한다.isSatisfied
는 조건에 부합하지 않을 경우 Event
의 상태를 조건에 부합하도록 변경한다.isSatisfied
가 부수효과를 가질 것으로 예상하지 못할 것이다.isSatisfied
가 처음 구현되던 당시, reschedule
의 호출 부분이 빠져 있었다.Event
가 조건이 맞지 않는 경우 Event
의 상태를 수정해야 한다는 요구사항이 있었고, 개발자는 별다른 고민 없이 기존의 isSatisfied
에 reschedule
를 호출하는 코드를 추가한 것이다.isSatisfied
; 겉으로 보기에 쿼리처럼 보이지만, 내부적으로 부수효과를 가지는 메서드public class Event {
public boolean isSatisfied(RecurringSchedule schedule) {
if (from.getDayOfWeek() != schedule.getDayOfWeek() ||
!from.toLocalTime().equals(schjedule.getFrom()) ||
!duration.equals(schedule.getDuration())) {
return false;
}
return true;
}
public void reschedule(RecurringSchedule schedule) {
from = LocalDateTime.of(from.toLocalDate().plusDays(dayDistance(schedule)),
schedule.getFrom());
duration = schedule.getDuration();
}
}
public class Event {
public boolean isSatisfied(RecurringSchedule schedule) {...}
public void reschedule(RecurringSchedule schedule) {...}
}
명령
인지 쿼리
인지 한눈에 파악할 수 있다.isSatisfied
는 리턴값을 가지는 쿼리이므로, 호출 시 부수효과를 걱정하지 않아도 된다.reschedule
은 리턴값을 가지지 않는 명령이므로, 부수효과에 주의해야 한다.reschedule
의 가시성이 private
에서 public
으로 변경됐다.isSatisfied
안에서 수행했던 명령을 클라이언트가 직접 실행할 수 있게 하기 위해서다.Event
가 조건을 만족시키지 않을 경우, reschedule
메서드 호출 여부를 Event
를 사용하는 곳에서 결정할 수 있다.if (!event.isSatisfied(schedule)) {
event.reschedule(schedule);
}
=
를 통한 값 변경f(1) + f(1) = 6
-> f(1) = 3
명령형 프로그램과 함수형 프로그래밍
- 명령형 프로그래밍(imperative programming)
- 부수효과를 기반으로 하는 프로그래밍 방식
- 상태를 변경시키는 연산들을 적절한 순서대로 나열함으로써 프로그램을 작성들을 적절한 순서대로 나열함으로써 프로그램을 작성
- 함수형 프로그래밍(functional programming)
- 부수효과가 존재하지 않는 수학적인 함수에 기반
- 참조 투명성의 장점을 극대화 + 실행 결과 이해 및 예측이 쉽다.
- 하드웨어의 발달로 병렬 처리가 중요해져, 최근에는 수요가 증가 중.
책임 주도 설계
디미터 법칙
; 객체보다 메시지를 먼저 고민하고 이후에 적절한 객체의 선정을 결정하므로, 의도적으로 디미터 법칙 위반을 최소화한다.묻지말고 시켜라
; 클라이언트 관점에서 메시지를 선택하므로, 필요한 정보를 물을 필요 없이 원하는 것을 표현한 메시지를 전송할 수 있다.의도를 드러내는 인터페이스
; 메시지를 먼저 선택하는 것은 클라이언트 관점에서 메시지 이름을 정하는 것으로, 그 의도가 분명히 드러날 수 밖에 없다.명령-쿼리 분리 원칙
; 예측 가능한 협력을 만들기 위해 명려과 쿼리 분리를 시도할 확률이 높다.