SmalltalkBestPracticePatterns:3.3

From 흡혈양파의 번역工房
Jump to navigation Jump to search
3.3 메시지(Messages)

메시지(Messages)

메시지는 스몰토크에서 핵심이다. 메시지 없이는 프로그램도 없을 것이다. 이 핵심을 솜씨 좋게 관리하는 것이 전문 스몰토크 프로그래머의 첫 번째 기술이다. 프로그램을 메시지의 패턴과 관련해 바라보는 법을 학습하고 문제를 해결하기 위해 그러한 메시지 스트림에 무엇을 할 수 있는지 학습하고 나면 스몰토크에서 상상할 수 있는 문제는 모두 해결할 수 있을 것이다.


절차적 언어는 명시적으로 선택을 내린다. case 문을 코드로 만들 때는 모든 가능성을 한 번만 이야기한다. 스몰토크에서는 메시지를 이용해 당신을 위한 선택을 내리도록 한다. 거기서 선택 집합이 최종적으로 정해지는 것이 아니라는 장점이 추가된다. 따라서 이후에 기존 선택에 영향을 미치지 않고 새 클래스만 정의함으로써 새로운 선택을 추가하면 된다.


이번 단락에서는 메시지 스트림(message stream)을 사용하는 전술적인 방법에 관해 논하겠다. 그리고 객체 간 통신을 조작함으로써 문제를 해결하는 기법을 모은 도구상자를 제공한다.


메시지(Message)

Composed Method(p.21)를 실행할 필요가 있다.

  • 계산을 호출하는 방법은?


계산을 사용하던 초기에는 위의 질문은 질문 축에도 끼지 못했다. 프로그램은 처음부터 끝까지 실행되는 하나의 큰 루틴이었기 때문이다.


프로그램이 복잡해진 순간부터 "루틴으로서 프로그램" 이 무너지기 시작했다. 개념적으로 전체 프로그램을 한 번에 조작하기가 너무 힘들어진 것이다. 그 시대 제한되었던 자원들도 갑자기 개입하기 시작했다. 여러 곳에 동일한 코드가 중복되어 있으면 코드를 한 번 복사해 필요한 곳마다 호출하여 공간을 절약했다. 두 가지 요인, 정신적 과부하(mental overload)와 메모리 과부하가 함께 작용했다. 루틴 이름에 대한 broken-out part(벗어난 부분)를 제공함으로써 공간을 절약하고, 한 번에 프로그램 조각 하나를 이해하는 편리한 도구를 얻게 되었다.


이 상태는 다년간 지속되었다. 고객이 서브루틴을 호출하길 원하면 서브루틴이 실행되므로 고객은 통제력을 다시 얻을 것이다.


동시에 제어 구조의 규율적 사용이 프로그램의 비용과 품질에 결정적이라는 인식이 증가해왔다. If-then-else와 case문이 개발되면서 프로그램의 실행을 다양화하는 공통 방법들이 수집되어왔다.


Simula는 이러한 두 가지 의견을 기가 막히게 반영하였다. 조건부 코드는 "루틴의 이 부분 또는 저 부분을 실행하라," 고 말한다. 서브루틴 호출은 "저기에 있는 저 코드를 실행하라," 고 말한다. 메시지는 "이 루틴을 여기에서, 저 루틴은 저기에서 실행하든 상관없다," 고 말할 수 있다.


스몰토크는 이에 한발 더 나아가 메시지를 시스템에서 유일한 제어 구조로 만들었다. 모든 절차적 제어 구조, 조건부와 루프가 메시지와 관련해 구현된다. 명시적 조건부 로직은 스몰토크 프로그램에서 절차적 프로그램에 비해 훨씬 더 적은 역할을 하는 것이 보통이다. 메시지가 대부분의 작업을 수행한다.


  • 명명된 메시지를 전송하고, 수신하는 객체가 그 메시지에 대한 결정을 내리도록 하라.


스몰토크에서 모든 것은 메시지의 결과로 발생하므로 예제를 하나 둘 골라내기가 힘들다. #size는 객체가 포함하는 요소의 (variable 변수는 제외) 수를 얻기 위해 어떤 객체로든 전송할 수 있는 메시지이다.


다른 객체가 대신 일을 하도록 시키기 위해선 Delegation(p.64)를 사용하라. Choosing Message(p.45)는 여러 대안책 중 하나를 호출한다. Decomposing Message(p.47)는 의도를 문서화하고 추후 정제(refinement)를 위해 제공한다. Intention Revealing Message(p.48)는 구현부에 그 의도를 매핑한다. Super(p.59)를 사용해 슈퍼클래스에서 행위를 호출하라.


Choosing Message(선택메시지)

메시지(p.43)를 사용하고 있다.

  • 여러 대안책 중 하나를 실행하는 방법은?


장기적 시스템 건강은 주제(theme)와 변형(variation)의 관리에 직결된다. 프로그램을 처음 작성할 때 특정 주제를 염두에 두고 있었을 것이다. 프로그램을 세상에 해방시키면 처음엔 간단하게 생각했던 작업에 대해 모든 변형이 제시된다.


절차적 프로그램은 if-then-else 또는 case문을 이용해 조건부 로직으로 변형을 구현한다. 그러한 하드 코딩된 로직의 경우 두 가지 문제가 발생한다. 첫째, 로직을 수정하지 않고 새로운 변형을 추가할 수 없다. 이는 기존 변형을 방해하지 않고 새 변형을 얻는 것이 곤란한 연산이 될 수 있다. 둘째, 그러한 로직은 전파되는 경향이 있다. 한 곳에서 변형을 설명할 필요가 없고 여러 곳에서 설명해야 한다. 새로운 변형을 추가하는 것은 로직이 위치한 모든 장소를 건들임을 의미한다.


메시지는 주제-변형 프로그래밍을 처리하는 규율적인 방법을 제공한다. 변형은 서로 다른 객체에 상주하기 때문에 변형을 동일한 루틴의 다른 부분에 놓을 때에 비해 서로 간섭하는 기회가 훨씬 적다. 변형을 호출하는 객체, 클라이언트 또한 현재 변형이 활성화된 곳으로부터 분리되어 있다.


새 변형을 추가하는 일은, 다른 변형들과 동일한 메시지 집합을 제공하는 새 객체를 추가한 후 변형 호출을 원하는 객체로 도입하는 것만큼이나 간단하다.


때때로 초보자는 여러 종류의 객체를 갖고 있는데도 조건부 로직에 의지한다:

responsible := (anEntry isKindOf: Film)
	ifTrue: [anEntry producer]
	ifFalse: [anEntry author]


이와 같은 코드는 Choosing Message를 이용해 전달이 쉽고 더 유연한 코드로 항시 변형이 가능하다:

Film>>responsible
	^self producer
Entry>>responsible
	^self author


이제 다음과 같이 작성할 수 있다:

responsible := anEntry responsible


하지만 Explaining Temporary Variable은 더 이상 필요하지 않을 것이다.


  • 각 객체가 하나의 대안책을 실행하는 여러 종류의 객체들 중 하나로 메시지를 전송하라.


프로그램을 시작할 때는 변형을 기대해선 안 된다. 프로그램이 개선되면서 명시적인 조건부 로직이 생기기 시작함을 목격할 것이다. 동일한 로직이 여러 곳에서 반복되는 것이 목격되면 대안책들을 객체로서 표현하여 Choosing Message를 이용해 호출할 시간이 된 것이다.


Choosing Message 의 예제를 몇 가지 들어보겠다:

메시지 대안책
Number>>+aNumber 수신자가 받는 Number의 종류에 따라 다른 코드가 호출될 것이다. Floats는 Integers와 다르게 호출되고, Integers는 Fractions와 다르게 호출된다.
Object>>printOn: aStream 모든 객체는 프로그래머에게 String으로 표현되는 방식을 변경할 수 있는 기회를 가진다.
Collection>>includes: 각각의 집합체(collection)는 이를 매우 다르게 구현한다. 기본 구현의 경우, 집합체의 크기에 비례하여 시간이 소요된다. 그 외는 일정한 시간이 소요된다.


Choosing Message가 self로 전송될 경우 향후 상속에 의한 정제(refinement)를 예상하여 이루어진다.


