parent previous next question (Smalltalk Textbook 33)

EngiEncapsulator & EngiField

This section explores a curious thing: an encapsulator object (EngiEncapsulator) and a field object (EngiField). An encapsulator is a special object which wraps itself around another object like a hull to pass itself off as the inside object. The field is a place in which objects are kept. If you place more than one object in a field, the field can manage relationships between those objects without the objects even being aware of any relationships with other objects. Yes, it does sound strange, but understanding this section will make you a better designer, because you will understand how to design simple, self-contained objects and distinguish clearly between objects and relationships.

Let me give you an example instead of a conceptual description to aid understanding. Imagine a triangle, composed of three straight lines, each of which is made up of two points. In other words, the triangle is composed of 6 points, so coordinates of each apex of the triangle are doubled.

If you select one of the lines of the triangle with the mouse and move it, the other two lines must also somehow move to preserve the shape of the triangle. The moving line must somehow inform the others in spite of the fact that the line does not know it is part of a triangle.

Usually the triangle will keep track of it's shape rather than lines, because there is a 'has-a' relationship between the triangle and three lines. In this situation above, someone manipulates one of the lines directly by a message (select, red-button-down, or mouse-down or whatever), and it is possible that that somehow does not know the line is a component of a triangle, but just moves the line directly. Furthermore imagine a case in which various geometrical figures have a line in common. If you move the common line, all the geometrical figures must move at the same time in the same way.

You will often encounter this situation in Smalltalk programming. You can not put the control mechanism to manage the whole shape in each component, or you do not think it is a good idea for the whole shape object to manage its own components because a component is also part of another shape, or perhaps it is not a good idea that explicitly represent a temporal relation between instances in the class. How you can solve the problem that you have a relation of object that you can not program anywhere. This is where an encapsulator and a field come into play.

The encapsulator and field code are in Appendix 18.

File in Appendix 18 then do it program 33-1, you see two lines of statement in the transcript.


Program-33-1: (EngiField, EngiEncapsulator; place:)
----------------------------------------------------
| aField anObject |
aField := EngiField new.
anObject := aField place: 'Atsushi Aoki'.
aField
        before: anObject
        receive: #yourself
        do:
                [:arguments |
                Transcript
                        cr;
                        show: 'something to do before sending this message'].
aField
        after: anObject
        receive: #yourself
        do:
                [:arguments |
                Transcript
                        cr;
                        show: 'something to do after sending this message'].
anObject yourself
----------------------------------------------------

The program :

  1. creates a 'field' then puts a string of 'Atsushi Aoki' in it
  2. sends 'before:receive:do:' and 'after:receive:do:' messages to the field to specify the things to do before and after 'yourself' message is sent to the 'Atsushi Aoki' string in the field
  3. sends 'yourself' message to the 'Atsushi Aoki' field object.

The string 'Atsushi Aoki' which is a temporary variable stored in 'anObject' of the program is wrapped by a encapsulator. You can not distinguish the string of 'Atsushi Aoki' and the wrapped string of 'Atsushi Aoki', that is they are '=' (but not '=='). Wrapped objects are called encapsulated objects. An object is created by encapsulating data and procedures as you know. Then an encapsulated object is encapsulated once more. We call a hull object the 'encapsulator.'

The encapsulator understands all messages the encapsulated object understands. The encapsulator receives all messages sent to the encapsulated object. In Program 33-1, the encapsulator monitors every message received and asks the field whether it needs to do anything before or after receiving the message.

The encapsulator can wrap any object except an 'immediate' object ('SmallInteger' and 'Character'; try '39 isImmediate'). There are many objects in ObjectWorks or VisualWorks. They understand a lot of messages like a multi-linguist who can understand many language. Do you think it's possible to make such an object, one which understand ALL messages?

The answer is, of course, Yes! You will be surprised at how easy it is to do when you see the 'EngiEncapsulator' source in Appendix 18. File it in now and notice that 'nil' is designated as the super class of 'EngiEncapsulator' in order to completely isolate the class. If you check it in a browser, 'Object' seems to be the super class of 'EngiEncapsulator', but in fact 'EngiEncapsulator' has no super class. It seems as though it could not understand any message because it inherits nothing from anyone. However it can understand any message for the encapsulated object.

You need to examine the 'doesNotUnderstand:' instance message in 'EngiEncapsulator' to understand this paradox that the encapsulator understands all messages because the it does not understand _any_ messages! 'doesNotUnderstand:' is sent with a message as argument to an object whenever it does not handle a message which it received. By default, 'doesNotUnderstand:' usually opens a notifier (Look at its implementation in 'Object' which is the super class of all classes).

Since the encapsulator does not understand any messages, it always receives the 'doesNotUnderstand:' message, along with the name of the message it does not handle. The encapsulator's 'doesNotUnderstand:' method then forwards messages to the encapsulated objects using 'perform:withArguments:', which all Smalltalk objects understand. Before and after forwarding a message to the encapsulated abject, the encapsulator asks the field whether it has anything to do. The field has a dictionary of before and after things (beforePropagations and afterPropagations). Then the field can find block closures (things to do before and after sending a message) which are registered by 'before:receive:do:' and 'after:receive:do:'.

