정리
영화 예매 시스템
목적 : 영화 예매를 더 쉽고 빠르게
개념 : 영화 상영을 위한 예매 시스템
할인 규칙
- 할인 정책
- 금액 할인 정책(할인액)
- 비율 할인 정책(할인율)
- 할인 조건
- 순번 조건
- 기간 조건
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public Screening(Movie movie, int sequence, LocalDateTime whenScreened){
this.movie = movie;
this.sequence = sequence;
this.whenScreened = whenScreened;
}
public LocalDateTime getStartTime(){
return whenScreened;
}
public boolean isSequence(int sequence){
return this.sequence == sequence;
}
public Money getMovieFee(){
return movie.getFee();
}
public Reservation reserve(Customer customer, int audienceCount){
return new Reservation(customer, this, calcuateFee(audienceCount), audienceCount);
}
private Money calculateFee(int audienceCount){
return movie.calculateMovieFee(this).times(audienceCount);
}
}
public class Money{
public static final Money ZERO = Money.wons(0);
private final BigDecimal amount;
public static Money wons(long amount) {
return new Money(BigDecimal.valueOf(amount));
}
public static Money wons(double amount) {
return new Money(BigDecimal.valueOf(amount));
}
Money(BigDecimal amount){
this.amount = amount;
}
public Money plus(Money amount){
return new Money(this.amount.add(amount.amount));
}
public Money minus(Money amount){
return new Money(this.amount.subtract(amount.amount));
}
public Money times(double percent){
return new Money(this.amount.multiply(
BigDecimal.valueOf(percent)));
}
public boolean isLessThan(Money other){
return amount.compareTo(other.amount) < 0;
}
public boolean isGreaterThanOrEqual(Money other){
return amount.compareTo(other.amount) >= 0;
}
}
public class Reservation{
private Customer customer;
private Screening screening;
private Money fee;
private int audienceCount;
public Reservation(Customer customer, Screening screening, Money fee, int audienceCount){
this.customer = customer;
this.screening = screening;
this.fee = fee;
this.audienceCount = audienceCount;
}
}
public class Movie{
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy){
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = discountPolicy; // 하나의 할인정책 인스턴스만 허용하도록 강제
}
public Money getFee(){
return fee;
}
public Money calculateMovieFee(Screening screening){
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
/* 이 메서드를 통해 실행시점에 할인 정책을 간단하게 변경할 수 있음.
상속보다 인스턴스 변수 관계를 연결한 설계가 더 유연함.
-> 이를 합성이라고 함.
*/
public void changeDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
// DiscountPolicy 인스턴스를 생성할 필요가 없기 때문에 추상 클래스로 구현
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountCondition ... conditions) {
this.conditions = Arrays.asList(conditions);
}
/*
**TEMPLATE METHOD 패턴**
부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를
자식 클래스에게 위임하는 디자인 패턴.
*/
public Money calculateDiscountAmount(Screening screening) {
for(DiscountCondition each : conditions) {
if (each.isSatisfiedBy(screening) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screening screening);
}
public interface DiscountCondition {
boolean isSatisfiedBy(Screening screening);
}
public class SequenceCondition implements DiscountCondition {
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
public boolean isSatisfiedBy(Screening screening) }
return screening.isSequence(sequence);
}
}
public class PeriodCondition implements DiscountCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public PeriodCondition(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
this.dayOfWeek = dayOfWeek;
this.startTime = startTime;
this.endTime = endTime;
}
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 AmountDiscountPolicy extends DiscountPolicy {
private Money discountAmount;
public AmountDiscountPolicy(Money discountAmount, DiscountCondition ... conditions) {
super(conditions);
this.discountAmount = discountAmount;
}
}
@Override
protected Money getDiscountAmount(Screening screening) {
return discountAmount;
}
public class PercentDiscountPolicy extends DiscountPolicy {
private double percent;
public PercentDiscountPolicy(double percent, DiscountCondition ... conditions) {
super(conditions);
this.percent = percent;
}
@Override
protected Money getDiscountAmount(Screening screening) {
return screening.getMovieFee().times(percent);
}
}
1. 어떤 클래스가 필요한지 고민하기 전에 어떤 객체들이 필요한지 고민하라.
→객체를 중심에 두는 접근법은 설계를 단순한고 깔끔하게 만듦.
2. 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
→이와 같은 시선은 설계를 유연하고 확장 가능하게 만듦.
훌륭한 협력이 훌륭한 객체를 낳고 훌륭한 객체가 훌륭한 클래스를 낳는다.
도메인의 구조를 따르는 프로그램 구조
도메인 : 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야.
객체지향 패러다임이 강력한 이유는 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문이다.
클래스 구현하기
클래스를 구현하거나 다른 개발자에 의해 개발된 클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분 짓는 것이다.
클래스는 내부와 외부로 구분되며 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지 결정하는 것이다.
자율적 객체는 상태와 행동을 함께 가지는 복합적인 존재
객체가 스스로 판단하고 행동하는 자율적인 존재.
데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화 라고 한다.
캡슐화를 서포트하는 접근 수정자(public, protected, private)를 통해 접근 제어.
캡슐화와 접근 제어는 객체를 두 부분으로 나눈다.
- 퍼블릭 인터페이스(외부에서 접근 가능)
- 구현(외부에서 접근 불가능)
인터페이스와 구현의 분리 원칙은 훌륭한 객체지향 프로그램을 위한 핵심 원칙이다.
프로그래머의 자유
프로그래머의 역할을 클래스 작성자와 클라이언트 프로그래머로 구분하는 것이 유용.[Eckel06]
클래스 작성자는 새로운 데이터 타입을 프로그램에 추가한다.
클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용한다.
클라이언트 프로그래머의 목표는 필요한 클래스들을 엮어 애플리케이션을 빠르고 안정적으로 구축하는 것이다.
클라이언트 프로그래머가 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지함으로써 클라이언트 프로그래머에 대한 영향을 걱정하지 않고 내부 구현을 마음대로 변경할 수 있는 것을 구현 은닉이라고 부른다.
→ 접근 제어 메커니즘이 support
설계가 필요한 이유는 변경을 관리하기 위해서!
협력(Collaboration) : 시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용
객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 ‘메시지 전송’이다.
메시지 수신: 다른 객체에게 요청이 도착할 때
메서드: 수신된 메시지를 처리하기 위한 자신만의 방법
메시지와 메서드의 구분은 다형성의 개념에서 출발.
오버라이딩과 오버로딩
오버라이딩 : 부모 클래스에 정의된 같은 이름의 같은 파라미터 목록을 가진 메서드를 자식 클래스에서 재정의 하는 것으로 메서드의 이름은 같지만 제공되는 파라미터의 목록이 다르다.
오버로딩 : 한 클래스에서 같은 이름을 가진 둘 이상의 메서드가 다른 파라미터 목록을 가진 것을 가리킨다.
컴파일 시간 의존성과 실행 시간 의존성
어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 한다.
코드의 의존성과 실행 시점의 의존성은 서로 다를 수 있다.
(클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다)
장점: 코드의 유연성과 확장가능성 증가
단점: 가독성 감소
위 트레이드오프를 잘 따져 설계해야 한다.
차이에 의한 프로그래밍
부모 클래스나 다른 부분만을 추가해 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라고 부른다.
상속과 인터페이스 그리고 다형성
업캐스팅 : 자식 클래스가 부모 클래스를 대신하는 것
다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.
이는 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 함을 의미한다.
→ 인터페이스가 동일해야 한다는 것이다.
인터페이스를 통일하기 위해 사용한 구현 방법이 상속인 것이다.
상속은 객체지향에서 코드를 재사용하기 위해 널리 사용되는 기법이다.
하지만 두 가지 관점에서 단점이 있다.
- 상속은 캡슐화를 위반한다.이는 자식 클래스가 부모 클래스에 강하게 결합되게 만들어 부모 클래스를 변경할 때 자식 클래스 또한 변경해야 할 확률을 높인다. 결과적으로 상속을 과도하게 사용한 코드는 변경하기 어렵다.
- 상속을 사용하면 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다.
- 상속은 설계를 유연하지 못하게 만든다.
- 상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정하여 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.
다형성을 구현하는 방법은 매우 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.
→ 메시지와 메서드를 실행 시점에 바인딩한다는 것.
이를 지연 바인딩 혹은 동적 바인딩이라고 한다.
(객체지향이 컴파일 시점의 의존성과 실행 시점의 의존성을 분리하고, 하나의 메시지를 선택적으로 서로 다른 메서드에 연결할 수 있는 이유)
이에 반해 전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을 초기 바인딩 혹은 정적 바인딩이라고 부른다.
동일한 메시지를 전송했을 때 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라지는 것을 의미한다.
구현 상속과 인터페이스 상속
구현 상속(서브클래싱) : 순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 것.
인터페이스 상속(서브타이핑) : 다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 것.
상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다. 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다.
추상화
장점
1. 추상화의 계층만 따로 떼어 놓고 보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.ex) 영화 예매 요금은 최대 하나의 ‘할인 정책’과 다수의 ‘할인 조건’을 이용해 계산할 수 있다.
→ ‘영화의 예매 요금은 ‘금액 할인 정책’과 ‘두 개의 순서 조건, 한 개의 기간 조건’을 이용해서 계산할 수 있다.’ 를 포괄할 수 있음.
세부사항을 무시하고 상위 개념만으로 도메인의 주요 개념을 설명할 수 있게 한다.
2. 추상화를 이용하면 설계가 더 유연해진다.
추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고 새로운 기능을 쉽게 추가하고 확장할 수 있다.
유연한 설계
책임의 위치를 결정하기 위해 조건문을 사용하는 것은 협력의 설계 측면에서 대부분 좋지 못한다.
항상 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법을 선택!
추상 클래스와 인터페이스 트레이드오프
작성하는 모든 코드에는 합당한 이유가 있어야 한다.
아주 사소한 결정이더라도 트레이드오프를 통해 얻어진 결론과 그렇지 않은 결론 사이의 차이는 크다.
합성
다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.
상속이 부모 클래스의 코드와 자식 클래스의 코드를 컴파일 시점에 하나의 단위로 강하게 결합하는 데 비해 다른 객체의 인스턴스를 포함해 이 다른 객체의 인터페이스를 통해 약하게 결합된다.
인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다.
합성은 상속이 가지는 두 가지 문제점을 모두 해결한다.
인터페이스에 정의된 메시지를 통해서만 재사용이 가능해 구현을 효과적으로 캡슐화할 수 있다.
의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.
상속은 클래스를 통해 강하게 결합되는 반면 합성은 메시지를 통해 느슨하게 결합된다.
따라서 코드 재사용을 위해선 상속보단 합성을 선호하는 것이 더 좋은 방법이다[GOF94].
그렇지만 다형성을 위해 상속과 합성을 함께 조합해서 사용할 수 밖에 없다.
객체지향 설계의 핵심은 적절한 협력을 식별하고 협력에 필요한 역할을 정의한 후에 역할을 수행할 수 있는 적절한 객체에게 적절한 책임을 할당하는 것이다.
느낀점
이번 장에선 ‘작성하는 모든 코드에는 합당한 이유가 있어야 하고 아주 사소한 결정이더라도 트레이드오프를 통해 얻어진 결론과 그렇지 않은 결론 사이의 차이는 크다’라고 작성하신 부분이 가장 인상 깊었다. 코드 한 줄을 짜더라도 이보다 더 나은 방법이 있을까 고민하고 최선의 방법을 선택할 수 있는 개발자가 되도록 노력해야겠다.
'독서 > 오브젝트(조영호 저)' 카테고리의 다른 글
6장 메시지와 인터페이스 (0) | 2023.06.02 |
---|---|
5장 책임 할당하기 (0) | 2023.06.02 |
4장 설계 품질과 트레이드오프 (0) | 2023.06.02 |
3장 역할, 책임, 협력 (0) | 2023.06.02 |
1장 객체, 설계 (0) | 2023.05.30 |