SqueakByExample:8.1
Object
어떠한 의도 또는 목적에 대해서도 Object 는 상속 계층의 뿌리root(또는 최하위 계층) 가 됩니다. 실제로 스몰토크에서, 상속도의 진짜 최하위 계층은 ProtoObject 이며, ProtoObject 는 객체로서 가장된 최소한의 엔티티를 정의하기 위해 사용되지만, 지금의 스몰토크는 이런 ProtoObject 에 대한 내용을 크게 신경쓰지 않아도 됩니다.
Obejct 는 kernel-Object 카테고리에서 발견할 수 있습니다. 놀랍게도, 여기에서는 400개의 메서드(확장을 포함한)를 발견할 수 있습니다. 달리 말한다면, 스몰토크에서 사용자가 정의하는 모든 클래스는 사용자가 이런 메서드들을 아는지와는 상관없이 400개의 메서드를 자동으로 제공한다는 의미가 됩니다. 몇몇메서드가 없을수도 있는데 새버전의 Squeak 에서는 몇몇 필요없는 메서드를 제거할 수도 있기 때문이라는 것에 주의해야 합니다.
Object 클래스에 적혀있는 주석:
Object 는 클래스 계층도에 속한 대부분의 다른 클래스들을 위한 루트 클래스(the root class)입니다. 예외가 있다면 ProtoObject(Object 의 superclass)와 그것의 하위클래스들(subclases)입니다. Object 클래스는 접근, 복사, 비교, 오류 처리, 메시지 전송, 그리고 반영과 같은 모든 일반적인 객체에 공통으로 적용되는 기본 동작을 제공합니다. 또한 모든 객체들이 응답해야할 유틸리티 메시지는 이 곳에서 정의됩니다. Object 는 인스턴스 변수를 가지고 있지 않으며 그것(인스턴스변수)를 추가해야할 필요도 없습니다. 이런 특성은 Object 의 여러 클래스들이 특별한 실행(예를 들면 smallInteger 와 UndefinedObject)을 가지는 Object 부터 상속되었기 때문이거나, 또는 VM 이 특정한 표준 클래스의 구조와 레이아웃에 관해 알고있거나 그것들을 기반으로 하기 때문입니다.
만약 Object 의 인스턴스 측면에서 메서드 카테고리 탐색을 시작하면, 여기서 제공되는 몇가지 중요한 동작을 발견하기 시작할 수 있을겁니다.
출력(Printing)
스몰토크에서 모든 객체들은 그 자체의 출력 폼을 반환할 수 있습니다. 사용자는 워크스페이스에서 모든 표현식을 선택할 수 있고 print it 메뉴를 선택할 수 있습니다: 이런 동작들은 프로그램식을 실행하고 반환된 객체에게 그 자체를 출력하도록 요청합니다. 사실 출력에 대한 동작은 반환된 오브젝트에 printString 메시지을 전송합니다. 메서드 printString 은 템플릿 메서드이며 그 핵심은 메시지 printOn: 을 그 자신의 수신자(receiver)에 발송하는것에 있습니다. printOn: 은 hook 메서드 로서 특별하게 취급됩니다.
Object>>printOn: 은 사용자가 가장 빈번하게 오버라이딩 하게되는 메서드중 하나가 될겁니다. printOn 메서드는 인수로 Stream 을 받은후, 받은 인수를 이용해서 String 으로 객체의 이름을 표현하는것을 만들어냅니다. printOn 의 기본적인 구현은 "a" 또는 "an" 뒤에 클래스 이름을 사용하는것 밖에 없습니다. Object>>printString 은 이렇게 만들어진 문자열을 반환 합니다.[1]
예를 들어보면, 클래스 브라우저는 printOn: 메서드를 재정의 하지 않으며, printSting 메시지를 Object 에서 정의된 메서드들을 실행하는 인스턴스로 전송합니다.
Browser new printString ⇒ 'a Browser'
TTCFont 클래스에서 PrintOn: 특수화에 대한 좋은 예를 확인할 수 있습니다. 아래의 코드에서 볼 수 있듯이 클래스의 인스턴스를 출력할때, 폰트의 모음 이름, 크기 그리고 종속 모음 이름이 클래스 이름의 뒤에 같이 출력됩니다.
메서드 8.1: printOn: 메서드의 재정의
TTCFont>>printOn: aStream
aStream nextPutAll: 'TTCFont(';
nextPutAll: self familyName; space;
print: self pointSize; space;
nextPutAll: self subfamilyName;
nextPut: $)
TTCFont allInstances anyOne printString ⇒ 'TTCFont(BitstreamVeraSans 6 Bold)'
메시지 PrintOn: 은 storeOn: 과 같지 않다는것에 주의해주세요. storeOn: 메시지는 수신자를 재생성하는데 사용할 수 있는 프로그램식을 그 자체의(storeOn의)인수에 stream 으로 대입합니다. 이렇게 만들어진 프로그램식은 readFrom 메시지를 사용해서 읽어들일때 처리됩니다. pringOn: 은 단지 수신자의 텍스트 버전을 반환할 뿐입니다. 물론 텍스트로 만들어진 프로그램식이 수신자를 표현하는 자체 처리 프로그램식이 될 수도 있습니다.
표현식과 자체평가표현식 이라는 단어에 대해서 함수 프로그래밍에서, 프로그램식은 실행이 될 때 값을 반환합니다. 스몰토크에서, 메시지(프로그램식)들은 객체(값value)를 반환합니다. 그 자신이 스스로 값으로 인식되는 좋은 속성들을 가지는 객체들이 있습니다. 예를 들면, 객체 true 의 값은 그 자신으로서 true 라는 객체가 됩니다. 이러한 객체들을 자체평가 오브젝트self-evaluating objects 라 부릅니다. 워크스페이스에서 객체를 출력(print it)할 때 객체의 출력용 버전을 볼 수 있습니다. 여기에 위에서 설명한 자체평가 프로그램식의 예제가 있습니다.
true | ⇒ true |
3@4 | ⇒ 3@4 |
$a | ⇒ $a |
#(1 2 3) | ⇒ #(1 2 3) |
배열과 같은 몇몇 객체는 자체평가 또는 포함하고 있는 객체에 의존하지 않는다는 사실을 주의해주세요. 예를 들면, Boolean 으로 만들어진 배열은 자체평가가 되지면 persons 의 배열은 그렇지 않다는거죠. 스퀵 3.9에서 이러한 동작방식은 가능한 많이 (isSelfEvaluating 메시지처럼) 자체평가 형태로 컬렉션을 출력하기 위해 도입되었으며, 이는 특별히 중괄호 배열에 있어 true 가 됩니다. 다음 예제는 동적 배열의 구성요소들일 경우에만, 동적 배열이 자체 처리됨을 보여드립니다. 다음 예제는 동적배열의 구성요소가 자체평가형인 경우 동적배열이 자체평가로 처리됨을 보여줍니다:
{10@10 . 100@100} | ⇒ {10@10 . 100@100} |
{Browser new . 100@100} | ⇒ an Array(a Browser 100@100) |
리터럴 배열은 오직 리터럴만 포함할 수 있다는걸 기억해주세요. 그렇기 때문에 다음의 배열은 두 개의 점(point)을 가지지 않으며 여섯개의 리터럴 요소만을 가지게 됩니다.
#(10@10 100@100) | ⇒ #(10 #@ 10 100 #@ 100) |
printOn: 메서드의 대부분은 자체평가를 구현하는 특수화를 진행하게 됩니다. Point>>printOn: 과 Interval>>printOn: 의 구현은 자체평가방식이 됩니다.
메서드 8.2: Point의 자체평가
Point»printOn: aStream
"The receiver prints on aStream in terms of infix notation."
x printOn: aStream.
aStream nextPut: $@.
y printOn: aStream
메서드 8.3:인터벌(Interval)의 자체평가(Self-evaluation)
Interval»printOn: aStream
aStream nextPut: $(;
print: start;
nextPutAll: ' to: ';
print: stop.
step ∼= 1 ifTrue: [aStream nextPutAll: ' by: '; print: step].
aStream nextPut: $)
1 to: 10 ⇒ (1 to: 10) "intervals are self--evaluating"
정체성(Identity)과 동일성(equality)
스몰토크에서 = 메시지는 객체가 같은지(동일성)를 테스트하며(예: 두개의 객체가 같은값을 가지는지에 대해), == 메시지는 객체의 정체성(identity)을 테스트 합니다(예: 두개의 프로그램식이 같은 객체를 나타내는지에 대한 부분)
객체 동일성 비교에 대한 기본구현은 객체의 정체성에 대한 테스트입니다.
메서드 8.4: 객체의 동일성
Object>>= anObject
"Answer whether the receiver and the argument represent the same object.
If = is redefined in any subclass, consider also redefining the message hash."
↑ self == anObject
아래에 보이는것은 자주 오버라이드 하게될 메서드 입니다. 복소수의 경우를 생각해주세요:
(1 + 2 i) = (1 + 2 i) | ⇒ true | "same value" |
(1 + 2 i) == (1 + 2 i) | ⇒ false | "but different objects" |
이렇게 동작하는 이유는, Complex 가 = 를 다음과 같이 오버라이드(override) 하고 있기 때문입니다.
메서드 8.5: 복소수를 위한 동일성
Complex»= anObject
anObject isComplex
ifTrue: [↑ (real = anObject real) & (imaginary = anObject imaginary)]
ifFalse: [↑ anObject adaptToComplex: self andSend: #=]
Object>>∼= 의 기본구현은 Object>>= 을 부정하는 의미가 됩니다. 일반적으로 변경해야할 필요는 없습니다.
(1 + 2 i) ∼= (1 + 4 i) ⇒ true
만약 = 을 오버라이드 한다면, hash 를 사용한 부분 또한 오버라이드 하지 않으면 안됩니다. 클래스의 인스턴스가 Dictionary의 키로서 사용되었을 경우에, "같다"고 보이는 인스턴스는, hash값도 같아야 합니다:
메서드 8.6: hash는 반드시 복소수등을 위해 재실행되어야 합니다.
Complex»hash
"Hash is reimplemented because = is implemented."
↑ real hash bitXor: imaginary hash.
비록 = 과 bash 를 함께 오버라이드 해야한다고 해도, == 를 재지정하면 안됩니다. (객체의 identity 에 대한 의미는 모든 클래스에 동일합니다) == 기호는 ProtoObject 의 프리미티브 메서드 이기 때문입니다.
스퀵이 다른 스몰토크 구현과는 다른 몇가지 익숙하지 않은 동작을 한다는것에 주의해주세요: 예를들면 symbol 과 string 은 동일하지 않습니다.(이건 기능이 아니라 버그가 아닌가 라는 생각을 하고있습니다)
#'lulu' = 'lulu' ⇒ true
'lulu' = #'lulu' ⇒ true
클래스 멤버십
몇몇의 메서드를 이용하면 사용자는 객체의 클래스를 조회할 수 있습니다.
class. class 라는 메시지를 사용하면 모든 객체에 대해 객체의 클래스가 무엇인지를 요청할 수 있습니다.
1 class ⇒ SmallInteger
이와는 반대로, 객체가 특정 클래스의 인스턴스 인지의 여부에 대한 확인을 요청할 수 있는 방법도 있습니다:
1 isMemberOf: SmallInteger | ⇒ true "must be precisely this class" |
1 isMemberOf: Integer | ⇒ false |
1 isMemberOf: Number | ⇒ false |
1 isMemberOf: Object | ⇒ false |
스몰토크가 그 자신에게서 작성되었기 때문에, 여러분은 상위 클래스와 클래스 메시지(12장을 보십시오)의 올바른 조합을 사용하여 스몰토크의 구조 전체를 실제로 탐색할 수 있습니다.
isKindOf: Object»isKindOf:는 수신자의 클래스를 동일한 것 또는 인수 클래스의 하위 클래스로서 답변합니다.
1 isKindOf: SmallInteger | ⇒ true |
1 isKindOf: Integer | ⇒ true |
1 isKindOf: Number | ⇒ true |
1 isKindOf: Object | ⇒ true |
1 isKindOf: String | ⇒ false |
1/3 isKindOf: Number | ⇒ true |
1/3 isKindOf: Integer | ⇒ false |
1/3은 분수인 동시에 숫자의 한 종류이며, 숫자 클래스는 분수 클래스의 상위 클래스이지만 1/3은 정수가 아닙니다.
respondsTo: Object»respondsTo:은 인수로서 주어진 메시지 선택자를 수신자가 이해하는지의 여부로 답변합니다.
1 respondsTo: #, ⇒ false
보통 오브젝트의 클래스를 위해 오브젝트를 조회하거나 오브젝트가 어떤 메시지를 이해하는지를 오브젝트에게 요청하는 것은 좋지 않은 생각입니다. 여러분은 오브젝트의 클래스에 기초하여 결정을 하는 대신에, 단순히 오브젝트에 메시지를 발송하여야만 하며, 그 오브젝트가 어떻게 작동해야만 할지를 스스로 결정하도록(예컨데 오브젝트의 클래스에 기초하여) 해야 합니다.
복사
오브젝트를 복사하는 것은 몇 가지 미묘한 문제들을 유발합니다. 인스턴스 변수들이 참조에 의해 접근되기 때문에, 오브젝트의 얕은 복사는 인스턴스 변수에 대한 그 오브젝트의 참조를 원본 오브젝트와 공유합니다:
a1 := { { 'harry' } }. | |
a1 | ⇒ #(#('harry')) |
a2 := a1 shallowCopy. | |
a2 | ⇒ #(#('harry')) |
(a1 at: 1) at: 1 put: 'sally'. | |
a1 | ⇒ #(#('sally')) |
a2 | ⇒ #(#('sally')) "the subarray is shared" |
Object»shallowCopy 는 오브젝트의 얕은 복사를 만드는 프리미티브 메서드입니다. a2가 a1의 유일한 얕은 복사이므로, 두 개의 배열은 그것들이 포함하고 있는 상주하는 배열에 대한 참조를 공유합니다.
Object»shallowCopy 는 Object»copy에 대한 “공용 인터페이스”이며 만약 인스턴스들이 고유한 것일 경우 반드시 재지정 해야만 합니다. 이것은 케이스입니다. 예를 들면 클래스 Boolean, 문자, Smallinteger, 심볼 그리고 UndefinedObject와 함께 있는 경우,
Object»copyTwoLevel은 단순한 얕은 복사가 만족시키지 못할 때, 명확한 작업을 수행합니다:
a1 := { { 'harry' } } . | |
a2 := a1 copyTwoLevel. | |
(a1 at: 1) at: 1 put: 'sally'. | |
a1 | ⇒ #(#('sally')) |
a2 | ⇒ #(#('harry')) "fully independent state" |
Object»deepCopy 는 임의로 오브젝트의 깊은 복사를 만듭니다.
a1 := { { { 'harry' } } } . | |
a2 := a1 deepCopy. | |
(a1 at: 1) at: 1 put: 'sally'. | |
a1 | ⇒ #(#('sally')) |
a2 | ⇒ #(#(#('harry'))) |
깊은 복사의 문제는 상호 귀납적 구조에 적용될 때, 종료되지 않는다는 것입니다:
a1 := { 'harry' }. | |
a2 := { a1 }. | |
a1 at: 1 put: a2. | |
a1 deepCopy | ⇒ ...does not terminate! |
비록, 깊은 복사 재지정의 올바른 작업이 가능하지만, Object»는 더 나은 솔루션을 우리에게 제공합니다:
메서드 8.7: 템플릿 메서드로 오브젝트 복사하기
Object»copy
"Answer another instance just like the receiver. Subclasses typically override
postCopy;
they typically do not override shallowCopy."
↑self shallowCopy postCopy
공유되지 않아야 할 모든 인스턴스 변수들을 복사하기 위해 여러분은 반드시 postCopy를 재지정해야 합니다. posetCopy는 반드시 super postCopy를 실행해야 합니다.
디버깅
여기서 가장 중요한 메서드는 halt입니다. 메서드에서 중단점을 설정하기 위해, 메서드의 바디에 있는 몇몇 점에 메시지 전송 "self halt”를 삽입합니다. 메시지가 발송될 때, 실행은 간섭되며, 디버거는 여러분의 프로그램에서 이 지점에 열릴 것입니다. (디버거에 관한 좀 더 자세한 내용은 6장을 보십시오.)
다음 중요한 메시지는 assert:이며, 이 메시지는 그 자체의 인수로서 블록을 취합니다 만약 블록이 true를 리턴하면 실행이 계속됩니다. 그렇지 않을 경우, AssertionFailure 예외가 제기될 것입니다. 만약 이 예외가 별도로 잡히지 않으면, 디버거는 실행시 이 지점에서 열릴 것입니다. assert:는 계약으로 설계를 지원하는 작업에 특별히 유용합니다. 가장 전형적인 예시는 오브젝트의 공용 메서드에 대한 사소하지 않은 전제조건을 점검하는 작업에 쓰이는 것입니다. Stack»pop는 다음과 같이 쉽게 실행될 수 있습니다.
메서드 8.8: 전제조건 점검
Stack»pop
"Return the first element and remove it from the stack."
self assert: [ self isEmpty not ].
↑self linkedList removeFirst element
TestCase»assert:로 Object»assert:을 혼돈하지 마십시오, 이것은 SUnit 테스트 프레임워크에서 발생합니다. (7장을 보십시오) 전자가 그것의 인수[2]로서 블록을 기대하는 반면에, 후자는 Boolean을 기대합니다. 비록 두 개 모두 디버깅에 유용하지만, 매우 다른 용도로 쓰입니다.
오류 처리
이 프로토콜은 런타임 오류에 유용한 여러 개의 메서드들을 포함하고 있습니다.
만약, deprecation을 기본 설정 브라우저(Preference browser)의 디버그 프로토콜에서 활성화했다면, 현재 메서드인 self deprecated: anExplanationString 신호 전송은 더 이상 사용해서는 안됩니다. 문자열 인자는 반드시 대안을 제공해야 합니다.
1 doIfNotNil: [ :arg | arg printString, ' is not nil' ]
⇒ SmallInteger(Object)»doIfNotNil: has been deprecated. use ifNotNilDo:
doesNotUnderstand: 는 메시지 lookup이 실패할 때마다 발송됩니다. 디폴트 실행은 예컨대, Object»doesNotUnderstand:가 이 지점에서 디버거를 트리거 하게 될 것입니다. 몇 가지 다른 동작을 제공하기 위해 doestNotUnderstand: 재지정이 유용할 수 있습니다.
Object»error 와 Object»error:는 예외를 제기하기 위해 사용될 수 있는 일반 메서드입니다. 일반적으로, 여러분 자신의 사용자화된 예외들을 제기하는 것이 나으므로, 여러분의 코드에서 생긴 오류와 커널 클래스에서 생긴 오류를 구별할 수 있을 것입니다.
스몰토크에서 추상 메서드는 바디 self subclassResponsibility 와 함께 관례에 따라 실행됩니다. 추상 클래스는 우연히 예시되어야만 하며, 그 다음 추상 메서드에 대한 호출은 처리 중인 Object»subclassResponsibility를 결과물로 내놓을 것입니다.
메서드 8.9: 추상 메서드임을 신호로 알리기.
Object»subclassResponsibility
"This message sets up a framework for the behavior of the class' subclasses.
Announce that the subclass should have implemented this message."
self error: 'My subclass should have overridden ', thisContext sender selector
printString
매그니튜드(혹은 등급, Magnitude), 숫자 그리고 Boolean은 우리가 이번 장에서 잠깐 살펴볼 추상 클래스들의 고전적인 예들입니다.
Number new + 1 ⇒ ''Error: My subclass should have overridden #+''
'self ShouldNotImplement는 관례에 따라 상속된 메서드인 시그널에 발송되었고, 그 발송은 이 서브클래스에 적합하지 않습니다. 이것은 일반적으로 클래스 계층도의 디자인과 꽤 잘맞지 않는 어떤 신호입니다. 그럼에도 불구하고 단일 상속의 한계 때문에, 때때로 이러한 예비수단을 피하는 것이 매우 어렵습니다.
전형적인 예는 딕셔너리에 의해 상속되었지만 실행된 것으로 알려지지 않은 Collection»remove:입니다. (딕셔너리는 removeKey:instead를 제공합니다.)
Testing
testing 메서드는 SUnit 테스트와는 아무 관계가 없습니다. testing 메서드는 여러분에게 수신자(receiver)의 상태에 관한 질문을 하도록 해주고 Boolean(Boolean)을 리턴하는 메서드입니다.
여러 개의 testing 메서드가 오브젝트에 의해 제공됩니다 우리는 이미 isComplex를 보았습니다. 다른 것들은 isArray, isBoolean, isBlock, isCollection 등을 포함합니다. 일반적으로 이러한 메서드들은 회피되어야 합니다. 그 이유는 메서드의 클래스를 위해 오브젝트를 조회(quering)하는 작업은 인켑슐레이션(encapsulation)의 위배 폼(a form of violation)입니다. 사용자는 오브젝트의 클래스를 테스트 하는 대신에, 요청(request)을 반드시 보내어 오브젝트로 하여금, 어떻게 그 클래스를 핸들 할지를 결정하도록 만들어야 합니다.
그럼에도 불구하고, 이 테스트 메서드중 일부는 부인할 수 없을 만큼 유용합니다. 가장 유용한 것은 아마도 ProtoObject»isNil 과 Object»notNil 입니다 (비록 Null 오브젝트[3] 디자인 패턴(Object design pattern)이 심지어 이 메서드들을 위한 필요요소를 제거할 수 있기 때문입니다.
릴리즈 초기화
ProtoObject에 있는 오브젝트에서 발생하지 않는 최종 키 메서드는 initialize입니다.
메서드 8.10: 빈 hook 메서드로서의 initialize
ProtoObject»initialize
"Subclasses should redefine this method to perform initializations on instance
creation"
이것이 중요한 이유는 버전 3.9로서의 스퀵에서, 시스템에 있는 모든 클래스들을 위해 정의된 기본 새 메서드가 새롭게 만든 인스턴스에 초기화(initialize)를 전송하기 때문입니다.
메서드 8.11: 클래스-사이드 템플릿 메서드로서의 new
Behavior»new
"Answer a new initialized instance of the receiver (which is a class) with no
indexable
variables. Fail if the class is indexable."
↑ self basicNew initialize
이 의미는 initialize hook 메서드를 단순히 재지정하여 여러분의 클래스의 인스턴스들이 자동으로 초기화된다는 것입니다. initialize 메서드는 반드시 모든 상속된 인스턴스 변수들을 위한 클래스 불변성을 확보하기 위해 보통 super initialize를 수행합니다. (이것이 다른 스몰토크들의 표준 동작이 아닌 것에 주의해야 합니다)
Notes
- ↑ http://ta.onionmixer.net/wordpress/?p=215 페이지를 참고해주세요
- ↑ 실제로, 이것은 Boolean을 포함한 값을 이해하는 모든 인수를 취할 것입니다.
- ↑ BobbyWoolf, Null Object. In Robert Martin, Dirk Riehle and Frank Buschmann, editors, Pattern Languages of Program Design 3. Addison Wesley, 1998