메시지에 Intention Revealing Selector(p.49)를 제공하라. 변형으로서 호출 가능한 코드 종류의 에는 Methods(p.20)에 관한 절을 참고하라.


Decomposing Message (분해 메시지)

계산을 여러 부분으로 분해하기 위해 메시지(p.43)를 사용하고 있다.

  • 계산의 부분을 어떻게 호출하는가?


Choosing Message가 작업을 완료한다. 이는 절차적 언어에서 case문과 동일하다. 따라서 상황에 따라 다른 코드가 호출된다.


메시지를 사용하는 또 다른 방법은 계산을 조각내는 것이다. 코드를 작성하는 도중에는 가능한 변형을 생각하지 않는다. 메소드가 너무 커지면 부분으로 나누어 더 잘 이해할 수 있도록 만들 필요가 있다. 또 다른 방법으로, 두 가지 또는 그 이상의 메소드가 동일한 부분을 갖고 있음을 확인하면 그 부분들을 단일 메소드로 넣을 수도 있겠다.


이는 절차적 프로그래밍에서 서브루틴이 사용되는 방법과 매우 유사하다. 큰 루틴을 가져와 작은 조각들로 나누는 부분에서 말이다. 스몰토크 코드는 다른 언어들에 비해 코드 분해에 훨씬 더 적극적인 태도를 취한다. 대부분의 스타일 가이드에서는 "루틴에 대한 코드는 한 페이지로 유지하라," 고 말한다. 대부분의 훌륭한 스몰토크 메소드는 몇 개의 행에 들어맞는데, 분명한 건 10행 이하, 주로 3~4행으로 이루어진다.


이것이 가능한 이유 중에서 부분적으로는 스몰토크가 제공하는 추상화(abstraction)가 대부분 언어에서 찾는 수준보다 높기 때문일 것이다. 반복(iteration)을 표현하기 위해 3개 또는 4개 행을 소비하지 않고 하나의 워드(word)를 사용한다. 이 또한 부분적으로는 스몰토크의 프로그래밍 툴이 작은 조각들을 쉽게 관리하도록 도와주기 때문에 가능한 일이라고 생각된다.


  • 여러 개의 메시지를 "self" 로 전송하라.


원본 스몰토크 이미지에서 얻은 이것의 전형적인 예는 다음과 같았다:

Controller>>controlActivity
	self
		controlInitialize;
		controlLoop;
		controlTerminate


이후 이러한 메시지들은 백여 가지의 다른 방식으로 오버라이드 되었기 때문에 모두 Choosing Message가 되었다.


Composed Method(p.21)를 이용해 메소드를 조각내라. 각 메소드에 Intention Revealing Selector(p.49)를 제공하라. Intention revealing Message(p.48)를 이용해 의도를 구현부와 구분하여 전달하라.


Intention Revealing Message (의도를 알려주는 메시지)

Message(p.43)를 사용해 계산을 호출하고 있다. Pluggable Behavior(p.69)의 사용을 숨기고 있는지 모른다.

  • 구현부가 간단할 때 어떻게 의도를 전달하는가?


이러한 메시지들은 스몰토크를 학습하면서 가장 당혹스러운 부분일 것이다. "highlight:" 와 같은 메시지를 보면 "뭔가 재밌는 부분임에 틀림없다" 라고 생각하는 것이 보통이다. 그 대신 다음과 같은 메시지를 보면:

ParagraphEditor>>highlight: aRectangle
	self reverse: aRectangle


무슨 일이 일어나고 있는가?


통신이다. 가장 중요한 것은 단일 행 메소드들이 통신을 위해 존재한다. 위의 메소드를 갖고 있다면 객체 내 나머지 코드는 강조(highlighting)와 관련해 작성할 수 있다. 영역을 강조하고 싶다면 highlight를 전송하는 것이다.


필자는 역(reverse)의 호출로 highlight의 호출을 모두 기계적으로 대체할 수 있었다. 코드는 동일하게 실행될 것이다. 하지만 모든 호출 코드는 구현부를 드러낸다- "I highlight by reversing a Rectangle."


여러 개의 의도를 드러내고 구현부를 숨기는 코드의 또 다른 이점으로는 상속에 의한 정제가 훨씬 수월하다는 점이다. 색상으로 강조하는 ParagraphEditor를 원할 경우 ParagraphEditor의 서브클래스를 만들어 하나의 메소드-hight:-를 오버라이드하면 된다.


Intention Revealing Messages는 컴퓨터 대신 독자들을 위한 글쓰기의 가장 극단적인 사례이다. 컴퓨터가 관련되는 한 두 버전 모두 괜찮다. 의도와 (당신이 완료하길 원하는 작업) 구현부를 (작업이 이루어지는 방법) 구분하는 버전이 사람에게 더 잘 전달한다.


  • "self" 로 메시지를 전송하라. 메시지의 이름을 정하여 작업이 이루어지는 방법(how)보다는 어떤 작업을 완료하길 원하는지(what) 전달하도록 하라. 메시지에 대해 단순한 메소드를 코딩하라.


Intention Revealing Message와 그 구현부의 예를 몇 가지 들어보겠다:

Collection>>isEmpty
	^self size = 0
Number>>reciprocal
	^1 / self
Object>>= anObject
	^self == anObject


메시지에 Intention Revealing Selector(p.49)를 제공하라.


Intention Revealing Selector (의도를 알려주는 선택자)

메소드의 이름을 지정하고 있을 수 있다: Constructor Method (p.23), Conversion Method (p.28), Converter Construct Method (p.26) 또는 Execute Around Method (p.37). 메시지를 명명하고 있을지도 모른다: Decomposing Message (p.47), Choosing Message (p.45) 또는 Intention Revealing Message (p.48). Double Dispatch(p.55)를 구현하는지도 모른다.

  • 메소드의 이름을 무엇으로 정하는가?


메소드의 명명에는 두 가지 선택이 있다. 첫째, 그것이 작업을 수행하는 방식을 이름으로 정하는 방법이다. 따라서 검색하는 메소드는 다음과 같이 호출될 것이다:

Array>>linearSearchFor:
Set>>hashedSearchFor:
BTree>>treeSearchFor:


이러한 명명 스타일을 반대하는 가장 중요한 주장은 통신이 원활하지 못하다는 점에 있을 것이다. 세 가지 다른 객체를 호출하는 코드가 있다면 코드를 이해하기 전에 구현부에서 서로 다른 세 개의 조각을 읽고 이해해야 한다.


또한 이런 방식으로 명명하게 되면 그것이 다루는 객체의 종류를 알고 있는 코드를 낳게 된다. Array와 작업하는 코드를 갖고 있다면 BTree 또는 Set를 대체할 수 없다.


두 번째 메소드 명명 방식은, 작업의 목표를 이름으로 짓고 "작업의 방식" 은 다양한 메소드 body로 넘기는 방법이다. 이는 꽤 어려운 작업으로, 특히 하나의 구현부만 있을 경우 더 까다롭다. 당신의 마음은 작업을 어떻게 수행할 것인지와 관련된 생각으로 가득 차 있기 때문에 이름이 "작업 방식" 을 따르는 것이 자연스럽다. 메소드의 이름을 "작업 방식" 에서 "작업 목표" 로 바꾸기 위한 노력은 장기간으로 볼 때나 단기간으로 볼 때 모두 그럴 만한 가치가 있다. 이로 인해 만들어진 코드는 읽기가 훨씬 수월하고 유연하기 때문이다.


  • 메소드가 행하는 작업을 따라 이름을 정하라.


이 규칙을 위의 예제에 적용하면 모든 메시지에 "searchFor:" 라는 이름을 정해야 할 것이다.

Collection>>searchFor:


실제로 검색은 좀 더 일반적인 개념, inclusion 을 구현하는 하나의 방식이다. 메시지의 이름을 이렇게 일반적인 "작업 목표" 로 정하면 "includes:" 가 선택자가 된다.

Collection>>includes:


단일 구현부로 된 메시지의 이름을 일반화하도록 도와줄 간단한 연습문제가 하나 있다. 두 번째로 매우 다른 구현부를 상상해보라. 그리고 그 메소드에도 동일한 이름을 지을 것인지 생각해보라. 그렇다면 그 순간 아마 당신이 알고 있는 작업방식만큼이나 이름을 추상화해봤을 것이다.


