✌️ Chapter 6: 상속을 이용해 새로운 행동 얻기
📚 고전적 상속 이해하기
- 상속이란 자동화된 메시지 전달시스템이다.
- 상속 시스템은 객체가 이해하지 못한 메시지를 어디로 전달해야 하는지를 정의한다. 특정 객체가 이해할 수 없는 메시지를 전달받았을 경우 그 객체는 이 메시지를 다른 객체에게 전달한다. 이런 전달의 관계를 만드는 것이 상속이다.
- 고전적인 상속 관계는 하위클래스(subclasses)를 만드는 것을 통해 정의된다. 메시지는 하위클래스에서 상위클래스로 전달된다.
📚 상속을 사용해야 하는 지점을 알기
🎈 구체 클래스에서 시작하기
- 예를 들어본다.
- 아래 코드는 Bicycle 클래스고 모든 로드 자전거는 이 클래스의 인스턴스이다.
class Bicycle
attr_reader :size, :tape_color
def initialize(args)
@size = args[:size]
@tape_color = args[:tape_color]
end
# 모든 자전거가 동일한 크기의 타이어와 체인을 기본값으로 갖는다.
def spares
{
chain: '10-speed',
tire_size: '23',
tape_color: tape_color
}
end
# 다른 메서드들..
end
bike = Bicycle.new(
size: 'M',
tape_color: 'red'
)
bike.size # M
bike.spares
# { :tire_size => '23', :chain => '10-speed' :tape_color => 'red'}
- 이 클래스에서 요구사항이 들어왔다. 디자인의 목표는 마운틴 자전거를 지원하도록 하는 것이다.
- 마운틴 자전거에는 서스펜션이 필요하다.
🎈 자전거 종류 추가하기
- 아래 코드의
spares
메서드에if
문을 포함하게 되었다. 이 예시는 안티패턴을 보여주기 위한 간단한 우회로에 불과하다. (문제가 많은 패턴)
class Bicycle
attr_reader :style, :size, :tape_color,
:front_shock, :rear_shock
def initialize(args)
@type = args[:style]
@size = args[:size]
@tape_color = args[:tape_color]
@front_shock = args[:front_shock]
@rear_shock = args[:rear_shock]
end
# 좋지 않다.
def spares
if style == :road
{
chain: '10-speed',
tire_size: '23',
tape_color: tape_color
}
else
chain: '10-speed',
tire_size: '2.1',
rear_shock: rear_shock
end
end
end
bike = Bicycle.new(
style: :mountain,
size: 'S',
front_shock: 'Manitou',
rear_shock: 'Fox'
)
bike.spares
# {:tire_size => "2.1", :chain => "10-speed", :rear_shock => 'Fox'}
- 위 코드는
style
변수가 가지고 있는 값을 확인하고 어떤 예비부품이 필요한지 결정한다. - 이렇게 코드를 구성하면 새로운
style
을 추가하는 경우if
문을 수정해야 한다. - 또한,
spares
메서드는 기본 문자열을 가지고 있는데, 이 문자열 중 몇몇은if
문의 조건마다 반복되고 있다. Bicycle
은 하나 이상의 책임을 지고 있으며 수정요쳥에 노출되기 쉬운 코드를 품고 있으며 그 자체로는 재사용이 불가능하다.- 이 코드는 자기가 어떤 종류인지 알고 있는 어트리뷰트를 확인하는
if
문을 포함하고 있다. 이 어트리뷰트를 통해 자기 자신에게 어떤 메시지를 보낼지 결정한다. - 오리 타입에서는 객체의 클래스를 확인하고 이 객체에게 어떤 메시지를 전송할지 결정하는
if
문이 있었다. - 송신자의 입장에서는 "나는 네가 누구인지 알고 있다. 때문에 네가 무엇을 하는지도 안다." 이런 지식은 수정 비용을 높이는 의존성이다.
🎈 숨겨진 타입 찾아내기
- 변수
style
은Bicycle
을 서로 다른 두 종류로 구분 하고 있다. - 하나의 클래스가 여러 개의 서로 다른, 하지만 연관된 타입을 가지고 있다.
- 이것을 상속을 통해서 해결할 수 있다. 다시 말해서, 밀접히 연관된 타입들이 같은 행동을 공유하고 있지만 특정한 관점에서는 다른 경우인 것이다.
🎈 상속을 선택하기
- 객체는 메시지를 직접 처리하거나 다른 객체가처리철할 수 있도록 메시지를 넘긴다. 상속은 두 객체 사이의 관계를 정의한다.
- 첫 번째 객체가 이해할 수 없는 메시지를 수신하면 이 객체는 다음 객체에게 자동으로 메시지를 전달한다. 상속은 이와 같은 관계를 맺도록 정의해준다.
- 여러 객체지향 언어들은 단일상속(부모가 하나)을 지원한다. 하나의 하위클래스가 하나의 상위클래스만을 가질 수 있다.
- 오리 타입은 클래스들을 가로지르기 때문에 공통의 행동을 공유하기 위해 고전적 상속을 사용하지 않는다.
- 어떤 객체가 이해할 수 없는 메시지를 수신하면 루비는 자동으로 이 메시지를 상위클래스에 연쇄적으로 전달한다. 이 메시지를 처리할 수 있는 메서드를 구현하고 있는 상위클래스를 찾는다.
- 이해하지 못하는 메시지가 상위클래스의 연쇄를 타고 올라간다는 사실은, 하위클래스는 상위클래스의 모든 행동을 갖고 있다는 점, 그리고 추가적인 행동을 더 가지고 있다는 사실을 말해준다.
📚 상속의 잘못된 사용
- 다음은 하위클래스
MountainBike
를 만들어 보려는 첫 번째 시도이다. initialize
와spares
메서드는 모두Bicycle
이 이미 구현하고 있다. 때문에MountainBike
에서는 재정의(override)되고 있다. 아래 코드에서 재정의된 메서드들은super
를 전송하고 있다.
MountainBike
class MountainBike < Bicycle
attr_reader :front_shock, :rear_shock
def initialize(args)
@front_shock = args[:front_shock]
@rear_shock = args[:rear_shock]
super(args) # 상위클래스의 연쇄 속으로 넘겨주게 된다.
end
def spares
super.merge(rear_shock: rear_shock)
end
end
MountainBike
의 인스턴스가 로드 자전거와 마운틴 자전거의 행동 을 뒤죽박죽으로 가지고 있다.Bicycle
클래스는 상위클래스 용도로 만들어진 클래스가 아니라 구체 클래스이다.
mountain_bike = MountainBike.new(
size: 'S',
front_shock: 'Manitou',
rear_shock: 'Fox',
)
mountain_bike.size # 'S'
mountain_bike.spares
# {:tire_size => '23' 틀림
# :chain => '10-speed'
# :tape_color => nil 해당 사항 없음
# :front_shock => 'Manitou'
# :rear_shock => 'Fox' }
🎈 추상화 찾아내기
MountainBike
가 만들어지면서Bicycle
이라는 이름은 잘못된 정보를 주고 있다. 이 두 클래스의 이름이 둘 사이의 상속 관계를 암시한다.- 하위클래스는 상위클래스의 특수한 형태(specialization)이다.
MountainBike
는Bicycle
의 모든 행동을 갖고 있고 추가적인 행동을 더 가지고 있어야 한다. Bicycle
과 협업할 수 있는 모든 객체는MountainBike
에 대해 아무것도 모른 채MountainBike
와 협업할 수 있어야 한다.- 상속은 이 두 원칙 즉, 모델링하는 객체들이 명백하게 일반-특수 관계를 따라야 한다는 것이고 올바른 코딩 기술을 사용해야 한다는 것이다.
🎈 추상화된 상위클래스 만들기
Bicycle
은 공통된 행동을 가지고 있고MountainBike
와RoadBike
에는 각자의 특수한 행동만을 추가해야 한다.Bicycle
의 퍼블릭 인터페이스에는spares
와size
가 포함되어 있어야 하고 하위클래스의 인터페이스에는 하위클래스만의 고유한 부품을 추가한다.- 추상 클래스는 상속받기 위해서 존재하고 이 클래스는 하위 클래스들의 공유하는 공통된 행동들의 저장소이고 이 추상 클래스를 상속받은 하위클래스들은 구체적인 형태를 제공할 수 있다.
- 상속 관계를 만드는 데는 높은 비용이 든다. 이 비용을 최소화하는 가장 좋은 방법은 하위클래스가 추상 클래스를 필요로 하기 바로 직전에 추상 클래스를 만드는 것이다.
- 새로운 상속 관계를 만들기 위한 첫 단추는 클래스 구조를 만드는 일이다.
class Bicycle
# 이 클래스에는 아무 내용이 없다.
# 여기 있던 코드는 모두 RoadBike로 옮겼다.
end
class RoadBike < Bicycle
# 어제 Bicycle의 하위클래스가 되었다.
# 기존의 Bicycle 클래스가 가지고 있던 모든 코드를 갖고 있다.
end
class MountainBike < Bicycle
# Bicycle의 하위클래스이다.
# 코드가 수정되지 않았다.
end
- 이러한 재배치가 의미 있는 이유는 하위클래스의 코드를 상위클래스로 올리는 것이 상위클래스의 코드를 하위클래스로 내리는 것보다 수월하기 때문이다.
- 이런식의 현재 구조에서
MountainBike
의 하위클래스는 에러가 발생한다. 이유는 상위클래스인Bicycle
에서size
를 구현하고 있지 않기 때문이다. (현재Bicycle
클래스는 빈 클래스)
🎈 추 상적인 행동을 위로 올리기
size
와spares
는 모든 자전거에 적용될 수 있는 메서드이다.size
를 상위클래스인Bicycle
로 옮긴다.
class Bicycle
attr_reader :size
def initialize(args={})
@size = args[:size]
end
end
class RoadBike < Bicycle
attr_reader :tape_color
def initialize(args)
@tape_color = args[:tape_color]
super(args) # super를 통해 Bicycle로 전달
end
# ...
end
- 이제
RoadBike
는size
메서드를Bicycle
클래스로부터 상속받고 있다.RoadBike
는Bicycle
의 하위클래스이기 때문에 메시지 전달이 자동으로 이루어진다.
road_bike = RoadBike.new(
size: 'M',
tape_color: 'red',
)
road_bike.size # "M"
mountain_bike = MountainBike.new(
size: 'S',
front_shock: 'Manitou',
rear_shock: 'Fox'
)
mountain_bike.size # 'S'
- 새로운 상속 관계를 만드는 리팩터링을 진행할 때 유념해야 하는 기본 원칙은, 구체적인 것을 내리기보다는 추상적인 것을 끌어올리는 방식을 취해야 한다.