이번장에서 이 원칙, 기법들을 알아보자!


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)
- 부수효과가 존재하지 않는 수학적인 함수에 기반
- 참조 투명성의 장점을 극대화 + 실행 결과 이해 및 예측이 쉽다.
- 하드웨어의 발달로 병렬 처리가 중요해져, 최근에는 수요가 증가 중.
책임 주도 설계디미터 법칙; 객체보다 메시지를 먼저 고민하고 이후에 적절한 객체의 선정을 결정하므로, 의도적으로 디미터 법칙 위반을 최소화한다.묻지말고 시켜라; 클라이언트 관점에서 메시지를 선택하므로, 필요한 정보를 물을 필요 없이 원하는 것을 표현한 메시지를 전송할 수 있다.의도를 드러내는 인터페이스; 메시지를 먼저 선택하는 것은 클라이언트 관점에서 메시지 이름을 정하는 것으로, 그 의도가 분명히 드러날 수 밖에 없다.명령-쿼리 분리 원칙; 예측 가능한 협력을 만들기 위해 명려과 쿼리 분리를 시도할 확률이 높다.