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
(or rather ProtoObject
).
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:
, 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
will intercept any messages in its #doesNotUnderstand:
method, and generate an accessor if the message sent matches any of its slot names: DynamicAccessors>>#doesNotUnderstand:
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:
will forward messages not understood by the value holder to the collection value itself. If we wrap the strip 'foo bar'
into a CollectionValueHolder
, then we see that this class does
not
understand the message #asCamelCase
(which is implemented in String
).
'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
.
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
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
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
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:
Messages sent to it are added to a log, and then forward to the subject: LoggingProxy>>#doesNotUnderstand:
Here's an example that creates a proxy for a Point
.
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