728x90
상속과 다형성
상속
- 코드를 재사용하기 위해 가장 널리 사용되는 방법
- 클래스 사이에 관계를 설정하는 것만으로도 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있다.
- 부모 클래스가 제공하고 있는 모든 인터페이스를 자식 클래스가 물려받을 수 있다.
- 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메세지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
- 상속은 두 클래스의 인터페이스를 통일하기 위해 사용된 구현 방법
- 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶는다.
- 추가할 클래스가 기존의 어떤 클래스와 매우 흡사할 경우
- 클래스의 코드를 재사용하는 방법 -> 상속
업캐스팅
: 자식 클래스가 부모 클래스를 대신하는 것
구현 상속과 인터페이스 상속
- 상속은 구현 상속과 인터페이스 상속으로 분류할 수 있다.
- 구현 상속 ❌
- 코드를 재사용하기 위한 목적으로 상속 사용
public class Stack extends ArrayList<String> {
public void push(String element) {
super.add(element);
}
public String pop() {
return super.remove(size() - 1);
}
}
ArrayList
의 코드를 재사용하기 위해 상속하는 예시Stack
은ArrayList
가 아닌데 IS-A 관계를 만들어버림ArrayList
의 모든 메서드(add, remove 등)가 Stack에 노출됨- Stack이 사용하지 않는 메서드도 노출된다.
인터페이스 상속 ⭕
- 다형적인 협력을 위해 부모 클래스에 자식 클래스가 인터페이스를 공유할 수 있도록 상속 사용
/** 기본 할인 정책. */
public abstract class DefaultDiscountPolicy implements DiscountPolicy {
/** 하나의 할인 정책은 여러 개의 할인 조건을 포함할 수 있다. */
private List<DiscountCondition> conditions = new ArrayList<>();
public DefaultDiscountPolicy(DiscountCondition... conditions) {
this.conditions = Arrays.asList(conditions);
}
/**
* 할인 금액을 계산한다.
* <p>
* Template Method.
*
* @param screening
* @return
*/
@Override
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);
}
// 구현체
/**
* 금액 할인 정책.
* <p>
* 조건을 만족할 경우 일정 금액을 할인해주는 정책.
*/
public class AmountDiscountPolicy extends DefaultDiscountPolicy {
/** 할인 요금. */
private Money discountAmount;
public AmountDiscountPolicy(Money discountAmount, DiscountCondition... conditions) {
super(conditions);
this.discountAmount = discountAmount;
}
@Override
protected Money getDiscountAmount(Screening screening) {
return discountAmount;
}
}
구현이 아닌 역할을 상속하자.
상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다.
- 단순 코드 재사용을 목적으로 하지 말고, 다형성(협력 관계에서 역할을 정의하기 위해)을 위해 상속을 사용해야한다.
- 상속의 "주된 목적"이 코드 재사용이 되어서는 안 된다.
- 부모 클래스의 코드가 수정되면 자식 클래스의 코드도 함께 수정되기 때문
- 이 경우에는 상속이 아닌 합성을 사용하자.
- 상속의 핵심 목적은 항상 "다형성을 통한 확장" 이어야 한다.
다형성
- 동일한 메세지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메세지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다.
추상 클래스 vs 인터페이스
- 추상 클래스 : 일부 구현 공유 필요
- 공통 로직(할인 조건 확인) 구현
- 자식 클래스에서 재사용
// 공통 구현이 필요한 할인 정책의 추상 클래스
public abstract class DefaultDiscountPolicy {
// 여러 할인 정책이 공유하는 공통 구현
private List<DiscountCondition> conditions = new ArrayList<>();
// 템플릿 메서드 - 할인 계산의 골격을 정의
public Money calculateDiscount(Screening screening) {
for (DiscountCondition condition : conditions) {
if (condition.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
// 자식 클래스가 구현해야 하는 추상 메서드
protected abstract Money getDiscountAmount(Screening screening);
}
// 추상 클래스 상속
public class AmountDiscountPolicy extends DefaultDiscountPolicy {
private Money discountAmount;
@Override
protected Money getDiscountAmount(Screening screening) {
return discountAmount;
}
}
- 인터페이스 : 단순 규약(설명서, 인터페이스) 공유
- 단순히 계약(할인 조건 검사)만 정의
/** 할인 조건. */
public interface DiscountCondition {
/**
* 전달된 상영 정보가 할인 조건을 만족시키면 true 를 리턴한다.
*
* @param screening 상영 정보
* @return 할인 조건을 만족했다면 true
*/
boolean isSatisfiedBy(Screening screening);
}
/** 순번 기준 할인 조건. */
public class SequenceCondition implements DiscountCondition {
/** 순번. */
private int sequence;
public SequenceCondition(int sequence) {
this.sequence = sequence;
}
@Override
public boolean isSatisfiedBy(Screening screening) {
return screening.isSequence(sequence);
}
}
// 기간 조건 구현
public class PeriodCondition implements DiscountCondition {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
@Override
public boolean isSatisfiedBy(Screening screening) {
return screening.getWhenScreened().getDayOfWeek().equals(dayOfWeek) &&
!screening.getWhenScreened().toLocalTime().isBefore(startTime) &&
!screening.getWhenScreened().toLocalTime().isAfter(endTime);
}
}
상속보다는 합성을 사용하라
상속의 문제점
- 상속은 캡슐화를 위반한다.
- 상속을 이용하기 위해서는 부모 클래스의 구조를 잘 알고 있어야 한다.
- 부모 클래스의 어떠한 추상 메서드를 구현해야한다는 사실을 알고 있어야 한다.
- 부모 클래스를 변경할 때 자식 클래스의 메서드도 변경되어야하므로 결합도가 높다.
- 설계를 유연하지 못하게 만든다.
- 상속은 부모 클래스와 자식 클래스의 관계를 컴파일 시점에 결정한다.
- 다른 구현체로 변경해야한다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy 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 calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
// 사용 예시
public class MovieExample {
public void createMovies() {
Movie avatarMovie = new Movie(
"아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(Money.wons(1000))
);
Movie marvelMovie = new Movie(
"어벤져스",
Duration.ofMinutes(180),
Money.wons(12000),
new PercentDiscountPolicy(0.1)
);
// 할인 없는 영화
Movie artMovie = new Mo
합성
- 인터페이스에 정의된 메세지를 통해서만 코드를 재사용하는 방법
- 다른 인스턴스로 변경하기 위해 단순히 새로운 인스턴스로 연결하면 된다.
- 느슨한 결합을 수행한다.
/** 영화. */
public class Movie {
private String title;
private Duration runningTime;
/** 기본 요금. */
@Getter
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;
}
/**
* 할인된 금액을 계산해 리턴한다.
*
* @param screening 상영 정보
* @return 할인된 금액
*/
public Money calculateMovieFee(Screening screening) {
if (discountPolicy == null) {
return fee;
}
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
/**
* 실행 시점에 할인 정책을 변경한다.
*
* @param discountPolicy 변경할 할인 정책.
*/
public void changeDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
상속보다 합성을 이용해 관계를 연결한 원래의 설계가 더 유연하다.
상속 이용
// 상속을 사용한 나쁜 예
class Stack extends ArrayList {
public void push(Object item) {
add(item);
}
public Object pop() {
return remove(size() - 1);
}
}
합성 이용
// 합성을 사용한 좋은 예
class Stack {
private List list = new ArrayList(); // 합성
public void push(Object item) {
list.add(item);
}
public Object pop() {
return list.remove(list.size() - 1);
}
public void change(){
list = new LinkedList<>();
}
}
대부분의 설계에서는 상속과 합성을 함께 사용한다.
- 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용해야한다.
- Movie - DiscountPolicy : 합성 관계
- DiscountPolicy, AmountPolicy, PercentDiscountPolicy : 상속 관계
- 코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳다.
- 새로운 할인 정책을 쉽게 추가할 수 있고 (상속) + Movie는 어떤 할인 정책이 적용되는지 몰라도 된다. (합성)
// 할인 정책 인터페이스
interface DiscountPolicy {
int calculateDiscount(Movie movie);
}
// 정액 할인 정책 구현
class AmountDiscountPolicy implements DiscountPolicy { // 상속(인터페이스 구현)
private int discountAmount; // 합성
public int calculateDiscount(Movie movie) {
return discountAmount;
}
}
// 비율 할인 정책 구현
class PercentDiscountPolicy implements DiscountPolicy { // 상속(인터페이스 구현)
private double discountPercent; // 합성
public int calculateDiscount(Movie movie) {
return (int)(movie.getPrice() * discountPercent);
}
}
// Movie 클래스
class Movie {
private DiscountPolicy discountPolicy; // 합성
public int calculateMoviePrice() {
return getPrice() - discountPolicy.calculateDiscount(this);
}
}
- Movie는 DiscountPolicy를 합성으로 가지고 있어 유연하게 정책을 변경한다.
- 각 할인 정책들은 DiscountPolicy 인터페이스를 상속(구현)하여 다형성을 제공한다.
- 대부분 이렇게 상속과 합성을 함께 사용한다.
- 새로운 할인 정책을 쉽게 추가 : 상속
- Movie는 어떤 할인 정책이 적용되는지 몰라도 됨 : 합성
Reference
https://wikibook.co.kr/object/
https://github.com/johngrib/study-objects
728x90
'설계' 카테고리의 다른 글
인터페이스, 추상 클래스, 합성 사용 시점 (6) | 2024.12.02 |
---|---|
[오브젝트] 객체 지향 설계 (0) | 2024.12.01 |
[설계] Okky ERD 분석하기 (0) | 2024.04.26 |