Design‎ > ‎Classes and Interfaces‎ > ‎

Class composition

Let's use an example: GUI widgets often use wrappers.  A good example is adding a border around a rectangular widget.  This wrapper can be used for different widgets, e.g., a label and an image.  We want to define and implement the wrapper only once and then use it for multiple widgets. Let us first do this with single inheritance:

         CLASS Label IMPLEMENTS I_Widget
            PROC $draw(Canvas c, Pos p) @default
               # draw the label
            }
            ...
         }
         CLASS Image IMPLEMENTS I_Widget
            PROC $draw(Canvas c, Pos p) @default
               # draw the image
            }
            ...
         }
         CLASS Border
            int $thickness
            PROC $draw(Canvas c, Pos p)
               # draw the border
            }
            FUNC $addOffset(Pos p): Pos
              RETURN NEW(p.x + $thickness, p.y + $thickness)
            }
         }
         CLASS BorderedLabel EXTENDS Label
            Border $border
            PROC $draw(Canvas c, Pos p) @replace
               # draw the label
               PARENT.draw(c, $border.addOffset(p))
               # draw the border around the label
               $border.draw(c, p)
            }
            ...
         }
         CLASS BorderedImage EXTENDS Image
            Border $border
            PROC $draw(Canvas c, Pos p) @replace
               # draw the image
               PARENT.draw(c, $border.addOffset(p))
               # draw the border around the label
               $border.draw(c, p)
            }
            ...
         }
         ...
         I_Widget label = BorderedLabel.NEW("Hello")
         I_Widget image = BorderedImage.NEW(helloImage)

We see the same mechanism repeated twice in the draw() method of BorderedLabel and BorderedImage.  It is not much in this simplified example, in a real implementation it can be substantial.  Also, the signature of the draw() method needs to be repeated.  Not good.

Alternative 1: use composition.

         CLASS Label IMPLEMENTS I_Widget
            ... as above
         }
         CLASS Image IMPLEMENTS I_Widget
            ... as above
         }
         CLASS Border
            ... as above
         }
         CLASS Bordered IMPLEMENTS I_Widget
            I_Widget $child
            Border   $border
            NEW(I_Widget child)
                $child = child
            }
            ...
            PROC $draw(Canvas c, Pos p)
               # draw the widget
               $child.draw(c, $addOffset(p))
               # draw the border around the widget
               $border.draw(c, p)
            }
            ...
         }
         ...
         I_Widget label = Bordered.NEW(Label.NEW("Hello"))
         I_Widget image = Bordered.NEW(Image.NEW(helloImage))

This moves the repetition to where the classes are instantiated.  Not good. Also, changing the label text or image properties now requires using Bordered.child and casting it to a Label or Image.

Alternative 2: As above, but with subclasses

         CLASS Label IMPLEMENTS I_Widget
            ...  as above
         }
         CLASS Image IMPLEMENTS I_Widget
            ... as above
         }
         CLASS Border
            ... as above
         }
         CLASS Bordered IMPLEMENTS I_Widget
            ... as above
         }
         CLASS BorderedLabel EXTENDS Label
            Bordered $bordered
            NEW(string text)
               $bordered = NEW(THIS)
            }
            PROC $draw(Canvas c, Pos p) @replace
               $bordered.draw(c, p)
            }
         }
         CLASS BorderedImage EXTENDS Image
            Bordered $bordered
            NEW(Image name)
               $bordered = NEW(THIS)
            }
            PROC $draw(Canvas c, Pos p) @replace
               $bordered.draw(c, p)
            }
         }
         ...
         I_Widget label = BorderedLabel.NEW("Hello")
         I_Widget image = BorderedImage.NEW(helloImage)

Not really getting better.  Some of the draw logic is now in Bordered, at the cost of two extra classes.