메소드의 이름을 정했다면 Composed Method(p.21)를 이용해 body를 쓰도록 하라. Inline Message Pattern(p.172)로 메소드에서 선택자를 포맷하라. 결과를 수집해야 한다면 Collecting Parameter(p.75)를 추가하라.


Dispatched Interpretation (전달된 해석)

  • 두 객체 중 하나의 객체가 표현을 감추길 원한다면 두 객체는 어떻게 협력할 수 있는가?


인코딩은 프로그래밍에 반드시 필요하다. 프로그래밍을 하다보면 "여기 정보가 있는데 어떻게 표현하지?" 라고 수시로 생각할 것이다. 정보를 인코딩하겠다는 결정은 하루에도 수 백 번씩 내린다.


데이터를 계산으로부터 분리하고서도 두 가지를 충족시키지 못했던 과거 시절엔 인코딩 결정이 매우 중요했다. 어떠한 인코딩 결정을 내리든 다른 많은 계산 부분으로 전파되었다. 인코딩을 잘못하면 변경 비용이 실로 엄청났다. 실수를 늦게 찾을수록 비용은 기가 막히게 늘어났다.


객체는 이 모든 것을 바꾸어 놓았다. 객체들 간에 책임을 분배하는 방식이 이제 중요한 결정이 되고, 인코딩의 중요도는 뒤로 밀려났다. 대부분의 경우 잘 팩토링된 프로그램이라면 단일 객체가 하나의 정보 조각에만 관심을 가진다. 객체는 직접 정보를 참조하고 필요한 인코딩 및 디코딩을 모두 개별적으로 수행한다.


하지만 때로는 하나의 객체에 있는 정보가 다른 객체의 행위에 영향을 미치기도 한다. 정보의 사용이 간단하거나, 가능한 선택이 제한된 정보를 바탕으로 하는 경우에는 인코딩된 객체로 메시지를 전송하는 것으로 충분하다. 따라서 부울 값이 True와 False, 둘 중 한 클래스의 인스턴스로서 표현된다는 사실은 #ifTrue:ifFalse: 메시지에 숨겨진다.

True>>ifTrue: trueBlock ifFalse: falseBlock
	^trueBlock value
False>>ifTrue: trueBlock ifFalse: falseBlock
	^falseBlock value


부울 값을 다른 방식으로 인코딩할 수도 있으며, 동일한 프로토콜을 제공하는 한 어떤 고객도 이를 눈치 채지 못할 것이다.


Sets는 그 요소들과 이렇게 상호작용한다. 객체가 어떻게 표현되는지와 상관없이 #=와 #hash에 반응할 수 있는 한 Set에 넣을 것이다.


때로는 인코딩 결정을 중간 객체(intermediate object)에 숨길 수도 있다. 8-bit 바이트로 인코딩된 ASCII String는 Characters 와 관련해 외부 세계와 대화를 함으로써 그러한 사실을 숨긴다.

String>>at: anInteger
	^Character asciiValue: (self basicAt: anInteger)


인코딩해야 하는 정보의 유형이 많고 클라이언트의 행위가 정보를 바탕으로 변경되는 경우, 이러한 단순한 전략은 효과가 없다. 문제는 수백에 달하는 각 고객이 정보의 유형이 무엇인지 case문에 명시적으로 기록하는 것은 당신이 원하진 않는다는 점이다.


예를 들어, 직선, 곡선, 획(stroke), 채우기(fill) 명령 순차로 표현된 그래픽 Shape를 생각해보자. Shape가 내부적으로 어떻게 표현되는지와 상관없이 명령(command)을 표현하는 Symbol을 리턴시키는 #commandAt: anInteger 메시지와, argument 배열을 리턴하는 #argumentsAt: anInteger를 제공할 수 있다. 우리는 이러한 메시지를 이용해 Shape를 PostScript로 변환하는 PostScriptShapePrinter를 작성할 수 있겠다:

PostScriptShapePrinter>>display: aShape
    1 to: aShape size do:
        [:each || command arguments |
        command := aShape commandAt: each.
        arguments := aShape argumentsAt: each.
        command = #line ifTrue:
            [self
                printPoint: (arguments at: 1);
                space;
                printPoint: (arguments at: 2);
                space;
                nextPutAll: line].
        command = #curve...
        ...]


Shape에 존재하는 명령을 바탕으로 결정을 내리고 싶어 하던 모든 고객은 이제 동일한 case 문을 가져야 하는데, 이는 "꼭 한 번만" 규칙에 위배된다. case문이 코딩된 객체 내부에 숨어 있도록 해주는 해결책이 필요하다.


  • 고객이 코딩된 객체로 메시지를 전송하도록 하라. 인코딩된 객체가 디코딩된 메시지를 전송하는 곳으로 파라미터를 전달하라.


이를 보여주는 가장 간단한 예로 Collection>>do:를 들 수 있다. 당신이 가진 집합체 유형이 무엇이든 항상 #do:를 전송할 수 있다. 하나의 argument Block을 (또는 #value: 에 반응하는 다른 어떤 객체든) 전달함으로써, 집합체가 선형 리스트, 배열, 해시 테이블, 평형 트리(balanced tree) 중 무엇이 인코딩되든 상관없이 코드는 작용할 것이다.


이 예제는 돌아오는 메시지가 하나밖에 없기 때문에 간소화된 Dispatched Interpretation 사례라고 할 수 있다. 대개는 여러 개의 메시지가 돌아올 것이다. 예를 들면, 이 패턴은 Shape 예제에서 이용할 수 있다. 모든 명령마다 case 문을 가지는 대신 PostScriptShapePrinter 내에 모든 명령에 대한 메소드가 있다. 예를 들자면:

PostScriptShapePrinter>>lineFrom: fromPoint to: toPoint
    self
        printPoint: fromPoint;
        space;
        printPoint: toPoint;
        space;
        nextPutAll: 'line'


이는 #commandAt: 과 #argumentsAt:를 제공하는 Shapes보다는 #sendCommandAt: anInteger to: anObject를 제공하는데, 여기서 #lineFrom:to: 는 되돌려 보내기가 가능한 메시지들 중 하나다. 이후 원본 표시 코드는 아래와 같이 읽힐 것이다:

PostScriptShapePrinter>>display: aShape
    1 to: aShape size do:
        [:each |
        aShape
            sendCommandAt: each
            to: self]


이는 다시 Shapes에게 스스로 반복(iterate)할 수 있는 책임을 부여함으로써 아래와 같이 더 간소화될 수 있겠다:

Shape>>sendCommandsTo: anObject
    1 to: self size do:
        [:each |
        self
            sendCommandAt: each
            to: anObject]


따라서 원본 표시 코드는 아래가 된다:

PostScriptShapePrinter>>display: aShape
    aShape sendCommandsTo: self


"dispatched interpretation" 은 책임의 분배에서 지어진 이름이다. 인코딩된 객체는 메시지를 클라이언트에게 "전송" 한다. 클라이언트는 그 메시지를 "해석한다." 따라서 Shape은 #lineFrom:to:나 #curveFrom:mid:to: 와 같은 메시지들을 전송한다. PostScript 를 생성하고 화면에는 ShapeDisplayer가 표시되는 PostScriptShapePrinter 를 이용해 메시지를 해석하는 것은 클라이언트의 몫이다.


되돌려 보낼 메시지의 Mediating Protocol(p.57)을 설계해야 할 것이다. 두 객체 모두 디코딩을 해야 하는 계산은 Double Dispatch(p.55)가 필요하다.


Double Dispatch (이중 전달)

두 개의 객체 군(family) 사이에 Dispatch Interpretation(p.51)을 갖고 있다. 당신은 복합 Equality Method(p.124)를 구현하고 있을 수 있다.

  • 두 개의 클래스 군의 교적(cross product), 다수의 case가 있는 계산을 어떻게 코딩할 수 있는가?


이 패턴은 스몰토크의 또 다른 공학의 중간물을-메소드 전달(method dispatch)-관리하는 데 도움이 된다. 당신이 객체로 메시지를 전송하고 argument를 포함하고 있다면, 그에 상응하는 메소드를 검색 시 수신자의 클래스만 고려한다. 99%는 이 때 전혀 문제를 야기하지 않는다. 그렇지만 호출될 로직이 수신자의 클래스에만 의존하는 것이 아니라 argument 중 하나의 클래스에 의존하는 사례가 몇 가지 있다. 사실 어떤 객체가 수신자이고 어떤 객체가 argument인지는 전적으로 임의적이다.

    Argument
    C D
