설계 원칙과 관련된 용어를 정리해보자!
어떻게 코드를 수정하지 않고서 새로운 동작을 추가할 수 있을까?
Movie
설계는 계방-폐쇄 원칙(OCP)을 준수하고 있다.의존성 관점에서 개방-폐쇄 원칙을 따르는 설계란 컴파일타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조라고 할 수 있다.
public abstract class DiscountPolicy {
private List<DiscountCodition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountCondition ... conditions) {
this.conditions = Arrasy.asList(conditions);
}
// 구현; 할인여부 판별 로직
public Money calcualteDiscountAmount(Screening screening) {
for (DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening)) {
return getDiscountedFee(screening);
}
}
return screening.getMovieFee();
}
// 추상화; 할인요금 계산방법
abstract protected Money getDiscountAmount(Screening screening);
DiscountPolicy
= 추상화calcualteDiscountAmount
= 할인여부 판단getDiscountAmount
= 할인요금 계산public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
...
this.disocuntPolicy = discountPolicy;
}
public Money calcualteMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDisocuntAmount(screening));
}
}
Movie
는 할인 정책을 추상화한 DiscountPolicy
에만 의존한다.DiscountPolicy
= 변하지 않는 추상화Movie
가 더 안정된 추상화인 DiscountPolicy
에 의존하므로, 할인 젗액 추가를 위해 새로운 자식 클래스가 추가되더라도 영향을 받지 않는다.Movie
와 DiscountPolicy
는 변경에 닫혀있다.예시에서 Movie
는 생성자 안에서 DiscountPolicy
의 인스턴스를 생성하고, calculateMovieFee
메서드 안에서 생성한 인스턴스를 사용하고 잇다.
즉, 동일한 클래스에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하고 있다.
public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee) {
...
// 객체 생성
this.discountPolicy = new AmountDiscountPolicy(...);
}
public Money calculateMovieFee(Screening screening) {
// 생성한 객체 사용
return fee.minus(screening.calculateDiscountAmount(screening));
}
}
소프트웨어 시스템은 (응용 프로그램 객체를 제작하고 의존성을 서로 "연결"하는) 시작 단계와 (시작 단계 이후에 이어지는) 실행 단계를 분리해야 한다.
객체 생성 책임
을 옮기는 것Movie
가 어떤 할인 정책을 적용할지는, Movie
와 협력할 클라이언트가 지정하는 것이 가장 적절하다.public Money getAvatarFee() {
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDisocuntPolicy(...));
return avatar.getFee();
}
public class Factory {
public Movie createAvatarMovie() {
return new Movie("아바타",
Duration.ofMinutes(230),
Money.wons(10000),
new AmountDisocuntPolicy(...));
}
}
public class Client {
private Factory factory;
public client(Factory factory) {
this.factory =factory;
}
public Money getAvatarFee() {
Movie avatar = factory.createMadMaxMovie();
return avatar.getFee();
}
}
Client
에는 사용과 관련된 책임만 남는다.Factory
를 통해 생성된 Movie
객체를 얻기 위함Movie
를 통해 가격을 계산하기 위한 것Client
는 오직 사용과 관련된 책임만 지고, 생성과 관련된 어떤 지식도 가지지 않을 수 있다.FACTORY
사용은 도메인 모델과 무관한 기술적인 결정이다.PURE FABRICATION 패턴
- 문제: 도메인 객체에 책임을 할당할 경우, high cohesion, low coupling, 낮은 재사용성이 발생할 수 있다.
- 해결책: 도메인 개념을 표현하지 않는, 인위적으로 편의상 만든 클래스에 매우 응집된 책임을 할당해라.
- 순수한 가공물(pure fabrication)이라는 용어는, 적절한 대안이 없을 때 사람들이 창조적인 무언가를 만들어낸다는 것을 의미하는 관용적 표현이다.
SERVICE LOCATOR
에게 의존성 해결을 요청한다.movie
는 직접 ServiceLocator
의 메서드를 호출하여 DiscountPolicy
에 대한 의존성을 해결한다.public class Movie {
...
private DiscountPolicy discountPolicy;
public Moive(String title, Duration runningTime, Money fee) {
this.title = title;
this.runningTime = runningTime'
this.fee = fee;
this.distcountPolicy = ServiceLocator.discountPolicy();
}
]
ServiceLocator
는 DisocuntPolicy
의 인스턴스를 등록하고 반환할 수 있는 메서드를 구현한 저장소다.public class ServiceLocator() {
private static SericeLocator soleInstance = new ServiceLocator();
private DiscountPolicy discountPolicy;
public static DiscountPolicy discountPolicy() {
return soleInstance.discountPolicy;
}
public static void provide(DiscountPolicy disocuntPolicy) {
soleInstance.disocuntPolicy = discountPolicy;
}
private ServiceLocator() {
}
}
Movie
의 인스턴스가 의존하기를 바라는 인스턴스를 등록한 후 Movie
를 생성하는 방식으로 사용할 수 있다.ServiceLocator.provide(new AmountDiscountPolicy(...));
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(1000));
ServiceLocator.provide(new PercentDiscountPolicy(...));
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(1200));
각 단위 테스트는 서로 고립되어야 한다
는 단위 테스트의 기본 원칙을 위반하게 된다.private
로 지정하고, 내용을 숨기는 개념이 아니다.명시적인 의존성이 숨겨진 의존성보다 좋다.
가급적 퍼블릭 인터페이스를 노출해라.
숨겨진 의존성은 코드 이해와 수정을 어렵게 한다.
Movie
가 DiscountPolicy
에 의존하고 있다.Movie
를 정상적으로 컴파일하기 위해 DiscountPolicy
클래스가 필요한데, 이 클래스가 포함돼있는 패키지 안에 AmountDiscountPolicy
, PercentDiscountPolicy
가 함께 포함되어 있다.Movie
를 사용하기 위해 DiscountPolicy
가 요구되고, 같은 패키지에 있는 불필요한 클래스까지 함께 존재해야 한다.내 두 번째 주장은 우리의 지적 능력은 정적인 관계에 더 잘 들어맞고, 시간에 따른 진행 과정을 시각화하는 능력은 상대적으로 덜 발달했다는 점이다. 이러한 이유로 우리는 (자신의 한곌르 알고 있는 현명한 프로그래머로서) 정적인 프로그램과 동적인 프로세서 사이의 간극을 줄이기 위해 최선을 다해야 하며, 이를 통해 프로그램(텍스트 공간에 흩뿌려진)과 (시간에 흩뿌려진) 진행과정 사이를 가능한 한 일치시켜야 한다.[Dijkstra68].
그것은 바로 객체가 무엇이 되고 싶은지를 알게 될 때까지 객체들을 어떻게 인스턴스화 할 것인지에 대해 전혀 신꼉쓰지 않았다는 것이다. 이때 가장 중요한 관심거리는 마치 객체가 이미 존재하는 것처럼 이들 간의 관곌르 신경쓰는 일이다. 필자는 때가 되면 이러한 관계에 맞게 객체를 생성할 수 있을 것이라고 추측했다.
이렇게 추측했던 이유는 설계 동안 머리속에 기억해야 할 객체 수를 최소화해야하기 때문이다. 보통 요구사항을 충족시킬 수 있는 객체를 인스턴스화하는 방법에 대해 생각하는 것을 뒤로 미룰 때 위험을 최소화한 상태로 작업할 수 있다. 너무 일찍 결정하는 것은 비생산적이다.
객체를 생성하는 방법을 여러분 자신이 신경쓰기 전에 시스템에 필요한 것[책임]들을 생각하자[Shalloway01].