인터페이스와 설계 품질
퍼블릭 인터페이스의 품질에 영향을 미치는 원칙과 기법
- 디미터 법칙
- 묻지 말고 시켜라
- 의도를 드러내는 인터페이스
- 명령-쿼리 분리
디미터 법칙
디미터 법칙
낯선 자에게 말하지 마라.
오직 인접한 이웃하고만 말하라.
오직 하나의 도트만 사용하라.
클래스 내부의 메서드가 아래 조건을 만족하는 인스턴스에만 메시지를 전송하도록 프로그래밍해야 한다.
- this 객체
- 메서드의 매개변수
- this의 속성
- this의 속성인 컬렉션의 요소
- 메서드 내에서 생성된 지역 객체
부끄럼타는 코드 Shy code : 불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코
기차 충돌 Train wreck : 메시지 전송자가 수신자의 내부 구조에 대해 물어보고 반환받은 요소에 대해 연쇄적으로 메시지를 전송하게 되면 내부 구현이 외부로 노출되는 효과와 같다. 메시지 전송자가 메시지 수신자의 내부 구현에 강하게 결합된다.
디미터 법칙과 캡슐화
디미터 법칙은 캡슐화를 다른 관점에서 표현한 것이다. 디미터 법칙이 가치 있는 이유는 클래스를 캡슐화하기 위해 따라야 하는 구체적인 지침을 제공하기 때문이다. 캡슐화 원칙이 클래스 내부의 구현을 감춰야 한다는 사실을 강조한다면 디미터 법칙은 협력하는 클래스의 캡슐화를 지키기 위해 접근해야 하는 요소를 제한한다. 디미터 법칙은 협력과 구현이라는 사뭇 달라 보이는 두 가지 문맥을 하나의 유기적인 개념으로 통합한다. 클래스의 내부 구현을 채워가는 동시에 현재 협력하고 있는 클래스에 관해서도 고민하도록 주의를 환기시키기 때문이다.
묻지 말고 시켜라
- 훌륭한 메시지는 객체의 상태에 관해 묻지 말고 원하는 것을 시켜야 한다.
- 메시지 전송자는 메시지 수신자의 상태를 기반으로 결정을 내린 후 메시지 수신자의 상태를 바꾸면 안된다.
절차적인 코드는 정보를 얻은 후에 결정한다.
객체지향 코드는 객체에게 그것을 하도록 시킨다.
- 상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체하여 인터페이스를 향상시켜야 한다.
의도를 드러내는 인터페이스
public class PeriodCondition {
public boolean isSatisfiedBy(Screening screening) {
//...
}
}
public class SequenceCondition {
public boolean isSatisfiedBy(Screening screening) {
//...
}
}
- 메서드에 대해 제대로 커뮤니케이션 하지 못한다.
동일한 작업을 수행하지만 메서드의 이름이 다르기 때문에 두 메서드의 내부 구현을 이해하지 못하면 동일한 작업을 수행하는 사실을 알기 어렵다. - 메서드 수준에서 캡슐화를 위반한다.
코드를 변경하면 단순 참조 객체만 변경되는것이 아니라 메서드마저 변경된다. 책임을 수행하는 방법을 드러내는 메서드는 변경에 취약하다.
public insterface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
public class PeriodCondition implements DiscountCondition {
@Override
public boolean isSatisfiedBy(Screening screening) {
//...
}
}
public class SequenceCondition implements DiscountCondition {
@Override
public boolean isSatisfiedBy(Screening screening) {
//...
}
}
- 의도를 드러내는 선택자 Intention Revealing Selector, 의도를 드러내는 인터페이스 : 메서드의 이름은 ‘어떻게’가 아니라 ‘무엇’을 하는지 드러내야 한다.
무엇을 하는지 드러내도록 이름을 짓기 위해서는 객체가 협력 안에서 수행해야 하는 책임에 관해 고민해야 한다. 외부의 객체가 메시지를 전송하는 목적을 먼저 생각하도록 만든다. 결과적으로 협력하는 클라이언트의 의도에 부합하도록 메서드의 이름을 짓게 된다.
함께 모으기
디미터의 법칙을 위반하는 티켓 판매 도메인
public class 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 ticekt = ticketSeller.getTicketOffice().getTicket();
audience.getBag().minusAmount(ticket.getFee());
ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
audience.getBag().setTicket(ticket);
}
}
}
묻지말고 시켜라
public class 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 ticekt = ticketSeller.getTicketOffice().getTicket();
audience.getBag().minusAmount(ticket.getFee());
ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
audience.getBag().setTicket(ticket);
}
}
}
public class Theater {
public void enter(Audience audience) {
ticketSeller.setTicket(audience);
}
}
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();
}
}
}
public class Bag {
public Long setTicket(Ticket ticket) {
if (bag.hasInvitation()) {
bag.setTicket.ticket);
return 0L;
} else {
bag.setTicket(ticket);
bag.minusAmount(ticket.getFee());
return ticket.getFee();
}
}
}
public class Audience {
public Long setTicket(Ticket ticket) {
return bag.setTicket(ticket);
}
}
인터페이스에 의도를 드러내자
public class TicketSeller {
public void sellTo(Audience audience) {
//...
}
}
public class Audience {
public Long buy(Ticket ticket) {
//...
}
}
public class Bag {
public Long hold(Ticket ticket) {
//...
}
}
원칙의 함정
법칙에는 예외가 없지만 원칙에는 예외가 넘쳐난다.
디미터 법칙은 하나의 dot을 강제하는 규칙이 아니다
InsStream.of(1,12,30,2,3)
.filter(x -> x > 0)
.distinct()
.count();
- 기차 충돌처럼 보이더라도 객체의 내부 구현에 대한 어떤 정보도 노출되지 않는다면 디미터 법칙을 준수하는 것이다.
결합도와 응집도의 충돌
- 모든 상황에서 맹목적으로 위임 메서드를 추가하면 같은 퍼블릭 인터페이스 안에 어울리지 않는 오퍼레이션들이 공존하게 된다.
- 결과적으로 객체는 상관 없는 책임들을 한꺼번에 떠안게 되기 때문에 응집도가 낮아지게 된다.
- 서로 상관없는 책임들이 뭉쳐있는 클래슨느 작은 변경으로 쉽게 무너진다.
- 가끔씩 묻는 것 외에는 다른 방법이 존재하지 않는 경우도 있다.