Receiver A Method 1 Method 2
B Method 3 Method 4


이러한 관계가 존재하는 클래스의 예를 들자면, 산술(arithmetic)을 들 수 있겠다. Integer를 Integer로 추가할 때 하나의 메소드를 원하고, Float를 Float에 추가 시 또 다른 메소드를, Integer를 Float으로 추가 시 또 다른 메소드를, Float를 Integer로 추가 시에도 다른 메소드를 원한다.


이러한 상황에서 절차적 해결방법은 커다란 case문을 가지는 것이다. 모든 명시적 case 로직과 마찬가지로, 이는 한 곳에 모든 프로그램 로직을 모아 두는 이점은 있겠지만 관리와 확장이 어렵다는 단점이 있다.


이에 해결책은 계산에 두 객체를 모두 관련시키는 메시지들로 이루어진 계층을 추가하는 방법이다. Self Delegation과 마찬가지로 이는 더 많은 메시지를 생성하여 복잡성이 증가하지만 그럴만한 가치가 있다.


  • argument에 메시지를 전송하라. 수신자의 클래스 이름을 선택자 이름 앞에 붙여라. 수신자를 argument로서 전달하라.


산술 예제는 아래와 같이 코딩할 수 있다. Integer와 Float 둘 다 argument로 Double Dispatch(이중 전달)한다:

Integer>>+ aNumber
    ^aNumber addInteger: self
Float>>+ aNumber
    ^aNumber addFloat: self


Integer와 Float 둘 다 두 가지 덧셈 방식을 구현해야 한다. Integer-Integer과 Float-Float case는 기본(primitive)으로 처리된다.

Integer>>addInteger: anInteger
    <primitive: 1>
Float>>addFloat: aFloat
    <primitive: 2>


각 클래스의 수가 하나인 경우, Integer를 Float로 변환하고 다시 시작해야 한다:

Integer>>addFloat: aFloat
    ^self asFloat addFloat: aFloat
Float>>addInteger: anInteger
    ^self addFloat: anInteger asFloat


최악의 경우 Double Dispatch(이중 전달)는 N x M 메소드로 이끌 수 있는데, 여기서 N은 본래 수신자의 클래스 수이고 M은 본래 argument의 클래스 수를 의미한다. 사실 수신자 클래스는 주로 argument 클래스와 마찬가지로 상속에 의해 연관되므로 많은 공통 구현부가 제외될 수 있다.


한 검토자는 또 다른 Double Dispatch(이중 전달)의 유용한 사용을 제시하였다-드래그앤드롭(drag-and-drop) 연산의 구현이 그것이다. 어떠한 종류의 수신자 위에 어떠한 종류의 객체를 드래그하는지에 따라 다른 코드를 실행하고자 한다. 가장 단순하고 유연한 구현 방법은 Double Dispatch(이중 전달)를 사용하는 것이다.


객체가 통신하는 대상을 이용해 Mediating Protocol(p.57)를 생성하라. Type Suggesting Parameter Names(p.174)는 어떤 프로세스 단계에서든 당신이 얼마나 많이 알고 있는지 기록하는 데에 중요하다.


Mediating Protocol (매개 프로토콜)

Dispatched Interpretation (p.51) 또는 Double Dispatch(p.55)를 구현하고 있다.

  • 서로 독립된 채로 남아있어야 하는 두 객체 간 상호작용을 어떻게 코딩할 것인가?


두 객체의 협력을 수반하는 프로그램을 작성할 때 대개 당신은 필요한 메소드를 생성한다. 그리고 dialog는 유기적으로 증가한다. 완료되면 두 객체가 함께 작용하지만 굳이 모든 메시지가 왔다 갔다 한다는 느낌을 가질 필요는 없다.


대부분의 경우 이러한 적시의 상호작용은 비용이 별로 들지 않는다. 프로그래머가 변경해야 하는 것을 들자면, 객체를 한두 개 변경하거나 이따금씩 그 객체들 간 교환되는 메시지에 추가하는 것뿐이다.


하나 또는 그 이상의 객체를 교체하기로 결정할 때는 객체들 간 프로토콜을 만들 필요가 있다. 따라서 여기서 중요한 질문은 "두 객체 간 정확히 어떤 메시지가 흐르는가?" 가 된다.


그 답, 메시지 선택자의 리스트를 찾으면 이제 할 일이 생긴 것이다. 첫째, 선택자에서 단어들을 보고 코히런트(coherent) 시스템을 형성하는지 확인하라. 조금씩 증가하는 프로토콜은 부적합성이 거의 축적되지 않는 경향을 보인다. #show가 #makeInvisible와 상반(opposite)되듯이, 때로는 메시지에서 일관된 상반 대상(opposite)을 갖지 않을 수도 있다. 그리고 때로는 선택자를 비일관적으로 복수로 만들기도 하는데, #addEmployee:의 상반 대상(opposite)이 #removeAllEmployees:라는 것이 예가 된다.


상호작용에서 객체 중 하나를 교체할 필요가 있다고 생각되면 향후에 나머지 객체들은 다른 대체물(replacement)을 생성해야 할 가능성이 높다. 프로토콜 내 단어들이 일관되고 분명하게 표현되었다면 당신의 코드를 예제로 이용해 재빠르게 그들의 대체물을 생성할 수 있을 것이다.


  • 객체들 간 프로토콜을 정제하여 사용된 단어들의 일관성을 확보하라.


VisualWorks에서 #value와 #value:는 사용자 인터페이스 컴포넌트와 애플리케이션 모델 간 Mediating Protocol(매개 프로토콜)이다. Double Dispatch(이중 전달) 예제에서 Mediating Protocol(매개 프로토콜)은 #addFloat:와 #addInteger: 가 해당된다. 물론 복합방식 산술을 완료하면 프로토콜은 훨씬 더 커질 것이다.


필자는 몇 년에 걸쳐 매킨토시용 Smalltalk/V로 작업을 해왔다. 필자가 시도한 실습 중에는 스몰토크로 프로그래밍된 TextPane를 네이티브 매킨토시 텍스트 에디터를 둘러싼 래퍼(wrapper)로 대체하는 작업이었다. Smalltalk/V는 Model/Pane/Dispatcher 기반의 사용자 인터페이스 프레임워크를 사용했기 때문에 pane과 연관된 TextDispatcher가 있었다. 그 목적은 사용자 입력을 해석하고 의미 있는 메시지를 pane으로 전달하는 것이었다.


한동안 필자는 새로운 pane에 매달려 필자의 방식대로 디버깅을 시도하였다. 오래 지나지 않아 달리 방도가 없음을 깨달았다. TextPane과 TextDispatcher 사이에 너무나 많은 일이 일어나고 있었다. 그래서 코드를 붙들고 앉아 pane으로부터 dispatcher로 가는 메시지, 그 반대방향으로 가는 메시지를 모두 기록했다. 마침내 pane에서 dispatcher로 가는 메시지가 몇 안 남았을 때 dispatcher는 pane으로 56개의 서로 다른 메시지를 전송하고 있었다.


56개 메시지의 Mediating Protocol(매개 프로토콜)을 제자리에 두고 자리에 앉아 이 메시지들을 지원하기 위한 새 pane을 설계했다. 모두 구현하고 나서야 이제 끝났다고 생각했다.


Mediating Protocol(매개 프로토콜)을 지원하기 위한 모든 메소드를 단일 메소드 프로토콜에 넣어야 찾아서 복사하기 쉽다.


각 메시지에 Intention Revealing Selector(p.49)가 꼭 있는지 확인하라.


Super

메시지(p.43)를 전송하고 있다.

  • 슈퍼클래스 행위를 어떻게 호출할 수 있는가?


객체는 그 클래스의 컨텍스트와 그 클래스의 슈퍼클래스 모두를 함께 구성하여 생성된 행위 및 상태의 풍부한 컨텍스트에서 실행된다. 대부분의 경우 클래스 내 코드는 그것이 이용할 수 있는 메소드의 전체 세계가 평평한(flat) 것처럼 작성된다. 즉, 모든 메소드 집합을 슈퍼클래스 사슬(chain)로 가져가서 당신은 그것을 작업하면 된다.