Alternative 3: use PIECE and INCLUDE

         CLASS Label IMPLEMENTS I_Widget
            ... as above
         }
         CLASS Image IMPLEMENTS I_Widget
            ... as above
         }
         PIECE Border
            int $borderThickness
            PROC $drawBorder(Canvas c, Pos p) @local
               # draw the border
            }
            FUNC $addOffset(Pos p) Pos @local
              RETURN NEW(p.x + $borderThickness, p.y + $borderThickness)
            }

            PROC $draw(Canvas c, Pos p)
               # draw the widget
               $drawWidget(c, $addOffset(p))
               # draw the border around the widget
               $drawBorder(c, p)
            }
            ...
         }
         CLASS BorderLabel EXTENDS Label
            INCLUDE
              Border $border
              $draw -> $border.draw
              $border.drawWidget -> $draw
            }
         }
         CLASS BorderImage EXTENDS Image
            INCLUDE
              Border $border
              $draw -> $border.draw
              $border.drawWidget -> $draw
            }
         }
         ...
         I_Widget label = BorderedLabel.NEW("Hello")
         I_Wi
dget image = BorderedImage.NEW(helloImage)

That looks better.  No code or signatures are repeated.

The INCLUDE block declares the pieces that are included and the connections.  All functionality of the included pieces is available as if it was copied into the class.  With the exception of what is changed with connections, the lines with ->.

The connections link one symbol to another.  When the first one is invoked this results in the second one being used.  In the example the original draw() method is connected to border.draw(), the draw() method of the border.  This method then
in turn invokes drawWidget(), which is connected to the original draw() method of Image.

When a connection is defined this also implies that the target is removed from the scope, it can no longer be accessed.  It is only available through the connection.

The addOffset() and drawBorder() methods are declared @local, because they should not be available on the class that uses a Border.  BorderLabel.addOffset() doesn't make much sense.

When defining a piece the compiler detects both an outside interface, what is made available by methods and attributes, and an inside interface, what is required by methods and attributes that are undefined in the piece.  The class that includes the piece must define the inside interface, otherwise the compiler will complain.

Now it is easy to make a double border:

         CLASS DoubleBorderLabel EXTENDS Label
            INCLUDE
              Border $inner
              Border $outer

              $draw -> $outer.draw
              $outer.drawWidget -> $inner.draw
              $inner.drawWidget -> $draw
              $innerBorderThickness -> $inner.borderThickness
            }
         }

We need to handle the two identical thickness attributes.  The outer one remains visible through its original name, the inner one is renamed with a connection to innerBorderThickness.  If that connection would be missing the compiler would produce an error for borderThickness being defined twice.

Note that we need to specify a different color for the inner and outer border for this to be useful.

INCLUDE syntax

It is useful to keep all the include statements together.  Just like all the members shared between objects are in the SHARED section.  Also, the statements for rewiring can be confusing if they are put somewhere else than close to where the piece is included.

The way to do this is to make INCLUDE a block.  There can be multiple INCLUDE blocks, but they cannot make connections outside of their block.


Wrapping

Another mechanism is to wrap a method with another method.  One could add locks to the Stream class in this way:

        PIECE Locker
           Lock $lock = NEW()
           WRAPPER $doLocked()
             $lock.obtain()
             WRAPPED()
             $lock.release()
           }
        }
        CLASS LockedStream EXTENDS Stream
           INCLUDE
              Locker $locker
              $read <> $locker.doLocked
              $write <> $locker.doLocked
              $eof <> $locker.doLocked
           }
        }


The <> statements are similar to the -> for making a connection, except that it works in two directions.  The PARENT() method calls back to the original method.

Note that the WRAPPER definition does not define a signature. The signature (argument types and return type) are inherited from the original method.

This assumes that one knows exactly which methods of Stream require the lock.  This fails miserably if the Stream class is extended with another method in a new release, e.g. readByte().  The compiler would not give an error message for the missing wrapper.  So
let's not do it this way, it leads to mistakes.

