Overriding doesNotUnderstand:

TL;DR

When an object receives a message that neither it nor any of its superclasses understand, it sends itself the message #doesNotUnderstand: with the reified message as an argument. Although this normally produces an error, triggering the debugger, it is possible to override the default implementation of #doesNotUnderstand: to perform some other useful task.

Understanding doesNotUnderstand:

As we have seen in Understanding Smalltalk classes and metaclasses, when an object receives a message, the message selector is looked up in the method dictionary of its class. If no method is found with this selector, lookup continues up the class hierarchy all the way to Object ProtoObject subclass: #Object instanceVariableNames: '' classVariableNames: 'DependentsFields' package: 'Kernel-Objects' (or rather ProtoObject ProtoObject subclass: #ProtoObject instanceVariableNames: '' classVariableNames: '' package: 'Kernel-Objects'. ProtoObject superclass: nil ).

If a method is still not found, then the object sends a doesNotUnderstand: message to itself with the reified message as its argument. The default behavior for this message is the method Object>>#doesNotUnderstand: doesNotUnderstand: aMessage <debuggerCompleteToSender> "Handle the fact that there was an attempt to send the given message to the receiver but the receiver does not understand this message (typically sent from the machine when a message is sent to the receiver and no method is defined for that selector)." "Testing: (3 activeProcess)" | exception resumeValue | (exception := MessageNotUnderstood new) message: aMessage; receiver: self. resumeValue := exception signal. ^exception reachedDefaultHandler ifTrue: [aMessage sentTo: self] ifFalse: [resumeValue] , which will normally fire up the debugger.

This is what happens, for example, if we send:

Object new foo.
  

However — and this is the interesting part — the #doesNotUnderstand:method can be overridden by subclasses to do something different. There are quite a few examples in the system:

#doesNotUnderstand: gtImplementors 
  

Dynamic accesple:ors

For example, DynamicAccessors Object subclass: #DynamicAccessors instanceVariableNames: 'x' classVariableNames: '' package: 'GToolkit-Demo-Reflection-Intercession' will intercept any messages in its #doesNotUnderstand: method, and generate an accessor if the message sent matches any of its slot names: DynamicAccessors>>#doesNotUnderstand: doesNotUnderstand: aMessage | messageName | messageName := aMessage selector asString. (self class instVarNames includes: messageName) ifTrue: [self class compile: messageName , String cr , ' ^ ' , messageName. ^ aMessage sendTo: self]. super doesNotUnderstand: aMessage

We can see it in action in this example, where we send the (unimplemented) message #x, and an accessor for the x slot is then dynamically compiled.