이러한 작업에는 많은 장점이 있다. 주어진 메소드의 상속 구조에 대한 의존성을 최소화한다. 메소드가 self에서 다른 메소드를 호출할 경우, 메소드가 사슬 어딘가에서 구현되는 한 호출되는 메소드는 만족스럽다. 이는 일부 메소드의 위치를 가정하는 메소드를 크게 바꾸지 않고 코드를 재팩토링할 수 있는 자유를 준다.


이 모델에는 엄청난 예외가 있다. 특히 상속은 슈퍼클래스에서 메소드의 오버라이드를 가능하게 한다. 슈퍼클래스 메소드가 슈퍼클래스 메소드의 일부 측면을 원하면 어떨까? 훌륭한 스타일은 하나의 규칙으로 압축된다; 꼭 한 번씩만 말하라. 슈퍼클래스 메소드가 슈퍼클래스 메소드로부터 코드의 복사본을 포함한다면, 더 이상 유지가 쉽지 않을 것이다. 한 번에 두 개의 복사본 또는 (잠재적으로) 다수의 복사본을 업데이트해야 한다는 사실을 기억해야 한다. 오버라이드의 필요성, 메소드의 평평한 공간에 대한 환상을 유지해야 할 필요성, 그리고 코드를 완전하게 팩토링해야 할 필요성 간에 발생하는 긴장을 어떻게 해소할 수 있을까?


  • "self" 대신 "super" 로 메시지를 전송함으로써 슈퍼클래스에서 코드를 명시적으로 호출하라. 메시지에 해당하는 메소드는 전송하는 메소드를 구현하는 클래스의 슈퍼클래스에서 발견될 것이다.


슈퍼클래스 행위의 확장이 필요한 예를 하나 들자면, 바로 초기화가 되는데, 슈퍼클래스가 정의한 상태가 초기화되어야 할 뿐만 아니라 슈퍼클래스가 정의한 상태 또한 초기화될 필요가 있겠다.


"super" 를 사용하는 코드는 항상 주의 깊게 검사하라. "super" 를 "self" 로 변경 시 코드가 실행되는 방식을 변경하지 않는다면 그렇게 변경하라. 필자가 추적을 시도하면서 가장 짜증났던 버그 중 하나는 코드를 작성할 당시에는 별다른 일이 없었는데 막상 현재 실행되는 메소드가 아닌 다른 선택자를 호출하던 super의 사용과 관련된 버그다. 이후 필자는 서브클래스에서 그 메소드를 오버라이드하였고 반나절에 걸쳐 그것이 왜 호출되지 않았는지를 규명하려 했다. 수신자가 "self" 대신 "super" 이었다는 사실은 간과하고 그러한 가정을 살펴보느라 시간을 낭비했다.


Extending Super(p.60)는 슈퍼클래스에 행위를 추가한다. Modifying Super(p.62)는 슈퍼클래스의 행위를 변경한다.


Extending Super (Super 의 확장)

Super(p.59)를 사용하고 있다.

  • 메소드에 대한 슈퍼클래스 구현부에 추가하는 방법은?


Super의 사용은 결과적 코드의 유연성을 감소시킨다. 이제 어딘가에 특정 메소드의 구현부가 있을 것이라 가정할 뿐만 아니라 구현부는 메소드를 포함하는 클래스 위의 슈퍼클래스 사슬에 존재해야 한다고 가정하는 메소드가 있다. 이러한 가정이 크게 문제되는 경우가 드물지만 항상 상관관계(tradeoff)를 인식해야 한다.


Super를 이용해 코드의 중복을 피하고 있다면 상충관계가 꽤 합리적이다. 예를 들어, 슈퍼클래스에 일부 인스턴스 변수를 초기화하는 메소드가 있고 당신의 클래스가 자신이 소개한 변수를 초기화하길 원한다면 super 가 올바른 해답이다.

Class: Super
    superclass: Object
    instance variables: a

Super class>>new
    ^self basicNew initialize
Super>>initialize
    a := self defaultA


위와 같은 코드를 가지거나 아래와 같이 서브클래스에 initialization을 확장하는 것보다는:

Class: Sub
    superclass: Super
    instance variables: b

Sub class>>new
    ^self basicNew
        initialize;
        initializeB
Sub>>initializeB
    b := self defaultB


super 를 이용하면 두 가지 initialization을 모두 명시적으로 구현하면서:

Sub>>initialize
    super initialize.
    b := self defaultB


Sub가 "new" 를 전혀 오버라이드하지 않아도 된다. 그 결과 코드의 의도를 더 직접적으로 표현한다. Supers는 생성될 때 초기화되도록 하고 Sub에서 initialization의 의미를 확장하라.


  • 메소드를 오버라이드하고 오버라이딩 메소드 내 "super" 로 메시지를 전송하라.


Extending Super의 또 다른 예로 디스플레이를 들 수 있다. 슈퍼클래스처럼, 하지만 테두리(border)와 함께 표시해야 하는 Figure의 서브클래스가 있다면 아래와 같이 구현할 수 있다:

BorderedFigure>>display
    super display.
    self displayBorder


Modifying Super (Super 의 수정)

Super(p.59)를 사용하고 있다.

  • 슈퍼클래스의 메소드에 대한 행위를 수정하지 않고 일부만 변경하는 방법은?


이 문제는 Extending Super보다 서브클래스와 슈퍼클래스 간 더 긴밀한 결합도(coupling)을 유도한다. 우리는 슈퍼클래스가 우리가 수정하고 있는 메소드를 구현한다고 가정할 뿐만 아니라 우리가 변경해야 하는 것을 슈퍼클래스가 행하고 있다고 가정한다.


종종 이와 같은 상황은 Composed Method로 메소드를 리팩토링하여 순수 오버라이딩을 사용할 수 있도록 함으로써 강조하는 방법이 최선이다. 예를 들어 아래 initialization 코드는 super를 사용하여 수정할 수 있겠다:

Class: IntegerAdder
    superclass: Object
    instance variables: sum count

IntegerAdder>>initialize
    sum := 0.
    count := 0

Class: FloatAdder
    superclass: IntegerAdder
    instance variables:

FloatAdder>>initialize
    super initialize.
    sum := 0.0


이보다 더 나은 해결방법은, IntegerAdder>>initialization이 사실상 네 가지 일을 하고 있음을 인식하는 것이다: 두 변수 각각에 대한 기본값을 표현하고 할당하기. Composed Method로 리팩토링한 결과는 다음과 같다:

IntegerAdder>>initialize
    sum := self defaultSum.
    count := self defaultCount
IntegerAdder>>defaultSum
    ^0
IntegerAdder>>defaultCount
    ^0

FloatAdder>>defaultSum
    ^0.0


