[오브젝트 - 5 ] 상속과 상속의 문제점, 합성
이미 존재하는 클래스와 유사한 클래스가 필요 하다면
코드를 복사하지 말고 상속을 이용해 재사용하면 된다.
상속과 결합도
코드를 쉽게 재사용할수 있는 상속은 좋은 방법일까?
자식 클래스 작성자는 부모클래스 구현 방법에 대한 정확한 지식을 가져야한다.
따라서 상속은 자식과 부모 클래스의 결합도를 높인다.
부모의 변경에 자식은 취약하다.
또한 불필요한 인터페이스를 상속 받게되는 문제가 생기게 되며
오버라이드 함수가 오작동할수있다.
override func add<T>(t: T) {
addCount += 1
return super.add(t)
}
override func addAll<T>(t: [T]) {
addCount += t.count
return super.addAll(t)
}
super.add() 내부에서 다른 메소드를 호출하는 증 의도치않은 작업을 할수있음
상속은 객체지향에 반대된다.
오버라이딩 메소드가 다른 메소드를 호출하는지 어떤 순서인지 어떤 영향을 주는지 등등 상속을 쓰고싶다면 클래스를 문서화 해야하한다. 하지만 문서화도 캡슐화를 위반하는것이다.
코드 재사용을 위해서는 캡슐화를 희생해야한다.
class PlayList {
private var tracks: [Song] = []
func append(song: Song) {
getTracks().append(song)
}
func getTracks() {
return tracks
}
}
class PersonalPlayList: List {
func remove(song: Song) {
getTracks().remove(song)
}
}
위와 같은 상황에서 노래 뿐만 아니라 가수별 노래 둘다 관리 해야하는 요구사항이 주어졌다 보자
그러면 PlayList 에서 Dictionary 타입의 변수가 필요할것이고
PlayList, PersonalPlayList 둘다 변경되어야한다.
추상화에 의존하기
이를 위해 해결하는 방법은 부모클래스의 구현클래스가 아닌
추상클래스를 만들어 부모와 자식이 의존하게 하는방법이 있다.
“변하는것에서 변하지 않는것을 분리하라”
“변하는 부분을 찾고 캡슐화하라”
- 변하지 않는것을 분리하라
class Phone {
private let amount: Money
private let duraion: Duration
private let calls: [call]
func calculateFee() -> Int {
var result = 0
for call in calls {
result += (call.getSeconds() / duration.getSeconds() ) * amount
}
return result
}
}
class NightPhone {
private let amount: Money
private let nightAmount: Money
private let duraion: Duration
private let calls: [call]
func calculateFee() -> Int {
var result = 0
for call in calls {
if call.isNight() {
result += (call.getSeconds() / duration.getSeconds() ) * nightAmount
}else {
result += (call.getSeconds() / duration.getSeconds() ) * amount
}
}
return result
}
}
위의 경우는 캡슐화를 위해
상속을 피하고 중복코드를 사용한 경우이다.
Phone, NightPhone은 각각 비슷하지만 조금 다른 요금계산을 하고있다.
여기서 먼저 두 클래스의 다른 부분을 메소드로추출해보자
class NightPhone {
private let amount: Money
private let nightAmount: Money
private let duraion: Duration
private let calls: [call]
func calculateFee() -> Int {
var result = 0
for call in calls {
result += calculateCallFee(call:call)
}
return result
}
func calculateCallFee(call: Call) -> Int {
if call.isNight() {
return (call.getSeconds() / duration.getSeconds() ) * nightAmount
}else {
return (call.getSeconds() / duration.getSeconds() ) * amount
}
}
}
위와같이 다른부분을 뺴서 메소드로 만들면 기존에있던 calculateFee는 두 클래스 모두 동일해진다.
2. 중복코드를 부모클래스로 올려라
이때 이 부모를 추상클래스로 구현한다
class AbstractPhone {
private let calls = [Call]
func calculateFee() -> Int{
var result = 0
for call in calls {
result += calculateCallFee(call:call)
}
return result
}
//swift 는 protected 접근이 없어 추상 메소드 불가
//protocol을 쓰자니 구현 메소드 불가
func calculateCallFee(){
fatalError("Must Overried calculateCallFee")
}
}
그리고 자식 클래스에서는 다른부분을 구현하면 된다
class NightPhone: AbstractPhone {
private let amount: Money
private let nightAmount: Money
override func calculateCallFee() {
if call.isNight() {
return (call.getSeconds() / duration.getSeconds() ) * nightAmount
}else {
return (call.getSeconds() / duration.getSeconds() ) * amount
}
}
}
합성
상속은 부모클래스와 자식클래스를 연결해서 부모클래스를 재사용하지만
합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분객체의 코드를 재사용한다.
상속은 부모클래스의 코드를 쉽게 재사용할수 있지만 부모 자식 클래스간 강한 결합을 만든다.
합성은 퍼블릭 인터페이스 코드를 재사용 하고 퍼블릭 인터페이스에 의존한다.
따라서 객체 내부의 코드가 변경되어도 영향을 최소화할수 있다.
만약 위의 핸드폰 요금 코드에서 상속을 이용하여
기본 요금에 추가적으로
세금부과 , 요금할인에 대한 새로운 기능을 추가해보면 이렇게 된다.
class AbstractPhone {
private let calls = [Call]
func calculateFee() -> Int{
var result = 0
for call in calls {
result += calculateCallFee(call:call)
}
return result
}
//swift 는 protected 접근이 없어 추상 메소드 불가
//protocol을 쓰자니 구현 메소드 불가
func calculateCallFee(){
fatalError("Must Overried calculateCallFee")
}
func afterCalculated(fee:Money) -> Money {
return fee
}
}
class NightPhone: AbstractPhone {
private let amount: Money
private let nightAmount: Money
override func calculateCallFee() {
if call.isNight() {
return (call.getSeconds() / duration.getSeconds() ) * nightAmount
}else {
return (call.getSeconds() / duration.getSeconds() ) * amount
}
}
override func afterCalculated(fee:Money) -> Money {
return fee
}
}
class TaxableNightPhone: NightPhone {
private let taxRate: Double
override func afterCaculated(fee:Money) -> Money {
return fee + fee * taxRate
}
}
class RateDiscountableNightPhone: NightPhone {
private let discountAmount: Money
override func afterCaculated(fee:Money) -> Money {
return fee - discountAmount
}
}
여기서 우선 NightPhone - RegularPhone(위 코드에선 생략…)
의 하위 클래스의 afterCaculated 함수 구현 부분에서 중복 코드가 발생한다.
class TaxableRegularPhone: RegularPhone {
private let taxRate: Double
override func afterCaculated(fee:Money) -> Money {
return fee + fee * taxRate
}
}
class RateDiscountableRegularPhone: RegularPhone {
private let discountAmount: Money
override func afterCaculated(fee:Money) -> Money {
return fee - discountAmount
}
}
그리고 만약 이제 세금과 할인을 동시 적용한다면
또 세금과 할인 적용의 순서를 바꿀수있다면
수많은 클래스가 생기고
수많은 중복이 발생한다.
상속이 아닌 합성을 이용하면 컴파일 타임이 아닌 런타임에 객체간 관계를 형성하므로 문제를 해결할수 있다.
상속을 쓰면 컴파일타임 의존성 = 런타임 의존성 이 형성된다.
이 둘이 멀어질수록 설계가 유연해진다.
합성으로 리팩토링
우선 먼저 각 정책을 별도 클래스로 구현하는것이다.
protocol RatePolicy {
func calculateFee(phone: Phone)
}
기본정책이다
public class BasicRatePolicy: RatePolicy {
func calculateFee(phone:Phone) -> Money {
var result = Money.zero
for call in phone.getCalls() {
result += caculcateCallFee(call)
}
return result
}
//swift 는 protected 접근이 없어 추상 메소드 불가
//protocol을 쓰자니 구현 메소드 불가
func calculateCallFee(call: Call){
fatalError("Must Overried calculateCallFee")
}
}
기본정책은 이러하고 이제 자식클래스들이 calculateCallFee를 구현해야한다.
public final class RegularPolicy: BasicRatePolicy {
private let money: Money
private let seconds: Duration
/// 몇초당 얼마
init(money:Money, seconds: Duration) {
self.money = money
self.seconds = seconds
}
override func calculateCallFee(call: Call){
let calcaulateTime = call.getDuration().getSeconds() / seconds.getSeconds()
return money * calculateTime
}
}
public final class NightPolicy: BasicRatePolicy {
private let LATE_TIME = 22
private let money: Money
private let nightMoney:Money: Money
private let seconds: Duration
/// 몇초당 얼마
init(money:Money, nightMoney:Money, seconds: Duration) {
self.money = money
self.nightMoney= nightMoney
self.seconds = seconds
}
override func calculateCallFee(call: Call){
let calcaulateTime = call.getDuration().getSeconds() / seconds.getSeconds()
if call.getHour() >= LATE_TIME {
return nightMoney * calculateTime
}
return money * calculateTime
}
}
이렇게 각각 자식클래스에 caculateCallFee 를 구현했다.
이제 정책은 다 정해졌고 이 정책을 이용해 요금을 계산하도록 Phone을 수정한다.
class Phone {
private let ratePolicy: RatePolicy
private let calls: [call]
init(ratePolicy:RatePolicy) { self.ratePolicy = ratePolicy }
func calculateFee() -> Int {
return ratePolicy.caculateFee(phone:self)
}
}
Phone 내부에 정책을 포함했다. 이것을 합성이라 한다.
요금정책의 인터페이스인 RatePolicy로 정의되어있다.
생성자를 통해 주입받으므로 런타임의존성이 된다.
이렇게 Phone 처럼 다양한 객체와 협력하기 위해서는
합성할 객체 타입을 인터페이스를 쓰면된다.
여기까지 차이점은
NightPhone - RegularPhone 클래스가 사라졌고
Phone 만 존재하며
없었던 RatePolicy 와 그 자식 클래스들이 생겼다.
부가정책 추가하기
우선 알아야할것은 기본요금 계산 이후에 부가 정책( 세금 + 요금할인) 이 들어간다는 것이다.
부가정책(TaxablePolicy , RateDiscountPolicy) 들은 다른 부가정책, 기본정책 인스턴스를
참조해야한다.
Phone → TaxablePolicy → DiscountPolicy → RegularPolicy
/// RateDiscountPolicy 클래스 내부
let regularMoney = regularPolicy.caculateFee()
let taxedMoney = taxablePolicy.caculateFee(regularMoney)
return discountPolicy.caculateFee(taxedMoney)
Phone 의 입장에서는 인터페이스인 RatePolicy 를 가지고있다.
런타임에서 해당 Policy가 결정되므로
TaxablePolicy ,DiscountPolicy , RegularPolicy 모두
RatePolicy 를 구현한 형태여야 한다.
public class AdditionalPolicy: RatePolicy {
private let next: RatePolicy
init(next: RatePolicy) {
self.next = next
}
override func caculateFee(phone: Phone) {
let money = next.caculateFee(phone)
return afterCaculatedFee(money)
}
//swift 는 protected 접근이 없어 추상 메소드 불가
//protocol을 쓰자니 구현 메소드 불가
func afterCaculatedFee(money: Money) -> Money {
fatalError("Must Overried afterCaculatedFee")
}
}
next 이름의 변수는 기본 정책을 의미한다.
기본정책에서 기본 요금을 받아온 이후에
이제 부가 정책을 더할것이고 afterCaculatedFee 에서 구현될것이다.
public class TaxablePolicy: AdditionalPolicy {
private let taxRatio: Double
init(taxRatio: Double, next:RatePolicy) {
super.init(next)
self.taxRatio = taxRatio
}
override func afterCaculatedFee(money: Money) -> Money {
return money + money * taxRatio
}
}
public class DiscountablePolicy: AdditionalPolicy {
private let discount: Money
init(discount: Money, next:RatePolicy) {
super.init(next)
self.discount = discount
}
override func afterCaculatedFee(money: Money) -> Money {
return money - discount
}
}
이제 모든 설계가 끝났고 원하는대로 조합하면 된다.
///일반요금 + 할인요금 + 세금
Phone(ratePolicy : TaxablePolicy(taxRatio:0.1, next:DiscountablePolicy(money:Money(2000),next: RagularPolicy(...)
이제 앞으로 새로운 정책을 추가할때
기본정책이던
부가 정책이던
그에맞춘 구현클래스를 만들면 해결이 가능하다.
요구사항이 변경되면 관련한 구현 클래스 하나만 변경하면 된다.