A safer way is to explicitly connect all methods of Stream.  This means we do not use inheritance but composition:

        CLASS LockedStream IMPLEMENTS Stream.I
           Locker $locker = NEW()
           Stream $stream

           NEW -> $stream.NEW
           $read -> $stream.read
           $write -> $stream.write
           $eof -> $stream.eof
           ...
           $stream.read <> $locker.doLocked

           $stream.write <> $locker.doLocked
           $stream.eof <> $locker.doLocked
        }

If Stream is now extended the compiler will give an error, because the new method is missing from LockedStream, while it is part of the Stream.I interface.

This is better than normal composition, since we do not need to repeat all the method signatures.  But we still need to make a long list of connections.  And the author of Stream can unintentionally break our LockedStream class.

Another way is to wrap all methods and make an exception for methods that we are sure don't need wrapping.  This again can be done by inheritance:

        CLASS LockedStream EXTENDS Stream
           Locker $locker = NEW()

           ["*", ^NEW, ^getName] <> $locker.doLocked
        }

Here the NEW() and getName() methods are not wrapped, and all the remaining methods are.  The list before <> has the pattern "*", which matches all items, and then two items that are excluded, marked with ^.

This is safe, the lock will keep working even when Stream is changed.  It might be a bit inefficient when a new method is added that doesn't require a lock, but it won't break.

MIXIN, TRAIT or PIECE?

The most promising looks stateful traits: A block of functionality with methods and members that can easily be re-used.  The state is necessary to be able for the trait to hide implementation details from where it's used.

Trait is a strange name and we don't want to promise to provide what others call a trait. We should call it Piece or Part.  I tend to like PIECE, as in "piece of a class".

The piece can have an $INIT() method, so that classes that use it will automatically initialize the piece, no wiring required.

A piece defines an interface, just like a class defines an interface.  Possibly, it can also specify further interfaces it implements.  A class that includes it, without rewiring methods, then automatically also implements these interfaces.

Example with color methods

Assume a piece that defines methods to set a color in various ways and obtain it in RGB.  Then this color could be used for the foreground color of a Circle widget, but also for the background and border color.  The widget would expose this with methods such as setColor(), setBgColor() and setBorderColor().

Circle inherits from Widget
includes ColorPiece (various ways to set the color)
setColor(r, g, b)
setColor(name)
etc.
include BgColorPice (set the background color)
setBgColor(r, g, b)
setBgColor(name)
etc.
includes BorderColorPiece (set the border color)
setBorderColor(r, g, b)
setBorderColor(name)
etc.
BorderColorPiece
adds ColorPiece and renames the methods with "border", e.g.
setColor(r, g, b) to setBorderColor(r, g, b)


Example with container classes

A container contains zero or more items of a known type.  Different containers have different ways of accessing the items.  A List accesses the items by index, a Dict by key.  Some operations can be done on any container, for example to get the item with the maximum or minimum value. This does not require knowledge of the access method, thus exactly the same code can be used for all container classes, so long as they define an interface to iterate over all items.

PIECE ContainerLimits<Titem> USES I.Iterable<Titem>
  FUNC $max() Titem
    Titem max
    bool didFirst
    FOR item IN $iterator()
      IF !didFirst || max.COMPARE(item) < 0
        max = item
        didFirst = TRUE
      }
    }
    RETURN max
  }
  FUNC $min() Titem
    Titem min
    bool didFirst
    FOR item IN $iterator()
      IF !didFirst || min.COMPARE(item) > 0
        min = item
        didFirst = TRUE
      }
    }
    RETURN min
  }
}

The <Titem> specifies that this is a templated piece.  When included the type must be specified.
The USES clause says what the class must provide on the interface between the piece and the class.
This is not required, but it does make it easier to find mistakes.

Now this piece can be used in any container that implements $iterator(), for example SortedList:

CLASS SortedList<Titem> IMPLEMENTS I.Iterable<Titem>
  list<Titem> $items @private  # stores the items
  FUNC $iterator() I.Iterator<Titem>
    RETURN $items.iterator()
  }

  # ... more SortedList members go here

  # Include $max() and $min() in the class.
  INCLUDE ContainerLimits<Titem>
}




Comments