dynamicAccessor
	<gtExample>
	
	| dynamicAccessor |
	dynamicAccessor := DynamicAccessors new.
	
	self deny: (dynamicAccessor class methodDict keys includes: #x).
	self assert: dynamicAccessor x equals: nil.
	self assert: (dynamicAccessor class methodDict keys includes: #x).
	DynamicAccessors removeSelector: #x.
	
	^ dynamicAccessor
    

Forwarding value holders

As another example, CollectionValueHolder>>#doesNotUnderstand: doesNotUnderstand: aMessage ^ (value respondsTo: aMessage selector) ifTrue: [ value perform: aMessage selector withEnoughArguments: aMessage arguments ] ifFalse: [ super doesNotUnderstand: aMessage ] will forward messages not understood by the value holder to the collection value itself. If we wrap the strip 'foo bar' into a CollectionValueHolder NewValueHolder subclass: #CollectionValueHolder instanceVariableNames: '' classVariableNames: '' package: 'NewValueHolder-Core-Base' , then we see that this class does not understand the message #asCamelCase (which is implemented in String ArrayedCollection subclass: #String instanceVariableNames: '' classVariableNames: 'AsciiOrder CSLineEnders CSNonSeparators CSSeparators CaseInsensitiveOrder CaseSensitiveOrder LowercasingTable Tokenish TypeTable UppercasingTable' package: 'Collections-Strings-Base' ).

'foo bar' asValueHolder class canUnderstand: #asCamelCase.
  

If, however, we send #asCamelCase to the wrapped string, the #doesNotUnderstand: message will intercept it and forward it to the contained string value.

'foo bar' asValueHolder asCamelCase.
  

Minimal objects

A minimal object is very much like CollectionValueHolder NewValueHolder subclass: #CollectionValueHolder instanceVariableNames: '' classVariableNames: '' package: 'NewValueHolder-Core-Base' .

It wraps a normal object, does not implement (understand) very much, but it implements #doesNotUnderstand: to intercept all messages and forward them to the wrapped object, possible performing other before or after operations.

One problem with Smalltalk is that Object ProtoObject subclass: #Object instanceVariableNames: '' classVariableNames: 'DependentsFields' package: 'Kernel-Objects' implements a very large number of methods (about 600 at last count).

Object methods size.
  

For this reason, a minimal object should be defined as a subclass of ProtoObject ProtoObject subclass: #ProtoObject instanceVariableNames: '' classVariableNames: '' package: 'Kernel-Objects'. ProtoObject superclass: nil instead.

ProtoObject methods size.
  

In fact, most of the subclasses of ProtoObject implement #doesNotUnderstand:.

ProtoObject subclasses
	select: [ :class | 
		class selectors includes: #doesNotUnderstand: ].
  

Then, to transparently replace an object (AKA a “subject”) by a minimal object that wraps the subject, the minimal object can use #become: to swap all pointers to the subject to point to itself. A logging proxy is an example of this technique.

Logging proxy

LoggingProxy ProtoObject subclass: #LoggingProxy instanceVariableNames: 'subject messageLog' classVariableNames: '' package: 'GToolkit-Demo-Reflection-Intercession' is a minimal object that serves as a proxy for an existing subject, and logs messages sent to the subject by overriding #doesNotUnderstand:.

When we create a new proxy, we swap its pointers with that of its subject: LoggingProxy>>#for: for: aSubject self new become: aSubject. ^ aSubject

Messages sent to it are added to a log, and then forward to the subject: LoggingProxy>>#doesNotUnderstand: doesNotUnderstand: aMessage messageLog addLast: aMessage. ^ aMessage sendTo: subject

Here's an example that creates a proxy for a Point Object subclass: #Point instanceVariableNames: 'x y' classVariableNames: '' package: 'Kernel-BasicObjects' .

freshLoggingProxy
	"NB: The point and the proxy swap object ids."

	<gtExample>
	| point proxy |
	point := 1 @ 2.
	self assert: point class equals: Point.
	LoggingProxy for: point.
	self assert: point class equals: LoggingProxy.
	proxy := point.	"Alias"
	self assert: proxy subject class equals: Point.
	self assert: proxy messageLog isEmpty.
	^ proxy
    

Since we don't send any messages to the proxy, the log is empty at the end.

If we send it some messages, we can see the log increase in size.

usedLoggingProxy
	<gtExample>
	| proxy |
	proxy := self freshLoggingProxy.
	self assert: proxy messageLog isEmpty.
	self assert: proxy + (3 @ 4) equals: 4 @ 6.
	self assert: proxy printString equals: '(1@2)'.
	self assert: proxy messageLog size equals: 2.
	^ proxy
    

Caveat: self sends are not intercepted

One downsideis that self sends are directly sent to the subject, and won't be seen by the proxy. Here's an example: LoggingProxyExamples>>#loggingProxyWithSelfSends loggingProxyWithSelfSends "rectangle: does two self-sends, but self-sends are not intercepted, so only the send to #rectangle: is counted" <gtExample> | proxy | proxy := self freshLoggingProxy. self assert: proxy messageLog isEmpty. self assert: (proxy rectangle: 3 @ 4) area equals: 4. self deny: proxy messageLog size equals: 4. self assert: proxy messageLog size equals: 1. ^ proxy