하지만 완전히 팩토링되지 않은 슈퍼클래스로 작업해야 하는 경우도 있다(예: 슈퍼클래스는 #defaultSum을 구현하지 않는다). 당신은 코드를 복사하거나, super를 이용해 더 밀접한 서브클래스/슈퍼클래스 결합도에서 발생하는 비용을 수용할 것인가에 대한 선택에 직면한다. 대개는 추가적 결합도가 문제가 되진 않을 것이다. 슈퍼클래스의 주인(owner)과 원하는 변경사항을 서로 전달하라. 그 동안:


  • 메소드를 오버라이드하고 "super" 를 호출한 후, 결과 수정을 위해 코드를 실행하라.


디스플레이 영역으로부터 또 다른 예제로, 서브클래스의 색상이 슈퍼클래스의 색상과 다른 경우를 들 수 있겠다.

SuperFigure>>initialize
    color := Color white.
    size := 0@0
SubFigure>>initialize
    super initialize.
    color := Color beige


다시 말하지만, 기본 해결책은 기본 색상을 표현하기 위해 Default Value Method(p.86)를 이용한 후 그 메소드를 오버라이드하기만 하면 된다.


Delegation (위임)

Composed Method(p.21)는 다른 객체가 완료한 작업을 필요로 한다. 메시지(p.43)는 다른 객체 내에서 계산을 호출한다.

  • 객체가 상속 없이 구현부를 공유하는 방법은?


상속은 스몰토크에서 구현부를 공유하기 위해 일차적으로 내장된 구조이다. 하지만 스몰토크에서 상속은 하나의 슈퍼클래스로 제한된다. A와 같으면서 B와도 닮은 새 객체를 구현하고 싶다면 어떻게 할까? 또한 상속은 잠재적으로 엄청난 장기적 비용을 동반한다. 서브클래스 내 코드는 스몰토크에서 그냥 적히는 것이 아니다. 모든 슈퍼클래스 내 메소드와 모든 변수의 컨텍스트에서 작성된다. 깊고 풍부한 상속에서 서브클래스 내 가장 단순한 메소드를 이해하기 전에 다수의 슈퍼클래스를 읽고 이해해야할 것이다.


팩토링된 슈퍼클래스는 최저 개발 비용으로 상속을 효과적으로 사용하는 방법을 설명한다. 당신은 공통 구현부를 인식하지만 Factored Superclass가 적절하지 않은 상황에 직면할 것이다. 어떻게 반응할 수 있을까?


  • 작업 중 일부를 다른 객체로 전달하라.


예를 들어, 많은 객체들이 표시되어야 하기 때문에, 시스템 내 모든 객체는 브러시와 같은(brush-like) 객체에게(Visual Smalltalk에서 Pen, Visual Age와 VisualWorks에서 GraphicsContext) 디스플레이를 위임한다. 그렇게 모든 상세한 디스플레이 코드는 단일 클래스에서 집중할 수 있으며, 나머지 시스템 부분은 매우 간소화된 디스플레이 관점을 가질 수 있다.


delegate 가 본래 객체와 관련해 알아야 하는 것이 없는 경우엔 Simple Delegation(p.65)를 사용하라. 위임에 의하여 원본 객체의 정체성이나 그 상태 중 일부를 필요로 하는 경우 Self Delegation(p.67)를 사용하라.


Simple Delegation (단순 위임)

자립적(self-contained) 객체에 Delegation(p.64)이 필요하다. 다음 메소드 중 하나를 구현하고 있을지 모른다: Collection Accessor Method (p.96), Equality Method (p.124), 또는 Hashing Method (p.126).


  • 이해관계가 없는(disinterested) 위임을 호출하는 방법은?


위임을 이용할 때는 어떠한 특징의 위임이 필요한지 규명하도록 도와주는 두 가지 주제가 있다. 첫째, 위임하는 객체의 정체성이 중요한가? 클라이언트 객체가 스스로 전달을 하면서, 위임으로 인해 실제로 완료되는 작업의 일부를 알릴 것이라고 예상한다면 이에 대한 답은 '그렇다' 고 할 수 있다. 위임은 클라이언트에게 그 존재를 알리고 싶지 않으므로 위임하는 객체로 접근을 필요로 한다. 둘째, 위임하는 객체의 상태는 위임에 중요한가? 위임은 최대한으로 널리 사용되기 위해 단순하다 못해 심지어 상태가 없는(state-less) 객체인 것이 보통이다. 그럴 경우 위임은 그 작업을 성취하기 위해 위임하는 객체부터 상태를 요구할 가능성이 크다.


이 두 질문에 대한 답이 "아니요" 가 되는 위임의 사례들도 많다. 위임은 위임하는 객체의 정체성을 필요로 할 이유가 없다. 위임은 추가 상태 없이도 작업을 성취할 만큼 충분히 자체 처리가 가능하다(self-contained).


  • 메시지를 변경하지 않은 채 위임하라.


이 전형적인 예가 바로, Collection(최소한 약간은)과 같이 행위를 하지만 다른 프로토콜도 많이 가진 객체를 들 수 있다. 집합체 클래스 중 하나를 서브클래싱함으로써 상속을 낭비하는 대신 당신의 객체는 Collection을 참조한다. 하지만 클라이언트의 관점에서 보면 당신은 do: 또는 at:put: 과 같이 프로토콜에 응답한다.


Collection은 누가 그것을 호출했는지 신경 쓰지 않는다. 위임하는 객체로부터의 상태는 전혀 요구되지 않는다. 위임하는 객체의 정체성은 무관하다.


여기서 Numbers만 보유하는 Vector를 예로 들어보겠다. 우리는 Collection을 서브클래싱함으로써 구현하지만 Vector에 해당하지 않는 메시지가 많을 가능성이 크다. Collection을 서브클래싱하고 다수의 메시지를 차단하는 대신 객체를 서브클래싱하여 우리가 원하는 메시지만 위임할 수 있겠다.

Vector
    superclass: Object
    instance variables: elements


주어진 요소 수를 이용해 Vector를 생성한다:

Vector class>>new: anInteger
    ^self new setElements: (Array new: anInteger)
Vector>>setElements: aCollection
    elements := aCollection


Vectors의 산술 특성은 무시하고 그것이 어떻게 위임하는지에만 초점을 두겠다. 고객이 Vector를 Numbers의 Collection으로 취급하길 원하는 경우가 종종 있다. 누군가 Vector를 반복할 때 이것은 자신의 "elements" 인스턴스 변수로 위임한다.

Vector>>do: aBlock
    elements do: aBlock


위의 코드는 Simple Delegation의 예제이다. at:, at:put:, size 등의 구현도 같은 방식으로 상상하면 된다.


Self Delegation (자기 위임)

Delegation(p.64)을 사용하고 있다.

  • 위임하는 객체로의 참조를 필요로 하는 객체로 위임을 구현하는 방법은?


Self Delegation에 대한 문제는 Simple Delegation과 동일하다. 본래 위임하는 객체의 정체성이 필요한가? 위임하는 객체로부터 상태가 필요한가?


이 질문 중 하나라도 "예" 인 답이 있다면, Simple Delegation은 작동하지 않을 것이다. 왜 그런지 모르지만 위임은 위임하는 객체로의 접근을 필요로 한다.


위임 접근을 제공하는 한 가지 방법은, 위임으로부터 다시 위임 객체로의 참조를 포함하는 방법이다. 하지만 이러한 접근법에는 몇 가지 단점이 있다. 역참조는 프로그래밍 복잡성을 증가시킨다. 위임이 변경될 때마다 오래된 위임에서 참조는 파괴되어야 하고 새 위임에서 참조가 설정된다. 좀 더 중요한 건, 각 위임은 한 번에 하나의 객체를 위임할 때만 사용할 수 있다. 위임의 다중 복사를 생성하는 데 비용이 많이 들거나 아예 불가능할 경우, 그냥 작동하지 않는다고 보면 된다.


이 책을 통해 제시하는 또 다른 방법은 위임하는 객체를 추가 파라미터로서 전달하는 방식이다. 이는 본래 메소드의 변형을 도입하므로 별로 바람직하진 않지만 이로 인해 얻게 되는 유연성은 그만한 비용을 들여도 아깝지 않다.


  • "for:" 라 불리는 추가 파라미터에서 위임하는 객체를 (예: "self") 전달하라.


Digital Visual Smalltalk 3.0 image 는 Self Delegation 의 훌륭한 예이다. Dictionaries와 같은 hashed collection의 구현부는 두 부분으로 나뉜다. 첫 번째는 Dictionary, 두 번째는 HashTable이다. 여러 상황에서 유용한 HashTables의 변형이 존재한다. 동일한 집합체는 그 특성에 따라 (얼마나 큰지, 얼마나 찼는지 등) 여러 상황에서 다른 HashTables로 위임할 수 있다.

Sbpp 001.png


객체의 해시값은 Collections의 유형에 따라 다르게 구현된다. Dictionaries는 "hash" 를 전송함으로써 해시를 계산한다. IdentityDictionaries는 "basicHash" 를 전송하여 계산한다. 그리고 이것은 Self Delegation을 이용해 구현된다. Collection이 요소를 추가하라는 메시지를 HashTable로 전송하면 다음과 같이 스스로 전달한다:

Dictionary>>at: keyObject put: valueObject
    self hashTable
        at: keyObject
        put: valueObject
        for: self


HashTable 은 Collection으로 메시지를 다시 되돌려 보냄으로써 해시 값을 계산한다:

HashTable>>at: keyObject put: valueObject for: aCollection
    | hash |
    hash := aCollection hashOf: keyObject.
    ...


Dictionaries와 IdentityDictionaries는 이 메시지를 다르게 구현한다:

Dictionary>>hashOf: anObject
    ^anObject hash
IdentityDictionary>>hashOf: anObject
    ^anObject basicHash


Self Delegation은 hashed Collections 계층구조가 HashTables 계층구조와 독립되는 것을 허용한다.


누가 위임하는지에 따라 위임이 다른 로직을 필요로 할 경우 Double Dispatch(p.55)를 사용하라.


Pluggable Behavior (플러그 가능한 행위)

  • 객체의 행위를 어떻게 파라미터화하는가?


객체의 전형적인 모델에서는, 동일한 클래스에서 서로 다른 인스턴스들은 상태는 다르지만 동일한 행위를 가진다. 모든 Point는 x와 y에 대해 다른 값을 가질 수 있지만 그들 모두 "translatedBy:" 를 계산하기 위해 동일한 로직을 사용한다. 다른 로직을 원하면 다른 클래스를 사용한다.


클래스를 이용해 행위를 명시하는 방법은 간단하다. 독자들이 코드를 실행할 필요 없이 시스템의 행위를 정적으로 이해하도록 돕기 위한 프로그래밍 도구가 준비되어 있다.


이 모델은 당신이 생성하게 될 객체의 90%에서 작동한다. 하지만 클래스의 생성은 그만한 대가를 지불해야 하며, 때로는 당신이 문제를 생각하는 방식을 서로 다른 클래스들이 효과적으로 전달하지 않기도 한다.


클래스는 기회이다. 각 클래스는 인스턴스화와 (혹은) 특수화에 유용할 것이다. 하지만 당신이 생성하는 각 클래스는 작성자로서 당신이 목적과 구현부를 독자에게 전달하는 데에 짐이 되기도 한다. 수백 개 또는 수천 개의 클래스가 있는 시스템은 독자들에게 위협이 될 것이다. 여러 클래스에 걸쳐 명칭공간을 관리하는 데에는 비용도 많이 든다. 따라서 합리적인 성과가 있을 때에만 새 클래스의 비용을 충당할 것이다. 단일 메소드로 구성된 클래스의 커다란 군(family)은 그만한 돈을 들일 가치는 없을 것이다.


클래스만을 통한 행위의 특수화에 발생하는 또 다른 문제는 클래스가 유연하지 않다는 점이다. 특정 클래스의 객체를 생성하고 나면 코드를 정적으로 이해하는 능력을 완전히 망치지 않고서는 객체의 클래스를 변경할 수 있는 방법이 없다. single stepping 중에 유심히 관찰하는 방법만이 코드가 실행되는 방식에 대한 통찰력을 제공할 것이다. 스몰토크의 단일 상속도 동시에 여러 다른 축을 따른 특수화를 허용하지 않는다.


Pluggable Behavior(플러그 가능한 행위)를 사용할 예정이라면 아래와 같은 문제들을 고려해야 한다:

  • 얼마나 많은 유연성이 필요한가?
  • 얼마나 많은 메소드가 동적으로 다양해야 하는가?
  • 코드를 따르는 것이 얼마나 힘든가?
  • 클라이언트는 플러그해야 할 행위를 명시해야 하는가, 아니면 플러그된(plugged) 객체 내에 숨길 수 있는가?


런타임에서 클래스를 변경하거나 다수의 작은 클래스의 생성이 불가한 경우, 다른 인스턴스 내에서 서로 다른 로직을 어떻게 명시할 것인가?


  • 다른 행위를 트리거할 때 사용할 변수를 추가하라.


플러그 가능한(pluggable) 행위의 전형적인 예로, 많은 다른 객체의 내용을 표시해야 하는 사용자 인터페이스 컴포넌트처럼 다양한 다른 객체와 접속해야 하는 객체들을 들 수 있겠다. Pluggable Behavior(플러그 가능한 행위)를 이용한 해결책은 수백 개의 다른 서브클래스를 생성하여 각 서브클래스에서 하나 또는 두 개의 메소드만 차이가 나도록 만드는 것보다 훨씬 더 나은 해결책이다.


간단한 행위 변경의 경우 Pluggable Selector(p.70)를 사용하라. Pluggable Block(p.73)은 좀 더 많은 유연성을 제공할 것이다. Pluggability의 구현부는 Intention Revealing Message(p.48) 뒤에 숨겨라.


Pluggable Selector (결합 가능한 선택자)

간단한 Pluggable Behavior(p.69)가 필요하다.

  • 간단한 인스턴스 특정적 행위를 어떻게 코딩하는가?


Pluggable Behavior(플러그 가능한 행위)를 구현하는 가장 단순한 방법은 실행할 선택자를 저장하는 방법이다.


ListPane을 구현했다고 가정하자. 집합체 요소 중 하나를 취하여 String을 리턴하는 메소드를 생성한다:

ListPane>>printElement: anObject
    ^anObject printString


시간이 조금 지난 후 이 하나의 메소드만 오버라이드하는 ListPane의 서브클래스 수가 많다는 사실을 눈치챈다:

DollarListPane>>printElement: anObject
    ^anObject asDollarFormatString
DescriptionListPane>>printElement: anObject
    ^anObject description


이 모든 서브클래스가 하는 일이 단지 하나의 메소드를 오버라이딩하는 데에 그친다면 그만한 비용을 들일 가치가 없다. 간단한 해결책은 ListPane 자체를 좀 더 유연하게 만들어 각기 다른 인스턴스들이 그 요소로 각기 다른 메시지들을 전송하도록 만드는 방법이다. 우리는 "printMessage" 라 불리는 변수를 추가하여 #printElement를 수정한다:

ListPane>>printElement: anObject
    ^anObject perform: printMessage


이전 행위를 유지하기 위해선 printMessage를 초기화해야 한다:

ListPane>>initialize
    printMessage := #printString


Pluggable Selector(결합 가능한 선택자)는 아래와 같이 Pluggable Behavior(플러그 가능한 행위) 기준을 충족한다:


가독성 - Pluggable Selector(결합 가능한 선택자)는 단순한 클래스 기반의 행위보다 따르기가 힘들다. inspector(인스펙터)로 객체를 살펴봄으로써 그것이 어떻게 행위할 것인지 알 수 있다. 코드를 꼭 single stepping할 필요는 없다.
유연성 - Pluggable Selector(결합 가능한 선택자)에 대한 메소드는 수신하는 객체에서 구현되어야 한다. 호출 가능한 메소드 집합은 나머지 객체와 동일한 비율로 변경되어야 한다.
범위 - Pluggable Selector(결합 가능한 선택자)는 객체 당 2개를 넘어선 안 된다. 2개를 초과할 경우 프로그램의 의도를 모호하게 만들 위험이 있다. 좀 더 많은 다양성 차원이 필요하다면 State Object(상태 객체)를 사용하라.


  • 실행할 선택자를 포함하고 있는 변수를 추가하라. Role Suggesting Instance Variable Name 앞에 "Message" 를 붙여라. 선택자를 실행하는 Composed Method를 생성하라.


Pluggable Selector(결합 가능한 선택자)는 단순한 유형의 제약에도 유용하다. 예를 들어, 하나의 시각적 컴포넌트를 다른 시각적 컴포넌트를 기준으로 위치시키고 싶다면 RelativePoint를 생성하기 위해 Pluggable Selector(결합 가능한 선택자)를 사용할 수 있겠다:

Class: RelativePoint
    superclass: Object
    instance variables: figure locationMessage


아래는 Constructor Method이다:

RelativePoint class>>centered: aFigure
    ^self new
        setFigure: aFigure
        message: #center
RelativePoint>>setFigure: aFigure message: aSymbol
    figure := aFigure.
    locationMessage := aSymbol


RelativePoint를 사용하기 위해서는 일반적인(regular) Point와 마찬가지로 #x와 #y와 같은 메시지들을 전송한다.

RelativePoint>>asPoint
    ^figure perform: locationMessage
RelativePoint>>x
    ^self asPoint x


여기까지 완료되면 모든 필요한 Point 프로토콜을 복사하고, 성능의 재설계 등을 실행할 수 있다. 하지만 Pluggable Selector(플러그 가능한 선택자)의 예제에서, CenteredRelativePoint, TopLeftRelativePoint 등에 대한 서브클래스를 만들 필요가 없다는 흥미로운 점이 관찰된다; 따라서 단일 선택자에서 다양성을 포착할 수 있다.


Plugabble Block (결합 가능한 블록)

plugged object(결합 가능한 객체)가 구현하지 않은 복합 Pluggable Behavior(p.69)가 필요하다.

  • 고유의 클래스를 가질 만한 가치가 없는 복합 Pluggable Behavior(결합 가능한 행위)를 어떻게 코딩하는가?


Pluggable Selector(플러그 가능한 선택자)는 호출할 행위가 플러그된 객체 내에 상주할 때 작동한다. 하지만 때로는 플러그된 객체의 다른 책임과 관련되지 않고 복잡하거나, 플러그된 객체에 쉽게 접근이 불가한 다른 객체에서 이미 구현되었거나, 아니면 객체가 생성될 때 플러그되어야 할 행위 범위가 알려지지 않아서 행위가 플러그된 객체 내에 상주하지 않는 경우도 있다.


이런 경우, 특히 행위가 이미 구현된 경우에 공통 해결책은 실행할 선택자보다는 평가될 Block 내에서 플러그하는 방법이다. 블록은 어디서든 생성될 수 있고, Block Closure 사용을 통해 플러그된 객체로 접근이 불가한 객체까지 접근할 수 있으며, 추가 로직 양이 수반되기도 한다.


그러한 일반적 방식으로 사용된 블록은 비용이 엄청나다. 제어의 흐름을 이해하기 위한 코드의 정적인 분석은 절대 불가하다. 플러그된 객체를 검사한다손 치더라도 그 비밀을 밝혀낼 가능성이 적다. 블록 호출을 통한 single stepping만이 무슨 일이 일어나고 있는지를 독자에게 이해시킬 것이다.


블록은 Symbols보다 외부매체로의 저장이 더 힘들다. 일부 Object Steams와 객체 데이터베이스는 Blocks를 저장하고 복구하는 기능이 없다.


  • Block을 저장하기 위해 인스턴스 변수를 추가하라. Role Suggesting Instance Variable Name 이름 앞에 "Block" 을 붙여라. Pluggable Behavior(플러그 가능한 행위)를 호출하기 위한 Block의 평가를 위해 Composed Method를 생성하라.


VisualWorks 객체 PluggableAdapter는 Pluggable Block(플러그 가능한 블록)의 훌륭한 예이다. PluggableAdaptor가 해당하는 ValueModel 군 내의 모든 객체들은 프로토콜 #value와 #value:를 제공한다. PluggableAdaptor는 PluggableBlock으로 이러한 메시지들을 구현한다. 간소화된 구현부를 소개하자면 다음과 같다:

Class: PluggableAdaptor
    superclass: ValueModel
    instance variables: getBlock setBlock


Constructor Method는 블록을 설정한다:

PluggableAdaptor class>>getBlock: getBlock setBlock:
setBlock
    ^self new
        setGetBlock: getBlock
        setBlock: setBlock


Constructor Parameter Method는 Type Suggesting Parameter Name 의 변형을 사용해야 한다는 점을 주목해야 하는데, 이유는 인스턴스 변수 이름에 명백한 파라미터명이 이미 사용되기 때문이다.

PluggableAdaptor>>setGetBlock: gBlock setBlock: sBlock
    getBlock := gBlock.
    setBlock := sBlock


Pluggable Block(플러그 가능한 블록)를 호출함으로써 #value와 #value:를 구현할 수 있다.

PluggableAdaptor>>value
    ^getBlock value
PluggableAdaptor>>value: anObject
    putBlock value: anObject


이제 #value 와 #value:를 예상하는 어느 객체든 다른 객체로 연결할 수 있다:

Car>>speedAdaptor
    ^PluggableAdaptor
        getBlock: [self speed]
        putBlock: [:newSpeed | self speed: newSpeed]


Collecting Parameter (수집 파라미터)

Intention Revealing Selector(p.49)를 작성하였다.

  • 여러 메소드의 협력적 결과인 집합체를 어떻게 리턴하는가?


Composed Method의 결점 중 하나는 작은 메소드들 간 연결 때문에 이따금씩 문제가 생긴다는 점이다. 임시 변수에 저장되어 왔던 상태가 이제는 메소드들 간 공유되어야 한다.


이 문제의 가장 간단한 해결책은 모든 코드를 단일 메소드에 남겨두고 메소드의 부분들 간 통신을 위해 임시 변수를 사용하는 방법이다. 이 접근법을 취할 경우 Composed Method로부터 기대하던 모든 장점들도 사라질 것이다. 코드에서 알 수 있는 정보는 줄어들고, 재사용과 정제가 더 까다로워지며, 수정이 힘들어진다.


다른 해결책으로, 인스턴스 변수를 메소드 간에서만 공유되는 객체로 추가하는 방법이 있다. 이 변수는 객체 내 다른 변수들과 매우 상이하다. 이 방법은 객체의 라이프사이클에 걸쳐서가 아닌, 메소드가 실행되는 동안에만 이용 가능하다. 인스턴스 변수는 함께 어울려야 하는 상태만 전달 및 보관하도록 존재해야 한다.


모든 메소드로 전달되는 추가 파라미터를 추가함으로써 문제를 해결하는 방법도 있다. 유용한 작업이 아닌 이상 필자는 이와 같이 메소드의 계층을 추가하는 방법은 꺼리는 편이다. 하지만 위의 상황에서는 다른 해결책을 이용할 수 없으므로 올바른 해결책이겠다.


  • 결과를 수집하는 파라미터를 모든 서브메소드에 추가하라.


여기 한 가지 예가 있다. 아래 코드는 사람들의 집합체에서 모든 기혼남과 미혼여성을 추출한다:

marriedMenAndUnmarriedWomen
    | result |
    result := OrderedCollection new.
    self people do: [:each | each isMarried & each isMan
ifTrue: [result add: each]].
    self people do: [:each | each isUnmarried & each
isWoman ifTrue: [result add: each]].
    ^result


Composed Method를 이용해 각 반복(iteration)을 고유의 메소드에 넣는다:

marriedMen
    | result |
    result := OrderedCollection new.
    self people do: [:each | each isMarried & each isMan
ifTrue: [result add: each]].
    ^result
unmarriedWomen
    | result |
    result := OrderedCollection new.
    self people do: [:each | each isUnmarried & each
isWoman ifTrue: [result add: each]].
    ^result


이제 문제는 두 메소드를 어떻게 작성하느냐이다. 이렇게 간단한 예제라면 필자는 Concatenation을 사용해 아래와 같은 코드를 작성할 것이다:

marriedMenAndUnmarriedWomen
    ^self marriedMen , self unmarriedWomen


하지만 위의 코드는 이 패턴을 그다지 잘 보여주지는 못한다. 여러 메소드 계층, 또는 여러 객체들이 포함된 경우 서브메소드를 수정하는 것이 더 명확하다. Collection을 리턴하는 대신 각자 그 객체들을 Collection에 추가한다. 그러면 코드는 다음과 같이 바뀔 것이다:

marriedMenAndUnmarriedWomen
    | result |
    result := OrderedCollection new.
    self addMarriedMenTo: result.
    self addUnmarriedWomenTo: result.
    ^result
addMarriedMenTo: aCollection
    self people do: [:each | each isMarried & each isMan
ifTrue: [aCollection add: each]]
addUnmarriedWomenTo: aCollection
    self people do: [:each | each isUnmarried & each
isWoman ifTrue: [aCollection add: each]]


위의 코드는 본래 코드보다 행의 수는 적지만 더 직접적이다. (제품 코드였다면 아마 Composed Method를 통한 팩토링을 계속하여 addMarriedMenTo:와 addUnmarriedWomenTo: 간에 유사점에 중점을 두었을 것이다.)


보통은 OrderedCollection(p.116)을 Collecting Parameter로 사용하라. 수집할 객체가 바이트이거나 Characters일 경우 Concatenating Stream(p.165)를 Collecting Parameter 로서 사용해도 좋다. 중복을 피하고 싶다면 Set(p.119)를 사용하라.


Notes