Smalltalk80LanguageImplementationKor:Chapter 04
- 제 4 장 서브클래스
서브클래스(Subclasses)
Smalltalk-80 시스템에서 모든 객체는 클래스의 인스턴스다. 클래스의 모든 인스턴스는 동일한 유형의 시스템 구성요소를 나타낸다. 예를 들어 Rectangle의 각 인스턴스는 직사각형 면적을 나타내고, Dictionary의 각 인스턴스는 이름과 값 사이에 연관 집합을 나타낸다. 클래스의 인스턴스는 모두 동일한 유형의 구성요소를 나타낸다는 사실은 인스턴스가 메시지에 응답하는 방식과 그들의 인스턴스 변수 형태 모두에 반영된다.
- 클래스의 모든 인스턴스는 동일한 메시지 집합에 응답하고 이를 위해 동일한 메서드 집합을 사용한다.
- 클래스의 모든 인스턴스는 동일한 개수의 명명된 인스턴스 변수를 갖고 있으며, 이를 참조하기 위해 동일한 이름을 사용한다.
- 객체는 해당 클래스의 모든 인스턴스가 색인된 인스턴스 변수를 가질 때에만 색인된 인스턴스 변수를 가질 수 있다.
지금까지 소개한 바와 같이 클래스 구조체는 클래스 멤버쉽에 어떠한 교집합도 제공하지 않는다. 각 객체는 정확히 하나의 클래스의 인스턴스다. 이 구조체는 그림 4.1에 소개되어 있다. 그림에서 작은 원은 인스턴스를 나타내고, 박스는 클래스를 나타낸다. 원이 박스 안에 있으면 박스가 나타내는 클래스의 인스턴스를 표현하는 것이다.
클래스 멤버쉽에 교집합이 없다는 것은 객체 지향 시스템의 디자인에 나타나는 한계점이라고 할 수 있는데, 클래스 설명 간에 어떠한 공유도 허용하지 않기 때문이다. 우리는 두 개의 객체가 상당히 비슷하면서도 어떻게 보면 다르길 원한다. 예를 들어, 부동소수점 수와 정수는 산술 메시지에 응답하는 능력에서는 유사하지만 수치 값을 나타내는 방식에서는 차이가 있다.
정렬된 컬렉션과 백(bag)은 요소를 추가 및 제거할 수 있는 컨테이너라는 점에서 비슷하지만 개별적인 요소로 정확히 어떻게 접근하는지에 따라 차이가 있다. 객체 간 차이는 각자 다른 메시지로 응답하는 것처럼 외부적일 수도 있고, 서로 다른 메서드를 실행하여 동일한 메시지로 응답하는 경우처럼 완전히 내부적일 수도 있다. 클래스 멤버쉽에서 겹침이 허용되지 않으면 두 개의 객체 간 이렇게 부분적인 유사성은 시스템이 보장할 수 없다.
이러한 한계를 극복하는 가장 일반적인 방법은 클래스 경계에서 임의의 교집합을 허용하는 것이다 (그림 4.2).
이러한 접근법을 "multiple inheritance" (다중 상속)이라 부른다. 다중 상속은 일부 객체가 두 클래스의 인스턴스이고 다른 객체들은 하나의 클래스의 인스턴스인 상황을 허용한다. 클래스에서 교집합이 없는(nonintersection) 한계를 좀 덜 일반적으로 완화하는 방법으로는 어떤 클래스가 또 다른 클래스의 모든 인스턴스를 포함할 수 있도록 하되 일반적인 공유는 허용하지 않는 방법을 들 수 있다 (그림 4.3).
이러한 접근법을 "subclassing" (서브클래싱)이라 부른다. 이는 유사한 개념을 포함하는 프로그래밍 언어 Simula의 용어를 따른 것이다. 서브클래싱은 철저히 계층적이어서 A 클래스의 어떤 인스턴스가 B 클래스의 인스턴스일 경우 A 클래스의 모든 인스턴스는 B 클래스의 인스턴스가 되어야 한다.
Smalltalk-80 시스템은 클래스에 대해 상속의 서브클래싱 형태를 제공한다. 이번 장에서는 서브클래스가 슈퍼클래스를 어떻게 수정하는지, 이것이 메시지와 메서드의 연관에 어떻게 영향을 미치는지, 서브클래스 메커니즘이 시스템 내 클래스에 대한 프레임워크를 어떻게 제공하는지를 설명한다.
서브클래스 설명(Subclass Descriptions)
서브클래스가 명시하는 바에 따르면, 그 인스턴스들은 명확하게 제시된 차이점을 제외하면 "superclass" (슈퍼클래스)라는 클래스의 인스턴스와 동일할 것이다. Smalltalk-80 프로그래머는 항상 새로운 객체를 기존 클래스의 서브클래스로 생성한다. Object라는 시스템 클래스는 시스템 내 모든 객체의 유사점을 설명하므로 모든 클래스는 적어도 Object의 서브클래스에 해당할 것이다. 클래스 설명(프로토콜 또는 구현)은 인스턴스가 그 슈퍼클래스의 인스턴스와 어떻게 다른지를 명시한다. 슈퍼클래스의 인스턴스는 서브클래스 존재 유무의 영향을 받을 수 없다.
서브클래스는 모든 측면에서 클래스에 해당하므로 서브클래스를 가질 수 있다. 각 클래스는 하나의 슈퍼클래스를 가질 수 있지만 다수의 클래스가 동일한 슈퍼클래스를 공유할 수도 있으므로 클래스는 트리 구조체를 형성한다. 클래스는 변수와 메서드를 모두 상속하는 클래스의 시퀀스를 갖는다. 이러한 시퀀스는 그 슈퍼클래스로 시작되며, 슈퍼클래스의 슈퍼클래스 등으로 계속된다. 상속 구조(inheritance chain)는 Object를 만날 때까지 슈퍼클래스 관계로 계속된다. Object는 단일 루트 클래스로, 유일하게 슈퍼클래스가 없는 클래스다.
구현 설명은 세 가지 부분으로 나뉜다는것을 기억해보자.
- 클래스명
- 변수 선언
- 메서드 집합
서브클래스는 자체에 새로운 클래스명을 제공해야 하지만 그 슈퍼클래스로부터 변수 선언과 메서드를 모두 상속받는다. 슈퍼클래스에 의해 새 변수가 선언될 수도 있고 새로운 메서드가 추가될 수도 있다. 인스턴스 변수명이 서브클래스 변수 선언에서 추가되는 경우 서브클래스의 인스턴스는 슈퍼클래스의 인스턴스보다 많은 인스턴스 변수를 가질 것이다. 공유 변수가 추가될 경우 이는 서브클래스의 인스턴스로 접근 가능하겠지만 슈퍼클래스의 인스턴스로는 접근할 수 없을 것이다. 추가된 모든 변수명은 슈퍼클래스에 선언된 것과는 다를 것이다.
클래스가 색인된 인스턴스 변수를 갖고 있지 않으면 서브클래스는 그 인스턴스가 상속되는 명명된 인스턴스 변수에 더해 색인된 변수를 갖게 될 것이라고 선언할 수 있다. 클래스가 색인된 인스턴스 변수를 갖고 있다면 그 서브클래스는 색인된 인스턴스 변수 또한 가져야 하며, 서브클래스는 명명된 인스턴스 변수들을 새로 선언할 수도 있다.
서브클래스가 만일 슈퍼클래스 내 메서드와 동일한 선택자를 가진 메시지 패턴의 메서드를 추가할 경우, 그 인스턴스들은 새로운 메서드를 실행함으로써 해당 선택자를 이용해 메시지로 응답할 것이다. 이를 메서드의 오버라이딩이라 부른다. 서브클래스가 만약 슈퍼클래스의 메서드에서 발견되지 않은 선택자를 가진 메서드를 추가할 경우, 서브클래스의 인스턴스는 슈퍼클래스의 인스턴스가 이해하지 못하는 메시지로 응답할 것이다.
이를 요약하자면, 구현 설명의 각 부분은 서브클래스에 의해 다른 방식으로 수정이 가능하다.
- 클래스명은 오버라이드되어야 한다.
- 변수는 추가될 수 있다.
- 메서드는 추가되거나 오버라이드될 수 있다.
서브클래스 예제
구현 설명은 앞 장에서 표시하지 않았던 엔트리를 포함하는데, 이는 그 구현을 명시한다. 아래 예제는 제 3장에 소개된 FinancialHistory 클래스의 서브클래스로서 생성된 클래스다. 서브클래스의 인스턴스는 통화 지출과 영수증(receipt)에 관한 정보를 보관하기 위해 FinancialHistory의 함수를 공유한다. 이러한 인스턴스들은 소득 공제가 되는 지출을 추적하는 추가 함수도 갖고 있다. 서브클래스는 의무적으로 새로운 클래스명을 제공하고(DeductibleHistory), 하나의 인스턴스 변수와 네 개의 메서드를 추가한다. 이러한 메서드들 중 하나는(initialBalance:) 슈퍼클래스에서 메서드를 오버라이드한다.
DeductibleHistory에 대한 클래스 설명은 아래와 같다.
클래스명 | DeductibleHistory |
슈퍼클래스 | FinancialHistory |
인스턴스 변수명 | deductibleExpenditures |
인스턴스 메서드 | transaction recording
spendDeductible: amount for: reason
self spend: amount for: reason.
deductibleExpenditures ←
deductibleExpenditures + amount
spend: amount for: reason deducting: deductibleAmount
self spend: amount for: reason.
deductibleExpenditures ←
deductibleExpenditures + deductibleAmount
inquiries
totalDeductions
↑ deductibleExpenditures
initialization
initialBalance: amount
super initialBalance: amount.
deductibleExpenditures ← 0
|
DeductibleHistory의 인스턴스가 이해하는 모든 메시지를 알기 위해서는 DeductibleHistory, FinancialHistory, Object의 프로토콜을 검사할 필요가 있다. DeductibleHistory의 인스턴스에는 네 개의 변수가 있는데, 세 개는 슈퍼클래스 FinancialHistory로부터 상속된 것이고 하나는 DeductibleHistory 클래스에 명시되어 있다. Object 클래스는 어떠한 인스턴스 변수도 선언하지 않는다.
그림 4.4는 DeductibleHistory가 FinancialHistory의 서브클래스임을 나타낸다. 그림 속 각 박스에서 상단 좌측 모서리에 표시된 이름은 상자가 나타내는 클래스명에 해당한다.
DeductibleHistory의 인스턴스는 세금을 내는 개체(사람, 가계, 기업)의 내역을 기록하는 데에 사용할 수 있다. FinancialHistory의 인스턴스를 이용해 세금을 지급하지 않는 개체의 (자선 단체, 종교 단체) 내역을 기록하는 수도 있다. 사실 FinancialHistory의 인스턴스 대신 DeductibleHistory의 인스턴스를 사용하더라도 눈에 띄는 것은 없는데, 이 둘은 동일한 방식으로 동일한 메시지에 응답하기 때문이다. FinancialHistory로부터 상속된 메서드와 메시지 외에 DeductibleHistory의 인스턴스 또한 지출의 일부 또는 전부가 세금 공제 가능함을 나타내는 메시지에 응답할 수 있다. 새로 이용할 수 있는 메시지로는 전체 금액이 공제 가능할 때 사용되는 spendDeductible:for: 가 있고, 지출의 일부만 공제 가능할 때 사용되는 spend:for:deducting: 이 있다. 총 세금 공제는 DeductibleHistory로 totalDeductions 메시지를 전송함으로써 찾을 수 있다.
메서드 결정(Method Determination)
메시지가 전송되면 수신자의 클래스 내 메서드에서 일치하는 선택자로 된 메시지를 검색한다. 어떤 메시지도 발견되지 않으면 해당 클래스의 슈퍼클래스 내 메서드가 다음으로 검색된다. 검색은 일치하는 메서드가 발견될 때까지 슈퍼클래스 구조의 위로 지속된다. cashOnHand 선택자를 가진 메시지를 DeductibleHistory의 인스턴스로 전송한다고 가정해보자. 실행에 적절한 메서드의 검색은 수신자의 클래스, DeductibleHistory에서 시작된다. 메서드가 발견되지 않으면 검색은 DeductibleHistory의 슈퍼클래스인 FinancialHistory로 계속된다. 여기서 cashOnHand 선택자가 있는 메서드가 발견되면 메서드는 메시지에 대한 응답으로 실행된다. 이러한 메시지에 대해서는 인스턴스 변수 cashOnHand의 값을 리턴하는 것으로 응답한다. 해당 값은 메시지의 수신자, 즉 DeductibleHistory의 인스턴스에서 찾을 수 있다.
일치하는 메서드의 검색은 슈퍼클래스 구조를 따르며 Object 클래스에서 종료된다. 일치하는 메서드가 슈퍼클래스 구조 내 어떤 클래스에서도 발견되지 않으면 수신자에게 doesNotUnderstand: 메시지가 전송되는데, 인자는 문제가 되는 메시지다. Object에는 프로그래머에게 오류를 보고하는 doesNotUnderstand: 선택자에 대한 메서드가 존재한다.
spend:for: 선택자로 된 메시지를 DeductibleHistory의 인스턴스로 전송한다고 가정해보자. 해당 메서드는 슈퍼클래스 FinancialHistory에서 찾을 수 있다. 제 3장에서 제공된 메시지는 다음과 같다.
spend: amount for: reason
expenditures at: reason
put: (self totalSpendFor: reason) + amount.
cashOnHand ← cashOnHand - amount
인스턴스 변수의 값은 (expenditures와 cashOnHand) 메시지의 수신자인 DeductibleHistory의 인스턴스에서 찾을 수 있다. 의사 변수 self는 이 메서드에서도 참조되는데, self는 메시지의 수신자였던 DeductibleHistory 인스턴스를 나타낸다.
self 로의 메시지
메서드가 self를 수신자로 가진 메시지를 포함하면 해당 메시지를 메서드에서 찾으려는 검색은 self를 포함하는 메서드가 어떤 클래스에 포함되어 있는지와 상관없이 인스턴스 클래스에서 시작된다. 따라서 self totalSpendFor: reason 표현식이 FinancialHistory에서 발견되는 spend:for: 에 대한 메서드에서 평가되면 메시지 선택자 totalSpendFor: 에 연관된 메서드의 검색은 self 의 클래스, 즉 DeductibleHistory에서 시작된다.
self로의 메시지는 One과 Two라는 두 개의 클래스 예제를 이용해 설명할 것이다. Two는 One의 서브클래스이고 One는 Object의 서브클래스다. 두 클래스 모두 test 메시지에 대한 메서드를 포함한다. One 클래스 또한 result1 메시지에 대한 메서드를 포함하는데, 이는 self test 표현식의 결과를 리턴한다.
클래스명 | One |
슈퍼클래스 | Object |
인스턴스 메서드 | test
↑ 1
result1
↑ self test
|
클래스명 | Two |
슈퍼클래스 | One |
인스턴스 메서드 | test
↑ 2
|
각 클래스의 인스턴스를 이용해 self로의 메시지에 대한 메서드 결정을 설명하고자 한다. example1은 One 클래스의 인스턴스이고 example2는 Two 클래스의 인스턴스다.
example1 ← One new.
example2 ← Two new
One과 Two의 관계는 그림 4.5에 표시되어 있다. 클래스명을 표시하기 위해 박스에 이름을 붙였고, 그에 해당하는 인스턴스를 나타내는 이름 또한 표시하기 위해 몇 개의 원에 이름을 붙였다.
아래 표는 다양한 표현식을 평가한 결과를 표시한다.
표현식 | 결과 |
example1 test | 1 |
example1 result1 | 1 |
example2 test | 2 |
example2 result1 | 2 |
두 개의 result1 메시지 모두 One 클래스에서 찾을 수 있는 동일한 메서드를 호출한다. 이 둘은 해당 메서드에 포함된 self로 전송되는 메시지 때문에 서로 다른 결과를 생성한다. result1이 example2로 전송되면 일치하는 메서드의 검색은 Two에서 시작된다. 메서드는 Two에서 발견되지 않으므로 슈퍼클래스 One에서 검색을 계속한다. result1에 대한 메서드는 하나의 표현식, ↑self test로 구성된 One에서 찾을 수 있다. 따라서 test에 대한 응답의 검색은 Two 클래스에서 시작된다. test에 대한 메서드는 2를 리턴하는 Two에서 찾을 수 있다.
super 로의 메시지
메서드의 표현식에서 사용할 수 있는 추가 의사 변수로 super가 있다. 의사 변수 super는 self와 마찬가지로 메시지의 수신자를 나타낸다. 하지만 메시지가 super로 전송되면 메서드의 검색은 수신자의 클래스에서 시작되지 않는다. 대신 검색은 메서드를 포함하는 클래스의 슈퍼클래스에서 시작된다. super의 사용은 메서드가 서브클래스에서 오버라이드되었다 하더라도 슈퍼클래스에서 정의된 메서드로 접근하는 것을 허용한다. super를 수신자 이외의 것으로 (예: 인자로) 사용하더라도 self를 사용할 때와 별 다른 효과는 없으며, super를 사용하면 메시지가 처음으로 검색되는 클래스에만 영향을 미칠 뿐이다.
super로 전송되는 메시지는 Three와 Four라는 두 개의 클래스 예제를 이용해 설명할 것이다. Four는 Three의 서브클래스이고, Three는 앞의 예제에서 Two의 서브클래스에 해당한다. Four는 test 메시지에 대한 메서드를 오버라이드한다. Three는 두 개의 새로운 메시지에 대한 메서드를 포함하는데, result2는 표현식 self result1의 결과를 리턴하고 result3은 표현식 super test의 결과를 리턴한다.
클래스명 | Three |
슈퍼클래스 | Two |
인스턴스 메서드 | result2
↑ self result1
result3
↑ super test
|
클래스명 | Four |
슈퍼클래스 | Three |
인스턴스 메서드 | test
↑ 4
|
One, Two, Three, Four의 인스턴스들은 모두 test와 result1 메시지에 응답한다. 메시지에 대한 Three와 Four의 인스턴스의 응답은 super의 효과를 보여준다 (그림 4.6).
example3 ← Three new.
example4 ← Four new
result2 또는 result3 메시지를 example1 또는 example2로 전송하려 할 경우 오류가 발생하는데, One 또는 Two의 인스턴스는 result2 또는 result3 메시지를 이해하지 못하기 때문이다.
아래 표는 다양한 메시지를 전송한 결과를 보여준다.
표현식 | 결과 |
example3 test | 2 |
example4 result1 | 4 |
example3 result2 | 2 |
example4 result2 | 4 |
example3 result3 | 2 |
example4 result3 | 2 |
test가 example3로 전송되면 Two 의 메서드가 사용되는데, Three는 메서드를 오버라이드하지 않기 때문이다. example4는 result1에 대해 4로 응답하는데, example2가 2로 응답한 것과 같은 이유다. result2가 example3로 전송되면 일치하는 메서드의 검색은 Three에서 시작된다. 여기서 발견된 메서드는 표현식 self result1 의 결과를 리턴한다. result1에 대한 응답의 검색 또한 Three 클래스에서 시작된다. 일치하는 메서드는 Three 또는 그 슈퍼클래스인 Two에서 발견되지 않는다. result1에 대한 메서드는 One에서 발견되며, self test의 결과를 리턴한다. test에 대한 응답의 검색 또한 클래스 Three에서 시작된다. 이번에는 일치하는 메서드가 Three의 슈퍼클래스인 Two에서 발견된다.
super로 메시지를 전송하는 효과는 result3 메시지에 대한 example3과 example4의 반응으로 설명될 것이다. result3가 example3로 전송되면 일치하는 메서드 검색은 Three에서 시작된다. 여기서 발견되는 메서드는 super test의 결과를 리턴한다. test는 super로 전송되므로 일치하는 메서드는 Three 클래스가 아니라 그 슈퍼클래스 Two에서 시작된다. Two에서 test에 대한 메서드는 2를 리턴한다. result3가 example4로 전송되면 Four가 test에 대한 메시지를 오버라이드한다 하더라도 결과는 여전히 2가 될 것이다.
이 예제는 잠재적으로 발생할 수 있는 혼동을 강조하는데, super는 수신자의 슈퍼클래스, 즉 마지막 예제에서 Three에 해당하는 클래스에서 검색을 시작하라는 의미는 아니다. 이는 super가 사용된 메서드를 포함하는 클래스의 슈퍼클래스, 즉 마지막 예제에서 Two 클래스에서 검색을 시작하란 뜻이다. Three가 3을 리턴함으로써 test에 대한 메서드를 오버라이드했다 하더라도 example4 result3의 결과는 여전히 2가 될 것이다. 물론 때로는 super를 포함하는 메서드가 발견되는 클래스의 슈퍼클래스가 수신자의 슈퍼클래스와 동일하기도 하다.
super는 DeductibleHistory 에서 initialBalance에 대한 메서드에서도 사용된다.
initialBalance: amount
super initialBalance: amount.
deductibleExpenditures ← 0
해당 메서드는 FinancialHistory 슈퍼클래스에서 메서드를 오버라이드한다. DeductibleHistory에서 메서드는 두 개의 표현식으로 구성된다. 첫 번째 표현식은 잔액(balance)의 초기화를 처리하기 위해 슈퍼클래스로 제어를 전달한다.
super initialBalance: amount
의사 변수 super는 메시지의 수신자를 참조하지만 메서드 검색 시 DeductibleHistory를 건너뛰고 FinancialHistory에서 시작되어야 한다고 나타낸다. 이러한 방식을 통해 FinancialHistory로부터 표현식은 DeductibleHistory에서 중복되지 않아도 된다. 메서드에서 두 번째 표현식은 서브클래스 특정적인 초기화를 실행한다.
deductibleExpenditures ← 0
initialBalance: 메서드에서 self가 super를 대체한 경우 무한 재귀가 발생하는데, initialBalance: 가 전송될 때마다 다시 전송될 것이기 때문이다.
추상 슈퍼클래스(Abstract Superclasses)
추상 슈퍼클래스는 두 개의 클래스가 그 설명 중 일부를 공유하면서 어느 것도 서로의 서브클래스가 아닌 경우에 생성된다. 공유된 측면을 포함하는 두 개의 클래스에 대해 공통(mutual) 슈퍼클래스가 생성된다. 이러한 타입의 슈퍼클래스를 추상적이라고 부르는데, 인스턴스를 갖기 위해 생성되는 것이 아니기 때문이다. 앞서 소개한 그림을 상기시켜보면 추상 슈퍼클래스는 그림 4.7에 설명된 상황을 나타낸다. 추상 클래스는 인스턴스를 직접 포함하지 않음을 주목한다.
추상 슈퍼클래스의 또 다른 사용 예로, 인스턴스가 사전을 표현하는 두 개의 클래스를 고려해보자. 그 중 하나는 SmallDictionary로, 그 내용을 저장하는 데에 필요한 공간을 최소화하고, 나머지는 FastDictionary로 이름과 값을 간간이 저장하고 해싱 기법을 이용해 이름을 위치시킨다. 두 클래스 모두 이름 및 연관된 값을 포함하는 병렬 리스트(parallel list)를 두 개 사용한다. SmallDictionary는 이름과 값을 인접하여 저장하고, 간단한 선형 탐색을 이용해 이름을 위치시킨다. FastDictionary는 이름과 값을 간간이 저장하고 해싱 기법을 이용해 이름을 위치시킨다. 이름이 어떻게 위치하는지 차이 외에 이 두 클래스는 동일한 프로토콜을 공유하고 둘 다 병렬 리스트를 이용해 내용을 저장한다는 면에서 매우 비슷하다. 이러한 유사점은 DualListDictionary라는 추상 슈퍼클래스에서 표현된다. 이 세 가지 클래스 간 관계를 그림 4.8에 표시하겠다.
추상 클래스 DualListDictionary에 대한 구현 설명을 아래 소개하겠다.
클래스명 | DualListDictionary |
슈퍼클래스 | Object |
인스턴스 변수명 | names values |
인스턴스 메서드 | accessing
at: name
| index |
index ← self indexOf: name.
index = 0
ifTrue: [self error: 'Name not found']
ifFalse: [↑ values at: index]
at: name put: value
| index |
index ← self indexOf: name.
index = 0
ifTrue: [index ← self newIndexOf: name].
↑ values at: index put: value
testing
includes: name
↑ (self indexOf: name) ~= 0
isEmpty
↑ self size = 0
initialization
initialize
names ← Array new: 0.
values ← Array new: 0
|
DualListDictionary의 이러한 설명은 DualListDictionary 자체에서 정의된 메시지 또는 본 장과 앞 장에서 이미 설명한 메시지만 사용한다. DualListDictionary에 대한 외부 프로토콜은 at:, at:put:, includes:, isEmpty, initialize 메시지로 구성된다. new 메시지를 전송하면 DualListDictionary(사실 DualListDictionary의 서브클래스의 인스턴스)가 생성된다. 이후 여기로 initialize 메시지가 전송되어 두 개의 변수로 할당이 만들어지도록 한다. 두 변수는 처음에는 비어 있는 배열이다 (Array new:0).
메서드에서 사용된 self로의 메시지 3개, 즉 size, indexOf:, newIndexOf:는 DualListDictionary에서 구현되지 않는다. 이것이 바로 DualListDictionary를 추상적이라고 부르는 이유다. 인스턴스가 생성되었다면 필요한 메시지 모두에게 성공적으로 응답할 수 없을 것이다. SmallDictionary와 FastDictionary, 두 서브클래스는 세 가지 누락된 메시지를 구현해야 한다. 언제나 self가 참조하는 인스턴스의 클래스에서 검색이 시작되어야 한다는 사실이 의미하는 바는 슈퍼클래스 내 메서드는 self로 메시지가 전송되는 곳에서 명시될 수 있지만 해당 메서드는 서브클래스에서 발견된다는 것이다. 이 방법을 이용하면 슈퍼클래스는 서브클래스에 의해 개선되거나 사실상 구현된 메서드에 대한 프레임워크를 제공할 수도 있다.
SmallDictionary는 DualListDictionary의 서브클래스로서 연관을 나타내기 위해 최소 공간을 사용하지만 연관을 찾는 데 긴 시간이 소요될 수 있다. 이는 DualListDictionary에서 구현되지 않은 세 개의 메시지에 대한 메서드를 제공하는데, 이 메시지는 size, indexOf:, newIndexOf: 에 해당한다. 이는 변수를 추가하지 않는다.
클래스먕 | SmallDictionary |
슈퍼클래스 | DualListDictionary |
인스턴스 메서드 | accessing
size
↑ names size
private
indexOf: name
↑ to: names size do:
[ :index | (names at: index) = name ifTrue: [↑ index]].
↑ 0
newIndexOf: name
self grow.
names at: names size put: name.
↑ names size
grow
| oldNames oldValues |
oldNames ← names.
oldValues ← values.
names ← Array new: names size + 1.
values ← Array new: values size +1.
names replaceFrom: 1 to: oldNames size with: oldNames.
values replaceFrom: 1 to: oldValues size with: oldValues
|
이름은 인접하여 저장되므로 SmallDictionary의 크기는 그 이름의 배열, names의 크기다. 특정 이름의 색인은 배열 names의 선형적인 검색으로 결정된다. 매치 결과가 발견되지 않으면 색인은 0이고, 검색에서 실패를 신호로 보낸다. 새로운 연관이 사전으로 추가될 때마다 newIndexOf: 에 대한 메서드가 사용되어 적절한 색인을 찾는다. 여기서는 names의 크기와 values의 크기가 현재 요소를 저장하는 데에 필요한 크기와 정확히 같을 것이라고 가정한다. 즉, 새로운 요소를 추가하는 데에 이용할 수 있는 공간이 없다는 뜻이다. grow 메시지는 이전 요소의 사본에 해당하는 두 개의 새로운 Arrays를 생성하는데, 맨 끝에 하나의 요소가 더 있다. newIndexOf: 에 대한 메서드에서 먼저 names와 values의 크기가 증가하고, 새로운 이름이 새로운 빈 위치로 (마지막) 저장된다. newIndexOf:에서 호출되는 메서드는 값의 저장을 책임진다.
아래와 같은 표현식 예를 평가할 수 있겠다.
표현식 | 결과 |
ages ← SmallDictionary new | a new, uninitialized instance |
ages initialize | instance variables initialized |
ages isEmpty | true |
ages at: 'Brett' put:3 | 3 |
ages at: 'Dave' put: 30 | 30 |
ages includes: 'Sam' | false |
ages includes: 'Brett' | true |
ages size | 2 |
ages at: 'Dave' | 30 |
위의 표현식 예제마다 메시지가 발견되는 클래스와 self로 전송된 메시지가 발견되는 클래스를 나타낸다.
메시지 선택자 | self 로 전송된 메시지 | 메서드의 클래스 |
initialize | DualListDictionary | |
at:put: | DualListDictionary | |
indexOf: | SmallDictionary | |
newIndexOf: | SmallDictionary | |
includes: | DualListDictionary | |
indexOf: | SmallDictionary | |
size | SmallDictionary | |
at: | DualListDictionary | |
indexOf: | SmallDictionary | |
error: | Object |
FastDictionary는 DualListDictionary의 또 다른 서브클래스다. 이것은 해싱 기법을 이용해 이름을 찾는다. 해싱 기법에는 더 많은 공간이 필요하지만 선형 탐색보다 시간이 덜 소요된다. 모든 객체는 숫자를 리턴함으로써 hash 메시지에 응답한다. 숫자는 인자의 모듈러스(modulus)에서 값을 리턴함으로써 \\ 메시지에 응답한다.
클래스명 | FastDictionary |
슈퍼클래스 | DualListDictionary |
인스턴스 메서드 | accessing
size
| size|
size ← 0.
names do: [ :name | name notNil ifTrue: [size ← size +1]].
↑ size
initialization
initialize
names ← Array new: 4.
values ← Array new: 4
private
indexOf: name
| index |
index ← name hash \\ names size + 1.
[(names at: index) = name]
whileFalse: [(names at: index) isNil
ifTrue: [↑ 0]
ifFalse: [index ← index \\ names size + 1]].
↑ index
newIndexOf: name
| index |
names size - self size < = (names size / 4)
ifTrue: [self grow].
index ← name hash \\ names size + 1.
[(names at: index) isNil]
whileFalse: [index ← index \\ names size + 1].
names at: index put: name.
↑ index
grow
| oldNames oldValues |
oldNames ← names.
oldValues ← values.
names ← Array new: names size * 2.
values ← Array new: values size * 2.
1 to: oldNames size do:
[ :index |
(oldNames at: index) isNil
ifFalse: [self at: (oldNames at: index)
put: (oldValues at: index)]]
|
FastDictionary는 이미 어느 정도의 공간이 할당된 Arrays를 (Array new: 4) 생성하기 위해 initialize에 대한 DualListDictionary의 구현을 오버라이드한다. FastDictionary의 크기가 단순히 어떤 변수의 크기와 같지는 않은데, Arrays는 항상 빈 엔트리를 갖기 때문이다. 따라서 Array에서 각 요소를 검사하고 nil이 아닌 숫자를 계수하여 크기가 결정된다.
newIndexOf: 의 구현은 기본적으로 SmallDictionary에 사용된 것과 동일한 개념을 따르는데, 단 Array의 크기가 변경된 경우 (이번 예제에서 grow에 대한 메서드에서는 크기가 두 배가 된다) 각 요소가 오래된 Arrays에서 새로운 Arrays로 명시적으로 복사되어 요소가 다시 해싱(rehash)된다는 점에선 차이가 있다. 크기는 SmallDictionary에서 꼭 필요하므로 항상 변경되어야 하는 것은 아니다. FastDictionary의 크기는 names에 빈 위치의 개수가 최소 이하로 떨어질 경우에만 변경된다.
최소 크기는 요소의 25%에 해당한다.
names size - self size < = (names size / 4)
서브클래스 프레임워크 메시지(Subclass Framework Messages)
프로그래밍 스타일 문제에 있어서는 메시지가 클래스에 의해 구현되지 않고 슈퍼클래스로부터도 상속되지 않은 경우, self로 전송되는 메시지를 포함시켜선 안 된다. DualListDictionary의 설명에서는 그러한 메시지가 3개가 존재하는데, size, indexOf:, newIndexOf: 가 그것들이다. 앞으로 여러 장에 걸쳐서는 size에 응답하는 능력이 Object로부터 상속되고, 색인된 인스턴스 변수의 개수가 응답이 된다. DualListDictionary의 서브클래스는 사전 내 이름의 개수를 리턴하기 위해 이 메서드를 오버라이드하기로 되어 있다.
특수 메시지 subclassResponsibility는 Object에 명시된다. 이는 추상 클래스에 적절하게 구현될 수 없는 메시지의 구현에 사용되도록 되어 있다. 즉, Smalltalk-80 규칙에 따른 size, indexOf:, newIndexOf: 의 구현은 다음이 되어야 한다.
self subclassResponsibility
이 메시지에 대한 응답은 Object 클래스에 정의된 아래의 메서드를 호출하는 것이다.
subclassResponsibility
self error: 'My subclass should have overridden one of my messages.'
이와 같이 메서드가 추상 클래스의 서브클래스에서 구현되어야 한다면 보고된 오류는 프로그래머에게 문제를 어떻게 수정할 것인지를 나타낸다. 프로그래머는 이 오류를 사용해 추상 클래스 또한 생성하는데, 여기에는 self로 전송된 모든 메시지가 구현되어 있고, 서브클래스에서 오버라이드되어야 하는 메서드의 지시가 구현되어 있다.
관례에 따르면 프로그래머가 만일 추상 슈퍼클래스로부터 상속된 메시지가 사실상 구현되어선 안 된다고 결정할 경우, 상속된 메서드를 오버라이드하는 적절한 방법은 아래와 같다.
self shouldNotImplement
이 메시지에 대한 응답은 Object 클래스에 정의된 아래 메서드를 호출하는 것이다.
shouldNotImplement
self error: 'This message is not appropriate for this object.'
Smalltalk-80 시스템에는 서브클래스에서 구현이 완료되어야 하는 메시지의 프레임워크를 생성하는 개념을 활용한 몇 가지 주요 서브클래스 계층구조가 있다. 그리고 다양한 종류의 컬렉션을 설명하는 클래스도 있다 (제 9장, 10장 참고). 컬렉션 클래스는 동일한 유형의 컬렉션을 설명하는 클래스들 간에 가능한 한 많이 공유하기 위해 계층적으로 배열된다. 이러한 클래스들은 subclassResponsibility와 shouldNotImplement 메시지를 사용한다. 서브클래스의 또 다른 사용 예로 길이(linear measure)와 숫자 클래스의 계층구조를 들 수 있다 (제 7장, 8장 참고).
용어 정리
서브클래스(subclass) | 기존 클래스에서 변수와 메서드를 상속 받는 클래스. |
슈퍼클래스(superclass) | 변수와 메서드를 상속해주는 클래스. |
Object | 트리 구조의 클래스 계층구조에서 루트가 되는 클래스. |
메서드 오버라이드 하기(overriding a method) | 슈퍼클래스의 메서드와 동일한 메시지에 대한 메서드를 서브클래스에 명시하는 것. |
super | 메시지 수신자를 참조하는 의사 변수로, self와는 메서드의 검색을 시작하는 위치에 차이가 있다. |
추상클래스(abstract class) | 프로토콜을 명시하지만 완전히 구현할 수 없다. 관례상 이러한 클래스 유형의 인스턴스는 생성되지 않는다. |
subclassResponsibility | 서브클래스가 슈퍼클래스의 메시지 중 하나를 구현해야 한다는 오류를 보고하는 메시지. |
shouldNotImplement | 이것은 슈퍼클래스로부터 상속된 메시지이지만 서브클래스의 인스턴스에서는 명시적으로 이용할 수 없다는 오류를 보고하는 메시지. |