클래스를 재사용하기 위해 새로운 클래스를 추가하는 가장 대표적인 방법 - 상속
새로운 클래스의 인스턴스 안에 기존 클래스의 인스턴스를 포함시키는 방법 - 합성
01 상속과 중복 코드
DRY원칙(Don't Repeat Yourself)
요구사항이 변경됐을때 두 코드를 함께 수정해야 한다면 이 코드는 중복이다.
수정과 테스트에 드는 비용을 증가시킬뿐
코드가 변경에 반응하는 방식이 중요하다.
중복과 변경
중복코드 살펴보기
ex) 한달에 한번씩 가입자별 전화 요금을 계산하는 애플리케이션
개별 통화기간을 저장하는 Call 클라스
Call은 통화 시작 시간(from)과 통화 종료 시간(to)을 인스턴스 변수로 포함
Call 의 목록을 관리할 정보 전문가 Phone
단위요금을 저장하는 amount 단위시간을 저장하는 seconds
전체 통화 몰록을 저장하는 리스트 calls
calculateFee 메서드는 amount, seconds, calls를 이용해 전체 통화요금을 계산
밤10시 이후 통화에 대해 할인된 심야 할인 요금제를 추가하는 요구사항
Phone의 코드를 복사해서 NightlyDiscountPhone을 추가하는 방법은 요구사항을 아주 짧은 시간에 구현할 수 있게 해줌.
But, 중복코드가 존재하여 변경시 비용이 큼.
중복코드 수정하기
새로운 요구사항을 추가해보자
통화요금에 부과할 세금을 계산하는 것.
가입자의 핸드폰별 세율이 달라야 하기 때문에 Phone 은 세율을 저장할 인스턴스 변수 taxRate를 포함해야 함.
taxRate의 값을 이용해 통화요금에 세금을 부과하도록 Phone의 calculate메서드를 수정.
NightlyDiscountPhone 도 동일한 방식으로 수정
중복코드를 모두 식별해서 수정하기 힘들기도 하고
수정된 코드가 서로다를 가능성이 많다.
타입코드 사용하기
중복코드를 제거하는 한가지 방법 - 클래스 합치기
요금제를 구분하는 타입코드를 추가하고 타입코드 값에 따라 로직을 분기시켜 Phone과 NightlyDiscountPhone을 합칠수 있음.
But, 타입코드를 사용하는 클래스는 낮은 응집도와 높은 결합도라는 문제에 시달리게 됨.
상속을 이용해서 중복코드 제거하기
NightlyDiscountPhone클래스가 Phone클래스를 상속받게 하여 코드를 재사용 하자.
super 참조를 통해 부모 클래스인 Phone의 calculateFee 메서드를 호출해서 일반 요금제에 따라 통화요금을 계산한 후 이값에서 통화 시작 시간이 10시 이후인 통화의 요금을 빼줌.
상속을 염두에 두고 설계되지 않은 클래스를 상속을 이용해 재사용하는 것은 쉽지않음.
자식클래스의 작성자가 부모클래스의 구현 방법에 대한 정확한 지식을 가져야 함.
상속이 초래하는 부모클래스와 자식클래스 사이의 강한 결합이 코드를 수정하기 어렵게 만듬.
강하게 결합된 Phone 과 NightlyDiscountPhone
코드중복을 제거하기 위해 상속을 사용했음에도 세금을 계산하는 로직을 추가하기 위해 새로운 중복코드를 만들었다.
이것은 NightlyDiscountPhone이 Phone의 구현에 너무 강하게 결합돼 있기 때문에 발생하는 문제
경고1
자식클래스의 메서드 안에서 super 참조를 이용해 부모클래스의 메서드를 직접 호출할 경우 두 클래스는 강하게 결합된다.
super 호출을 제거할 수 있는 방법을 찾아 결합도를 제거하라.
02 취약한 기반클라스 문제
부모클래스의 변경에 의해 자식 클래스가 영향을 받는 현상
-> 취약한 기반 클래스 문제(Fragile Base Class Problem, Brittl Base Class Problem)
객체지향 프로그래밍의 근본적인 취약성, 결합도가 초래하는 문제점
불필요한 인터페이스 상속 문제
ex)
자바 초기버전에서 상속을 잘못 사용한 대표적인 사례
java.util.Properties , java.util.Stack
초기 프레임워크 개발자들은 요소의 추가, 삭제, 오퍼레이션을 제공하는 Vector를 재사용하기 위해
Stack을 Vector의 자식클래스로 구현했다.
Hashtable을 상속받는 Properties클래스는 키와 값의 타입으로 오직 스트링만 가질수 있으나, 인터페이스에 포함된 put메서드를 이용하면 String 타입 이외의 키와 값이라도 저장할 수 있어 문제가 생긴다.
단순히 코드를 재사용하기 위해 불필요한 오퍼레이션이 인터페이스에 스며들도록 방치해서는 안된다.
경고2
상속받은 부모클래스의 메서드가 자식 클래스의 내부구조에 대한 규칙을 깨트릴 수 있다.
메서드 오버라이딩의 오작용 문제
ex)
HashSet의 구현에 강하게 결합된 InstrumentedHashSet
addCount를 먼저 증가시킨후 super 참조를 이용해 부모 클래스 메서드를 호출해서 요소를 추가
상속받은 addAll() 메서드를 이용할 경우 카운트 수가 중복되어 계산되거나,
나중에 부모클래스가 수정된다고 할경우 추가되는 요소가 누락되어 계산될 가능성이 있는 코드.
나중의 수정까지 감안해 코드를 중복 구현하는 방법으로만 문제를 해결할 수 있음.
경고3
자식클래스가 부모클래스의 메서드를 오버라이팅할 경우 부모 클래스가 자신의 메서드를 사용하는 방법에 자식클래스가 결합될 수 있다.
조슈아 블로치는 상속을 위해 클래스의 내부 구현을 문서화해야한다고 주장함.
상속은 코드 재사용을 위해 캡슐화를 희생한다.
완벽한 캡슐화를 원한다면 코드 재사용을 포기하거나 상속 이외의 다른방법을 사용해야함.
부모클래스와 자식클래스의 동시 수정 문제
ex)
음악목록을 추가할 수 있는 플레이리스트를 구현
음악정보를 저장할 Song클래스 - 인스턴스 변수 singer(가수의 이름), title(노래제목),
음악목록을 저장할 Playlist 클래스 -인스턴스 변수 tracks(곡 리스트), append 메서드 (노래추가)
Playlist를 상속받은 PersonalPlaylist - remove 메서드 (노래삭제)
요구사항이 변경되어 Playlist 노래목록 뿐 아니라 가수별 노래의 제목도 함께 관리해야한다.
Playlist append 메서드 수정.
PersonalPlaylist의 remove 메서드로 함께 수정되어야 한다.
코드의 재사용을 위한 상속은 클래스끼리 강하게 결합되기 때문에 함께 수정되어야하는 일이 빈번함.
경고4
클래스를 상속하면 겹합도로 인해 자식 클래스와 부모 클래스의 구현을 영원히 변경하지 않거나,
자식클래스와 부모클래스를 동시에 변경하거나 둘 중 하나를 선택할 수 밖에 없다.
03 Phone 다시 살펴보기
추상화에 의존하자
코드중복을 제거하기 위해 상속을 도입할 때 따르는 두가지 원칙
|
차이를 메서드로 추출하라
"변하는 것으로 부터 변하지 않는 것을 분리하라"
ex)
NightlyDiscountPhone 과 Phone 클래스의 메서드에서 다른 부분을 별도의 메서드로 추출 하자.
(Call에 대한 통화요금을 계산하는 것)
다른 부분을 동일한 이름을 가진 메서드로 추출. calculateCallFee
두 클래스의 calculateFee메서드는 완전히 동일해졌고
추출한 calculateCallFee 메서드 안에 서로 다른 부분을 격리시켜 놓음.
중복코드를 부모 클래스로 올려라
목표는 모든 클래스들이 추상화에 의존하도록 만드는 것.
새로운 부모클래스인 AbstractPhone 클래스 만듬.
완전히 동일한 메서드를 부모추상클래스로 이동시키고 자식클래스에서 제거.
인스턴스 변수 calls 또한 부모추상클래스로 이동.
시그니처는 동일하지만 내부구현이 다른경우
자식클래스의 메서드 구현은 그대로 두고 시그니처만 부모클래스로 이동.
calculateCallFee메서드를 추상메서드로 선언하고 자식클래스에서 오버라이팅 할 수 있도록 protected로 선언.
결과
|
추상화가 핵심이다
AbstractPhone 은 전체 통화 목록을 계산하는 방법이 바뀔 경우에만 변경
Phone과 NightlyDiscountPhone 은 각자의 요금제 계산하는 방식이 바뀔경우에만 변경됨.
새로운 요금제를 추가하려면 새로운 클래스를 만들고 추상메서드를 상속 받아 calculateCallFee 메서드만 오버라이딩하면 됨.
이 설계는 낮은 결합도를 유지한다.
확장에는 열려있고 수정에는 닫여 있어 개방-폐쇄 원칙 준수
의도를 드러내는 이름 선택하기
아쉬운점 - 적절한 클래스 이름
요금제와 관련된 내용을 구현한다는 사실을 명시적으로 전달하게 하자.
public abstract class Phone {...}
public class RegularPhone extends Phone {...}
public class NightlyDiscountPhone extends Phone {...}
세금 추가하기
통화요금에 세금을 부과하는 새로운 요구사항이 추가된다면?
모든 요금제에 공통으로 부과되는 요구사항이므로
Phone 에 인스턴스 변수 taxRate추가. 두 인스턴스 변수의 값을 추기화하는 생성자를 추가
자식클래스의 생성자 역시 taxRate을 초기화하기 위해 수정
...
public RegularPhone (Money amount, Duration seconds, double taxRate) {
super(taxRate);
this.amount = amount;
this.seconds = seconds;
}
...
인스턴스 변수의 추가는 상속계층 전반에 걸친 변경을 유발함.
04 차이에 의한 프로그래밍(programming by difference)
상속을 이용하면 이미 존재하는 클래스를 기반으로 다른 부분을 구현, 새로운 기능을 빠르게 추가 확장 가능.
중복코드제거 - 코드 재사용
새로운 기능을 추가하기 위해 직접 구현해야하는 코드의 양을 줄일 수 있다.
다음 챕터에서 상속의 단점을 피하면서도 코드를 재사용할 수 있는 다른 방법 - 합성 을 살펴보자.
'IT Book Summary > Object: 객체지향설계' 카테고리의 다른 글
Chapter 12 다형성 (0) | 2020.01.07 |
---|---|
Chapter 11 합성과 유연한 설계 (0) | 2020.01.01 |
Chapter 09 유연한 설계 (0) | 2019.12.18 |
Chapter 08 의존성 관리하기 (0) | 2019.12.11 |
Chapter 07 객체분해 (0) | 2019.12.04 |