협력, 객체, 클래스
대부분의 객체지향 개발자들은 "클래스"를 먼저 고민한다.
도메인(Domain) 이란?
- 문제 해결을 위해 사용자가 System을 사용하는 분야
Domain과 클래스 구조 : 네이밍을 거의 동일하거나 유사하게 만든다.
요구사항 : 오브젝트 37p 참조
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();
}
}
- 외부에서는 직접 객체의 속성에 접근할 수 없다.
- 메서드를 통해서만 접근 가능
이렇게 내. 외부를 구분하는 이유는 "경계의 명확성이 객체의 자율성을 보장하기 때문이다"
자율적인 객체
객체
1. 상태와 행동을 같이 가진다.
2. 자기 스스로 판단이 가능하다.
외와 같이 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라고 부른다.
퍼블릭 인터페이스 : 외부에서 접근 가능한 부분
구현 : 외부에서는 접근 불가하고 오직 내부에서만 접근이 가능하는 부분
객체는 상태를 숨기고 행동만 외부에 공개한다.
프로그래머의 역할
1. 클래스 작성자 : 새로운 데이터 타입을 프로그램에 추가, 클라이언트 프로그래머에게 필요한 부분만을 공개 (구현 은닉)
-인터페이스를 바꾸지 않는 이상 외부에 미치는 영향을 걱정하지 않고도 내부 구현을 마음대로 변경 가능
-클라이언트 프로그래머에게 필요한 부분만을 공개한다. (구현 은닉)
2. 클라이언트 프로그래머 : 클래스 작성자에 의해 추가된 데이터 타입을 사용
-인터페이스만을 알고 있으면 된다.
코드 구현
메시지 전송
1. 상영 객체가 예약, 요금계산의 책임을 가진다.
2. 시작점이 상영 객체이다.
3. 객체를 의인화
Screening은 Movie안에 calculateMovie 메서드가 존재하고 있는지조차 알지 못한다.
단지 Movie가 calculateMovieFee 메시지에 응답할 수 있다고 믿고 메시지를 전송할 뿐이다.
일반 요금을 계산하기 위한 협력
할인 정책에 대한 정보가 없는 Movie 객체
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 abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
//여러개의 할인조건 포함
public DiscountPolicy(DiscountCondition ... conditions) {
this.conditions = Arrays.asList(conditions);
}
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);
}
실제 애플리케이션에선 DiscountPolicy의 인스턴스를 생성할 필요가 없으므로 추상 클래스로 선언한다.
(추상 클래스는 미완성 클래스이므로 객체 생성 불가)
- 할인 정책과 전체적인 흐름을 제어
- 실질적인 요금계산은 getDiscountAmount 추상 메서드에서 담당.
- DiscountPolicy를 상속하는 하위 클래스에서 실적적인 구현을 담당한다. (템플릿 메서드 패턴)
할인조건
할인 정책
하나의 영화에는 1개의 정책 / 할인조건은 여러 개가 선택 가능하다.
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 class AmountDiscountPolicy extends DiscountPolicy {
private Money discountAmount;
public AmountDiscountPolicy(Money discountAmount, DiscountCondition... conditions) {
super(conditions);
this.discountAmount = discountAmount;
}
컴파일 시간 의존성과 실행시간 의존성
Movie avator = new Movie("아바타"
Duration.ofMunutes(120),
Money.wons(10000),
new AmountDiscountPolicy(Money.wons(800),...));
Movie avator = new Movie("아바타"
Duration.ofMunutes(120),
Money.wons(10000),
new PercentDiscountPolicy(0.1,...));
현재 Movie 클래스의 코드만 살펴보는 것으로 인스턴스가 어떤 객체에 의존하는지 알 수 없다.
(의존하고 있는 대상이 DiscounPolicy와 동일 한 타입이라는 것뿐)
의존하고 있는 객체의 정확한 타입을 알기 위해선 의존성을 연결하는 부분(위와 같이 Movie의 인스턴스를 생성하는 부분)을 찾아야 한다.
코드의 의존성과 실행 시점의 의존성은 서로 다를 수 있다.
코드 의존성과 실행 시점의 의존성이 다르면 발생하는 부작용
1. 코드가 복잡해지고 디버깅이 어려워진다.
2. 확장하기 쉽고 유연 해지는 코드가 완성된다.
차이에 의한 프로그래밍
클래스를 추가하고 싶을 때 기존의 어떤 클래스와 유사한 경우
상속을 이용하면 코드를 재사용하면 된다.
ex) DiscountPolicy에 정의된 모든 속성과 메서드를 그대로 물려받는 AmountDiscountPolicy, PercentDiscountPolicy
부모 클래스와 다른 점을 추가해서 클래스를 쉽고 빠르게 변경하는 것 (상속을 이용)
다형성
그렇다면! 어떠한 메서드가 사용되는가?
협력하는 객체의 실제 클래스가 무엇인지에 따라 달라진다.
이를 다형성이라 부른다.
즉 다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 행동하는 것이다.
(동일한 인터페이스에 한해서)
다형성 구현 방법 : 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다.
- 메시지와 메서드를 실행 시점에 바인딩 - 동적 바인딩
동적 바인딩 : 컴파일 시점이 아닌 실행 시점에 실행할 메서드를 결정
정적 바인딩 : 컴파일 시점에 실행할 메서드 결정
상속의 종류
- 구현 상속(implementation inheritance) : 코드를 재사용할 목적으로 사용
- 인터페이스 상속(interface inheritance) : 다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유하는 목적으로 사용
추상 클래스 대신 인터페이스를 사용하는 이유
: 구현에 대한 고려 없이 단순히 협력에 참여하는 클래스들이 공유하는 외부 인터페이스를 정의하기 위한 목적으로 사용하기 위해서
추상화의 힘
금액 할인 정책 / 두 개의 순서 조건, 한 개의 기간 조건을 포괄한다.
추상화의 장점
1. 세부사항을 포괄하여 한 문장으로 설명이 가능하다
- 요구사항의 정책을 높은 수준에서 서술할 수 있다.
- 세부사항에 억눌리지 않고 상위 개념만으로도 도메인의 중요한 개념을 설명 가능
2. 유연한 설계가 가능하다.
- 추상화를 통해서 상위 정책을 표현하면 기존 구조를 수정하지 않고 새로운 기능을 쉽게 추가 혹은 확장할 수 있다.
할인 정책에 포함되지 않는 영화의 경우엔 어떻게 처리할까?
기존의 할인 정책은 금액을 계산하는 책임이 DiscountPolicy의 자식 클래스에 있었다.
하지만 할인 정책이 없는 경우는 할인 금액이 0원이라는 결정하는 책임이 Movie에 있다.
Movie를 수정하지 않고 일관성을 유지하는 방법으로 확장
문제점
NoneDiscountPolicy가 할인과는 상관없음에도 불구하고 getDiscountAmount를 구현하고 있다.
이는 NoneDiscountPolicy와 DiscountPolicy를 개념적으로 결합시킨다.
해결방법
1. 인터페이스를 하나 두고 calculateDiscountAmount()를 만든다.
2. NoneDiscountPolicy와 DiscountPolicy는 이를 구현한다.
상속은 항상 좋은 것일까?
상속의 문제점
- 캡슐화의 문제 : 자식의 부모에 대해서 너무 잘 알고 있다.
- 유연한 설계 불가능 : 컴파일 시점에서 부모와 자식과의 관계가 설정되므로 실행 시점에 변경이 불가능하다.
- 합성은 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화 가능
- 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 유연한 설계 또한 가능
참고문헌
|
'Java > 객체지향' 카테고리의 다른 글
(OOP) 리스코프 치환 원칙 (0) | 2020.02.20 |
---|---|
(OOP) 객체지향에 대해서(2) - 은유 (0) | 2020.02.20 |
(OOP) 객체지향에 대해서(1) - 객체, 행동, 상태 (0) | 2020.02.19 |
(OOP) 역할, 책임, 협력 (0) | 2019.12.30 |
(OOP) 객체, 설계 (0) | 2019.12.29 |