Now, I'll show you a program with a triangle as mentioned above. The triangle is implemented as an array which has three lines. Each line is implemented as an array which has two points.


Program-33-2: (EngiField, EngiEncapsulator; line, triangle)
-------------------------------------------------------------------
| aField line1 line2 line3 aTriangle activeWindow graphicsContext block |
aField := EngiField new.
line1 := aField place: (Array with: Point zero with: Point zero).
line2 := aField place: (Array with: Point zero with: Point zero).
line3 := aField place: (Array with: Point zero with: Point zero).
aTriangle := Array with: line1 with: line2 with: line3.
aField
        after: line1
        receive: #at:put:
        do:
                [:arguments |
                arguments first = 1
                        ifTrue: [line3 at: 2 put: line1 first].
                arguments first = 2
                        ifTrue: [line2 at: 1 put: line1 last]].
aField
        after: line2
        receive: #at:put:
        do:
                [:arguments |
                arguments first = 1
                        ifTrue: [line1 at: 2 put: line2 first].
                arguments first = 2
                        ifTrue: [line3 at: 1 put: line2 last]].
aField
        after: line3
        receive: #at:put:
        do:
                [:arguments |
                arguments first = 1
                        ifTrue: [line2 at: 2 put: line3 first].
                arguments first = 2
                        ifTrue: [line1 at: 1 put: line3 last]].
line1 at: 1 put: 75 @ 50.
line2 at: 1 put: 100 @ 100.
line3 at: 1 put: 50 @ 100.
activeWindow := ScheduledControllers activeController view.
graphicsContext := activeWindow graphicsContext.
block := [:pts| line1 at: 1 put: (pts at: 1).
        pts size > 1 ifTrue:[ line1 at: 2 put: (pts at: 2)].
        activeWindow clear.
aTriangle do: [:line | graphicsContext displayPolyline: line].
        activeWindow sensor waitClickButton.].
block value:  (Array with: 75 @ 50).
block value: (Array with:150 @ 50).
block value: (Array with:150 @ 50 with: 125 @ 125).
activeWindow display.
-------------------------------------------------------------------

One way to think about this is that the field enforces constraints between objects which do not know about each other.

Let me give you one more example. The following three gauges have a constraint that gauge 2 must read twice value of gauge 1 and gauge 3 must read three times gauge 1. Program 33-3 uses 'EngiGaugeModel' which we covered in a previous section.


Program-33-3: (EngiField, EngiEncapsulator, EngiGaugeModel, EngiGaugeView)
-------------------------------------------------------------
| aField gauge1 gauge2 gauge3 windowCreation |
aField := EngiField new.
gauge1 := aField place: EngiGaugeModel new.
gauge2 := aField place: EngiGaugeModel new.
gauge3 := aField place: EngiGaugeModel new.
aField
        after: gauge1
        receive: #value:
        do:
                [:arguments |
                gauge2 value: arguments first * 2.
                gauge3 value: arguments first * 3].
aField
        after: gauge2
        receive: #value:
        do:
                [:arguments |
                gauge1 value: arguments first * (1 / 2).
                gauge3 value: arguments first * 1.5].
aField
        after: gauge3
        receive: #value:
        do:
                [:arguments |
                gauge1 value: arguments first * (1 / 3).
                gauge2 value: arguments first * (1 / 1.5)].
windowCreation :=
        [:gaugeModel |
        | gaugeView edgeDecorator topWindow |
        gaugeView := EngiGaugeView
                                on: gaugeModel
                                get: #value
                                put: #value:
                                valuesMinMaxDivideRound: #(-100 100 10 1 ).
        edgeDecorator := LookPreferences edgeDecorator on: gaugeView.
        edgeDecorator noMenuBar.
        edgeDecorator noVerticalScrollBar.
        edgeDecorator noHorizontalScrollBar.
        topWindow := EngiTopView
                                model: nil
                                label: 'Gauge'
                                minimumSize: 100 @ 180.
        topWindow add: edgeDecorator in: (0 @ 0 corner: 1 @ 1).
        topWindow].
(windowCreation value: gauge1) open.
(windowCreation value: gauge2) open.
(windowCreation value: gauge3) open
-------------------------------------------------------------

I explained above how an encapsulator and field are suitable for handling temporary relations between instances which can not be described as relations of their classes. Also they can be used to give totality to parts. You can think of an encapsulator and field as the holon that Koestler proposed.

Perhaps you could think of a way to add declarative constraints to objects after they are already encapsulated, rather than at creation time as we've seen above with 'before:receive:do:' and 'after:receive:do:'.

We have made programs procedurally: "first do this, then do that, ... finally return something". However, it is difficult to represent declarative programs such as "A is always larger than B" or "object A and B should be bound be this constraint".

The encapsulator and field can be used for constraint programming, things like 'requireThat:' (pre-condition) and 'ensureThat:' (post-condition), or for contract based programming. Enjoy making some interesting examples.


parent previous next question
Copyright (C) 1994-1996 by Atsushi Aoki
Translated by Kaoru Rin Hayashi & Brent N. Reeves