bottom prev next

Extender

The Extender pattern may be used to extend a behavior of an existing data type via its code reuse at compile time.

Let us agree that:

- an existing data type whose behavior is being extended is called an ancestor

- a new data type that implements that extension is called a descendant

- when a descendant's behavior is extended it becomes an ancestor

- such an arrangement of descendants and ancestors forms a hierarchy

- an immediate ancestor is called a parent

- an immediate descendant is called a child

- within any given hierarchy any participating data type has at most one parent and an arbitrary (finite) number of children

- a hierarchy is a tree - a structure with no cycles

- a descendant exhibits all the fundamental traits of all of its ancestors but the opposite is not true


In the above hierarchy \(T\) is an abstract data type that defines a certain interface and suggests certain behavioral traits.

By being the first concrete data type to implement \(T\)'s interface \(T_1\) originates a new hierarchical branch, looks like \(T\) to a side observer and establishes a certain type of \(T\)-like behavior. \(T_1\) does not have any concrete ancestors but it does have one child, \(T_2\).

\(T_2\)'s parent is \(T_1\). As a data type \(T_2\) looks like \(T\) and behaves like \(T\) and \(T_1\) but the opposite is not true - \(T_1\) does not behave like \(T_2\) because the sole purpose of \(T_2\)'s existence is to augment \(T_1\)'s behavior in some way. To be sure, \(T_2\) does preserve the fundamental behavioral traits of \(T\) and \(T_1\) but it also introduces a certain extension to these traits. Symbolically:

$$T_2 - T_1 = \Delta > 0$$

\(T_2\) has two children, \(T_3\) and \(\tau_3\), each of which has only one (and the same) parent, \(T_2\). Both types, \(T_3\) and \(\tau_3\), differ somehow from each other and both augment \(T_2\)'s behavior in some \(T_3\)- and \(\tau_3\)-specific ways and so on.

Hierarchies comprised of an arbitrary finite number of branches each of which is of an arbitrary finite length may be constructed that way as long as the resulting structure remains to be a tree - a formation of nodes with no cycles.

By comparison, as defined Factory provides implementational width while Extender provides the corresponding depth.

In practice, for example, ed is a line-oriented text editor that provides certain basic text-editing functionality. ex extends ed by making text editing more visual or screen-oriented. vi extends ex by taking full advantage of ex's display editing capabilities. vim extends vi with colorful syntax and a more dynamic on-screen text manipulation with a peripheral device other than keyboard - mouse.

The Extender pattern consequences:

  • Behavior's extension is fixed at compile time

  • Common interface constancy throughout the hierarchy - all the descendants must implement their immediate ancestor's interface in its entirely - they can neither reduce nor expand it


Sample Problem

Implement a hierarchy of the following geometric shapes: oval, ellipse and circle by capturing the extension of ancestors' behavior by descendants in shapeDescribe() function:

- an abstract geometric shape should print the string "I am a geometric shape" to stdout

- an oval should augment the behavior of an abstract geometric shape by adding its own "I am an oval" part to the printout

- an ellipse should add its own "I am an ellipse" part to the printout

- a circle should add its own "I am a circle" part to the printout


Generic Solution Description

1) Originate a hierarchy with a data type implemented as Factory.

2) Make a parent of each child its integral or a non-integral owned part.

3) As appropriate extend parent's behavior in the child for each function of the interface.


Sample Solution

Step 1

Implement the generic geometric shape data type as Factory, see Factory chapter for details:

  • libshape.h
  • libshape.c
  • libshapeoval.c
  • libshapeellipse.c
  • libshapecircle.c

libshape.h:

#include <sys/types.h> typedef struct shape { void ( *describe )( struct shape* ); void ( *destruct )( struct shape* ); } shape_t; extern shape_t* shapeNew( const char* ); extern void shapeDelete( shape_t** ); extern shape_t* shapeConstruct( void*, const char* ); extern void shapeDestruct( shape_t* ); extern size_t shapeSizeOf( const char* ); extern void shapeDescribe( shape_t* );


Step 2

Make a parent an integral part of its child. Borrow the corresponding functions from Integral Parts chapter and add their prototypes to libshape.h:

extern size_t logicalSize( size_t, size_t ); extern size_t paddingAmount( size_t, size_t ); extern void* nextAddress( void*, size_t, size_t );


Step 3

Implement the geometric shape Factory proper yourself, consult Factory chapter if needed.

Below we highlight our version of shapeDescribe().

libshape.c:

extern void shapeDescribe( shape_t* shape ) { printf( "%s.%d: shapeDescribe(): I am a geometric shape.\n", __FILE__, __LINE__ ); shape->describe( shape ); }


Step 4

Implement an oval geometric shape.

An oval is the very first geometric shape in this particular branch - it does not have a parent as per above definition. As such, implement this data type as described in Factory chapter, there is no need to take any special action.

Below we highlight our version of oval's shapeDescribe().

libshapeoval.c:

static void shapeDescribeOval( shape_t* shape ) { shapeoval_t* oval = ( shapeoval_t* )shape; printf( " %s.%d: shapeDescribeOval(): I am an oval.\n", __FILE__, __LINE__ ); }


Step 5

Implement an ellipse geometric shape.

An ellipse is the second geometric shape in this particular branch. As such, make an oval - an ellipse's parent - an integral part of the ellipse's implementation data type.

Observe that when a variable of data type ellipse is created then effectively one extra variable is created also - of data type oval.

libshapeellipse.c:

extern size_t shapeSizeOfellipse() { size_t rv; rv = sizeof( shapeellipse_t ) + paddingAmount( sizeof( shapeellipse_t ), shapeSizeOf( "oval" ) ) + shapeSizeOf( "oval" ); return rv; } extern shape_t* shapeConstructellipse( void* mem ) { void* adrs; shapeellipse_t* ellipse = ( shapeellipse_t* )mem; ellipse->shapeadt = ellipseAdt; adrs = nextAddress( mem, sizeof( shapeellipse_t ), shapeSizeOf( "oval" ) ); ellipse->oval = shapeConstruct( adrs, "oval" ); return &ellipse->shapeadt; }


Step 6

Being an ellipse's integral part, an oval must be destructed when an ellipse is destructed. Observe that to destruct an oval we invoke the oval-specific destruction function - not its generic equivalent, shapeDestruct().

libshapeellipse.c:

static void shapeDestructEllipse( shape_t* shape ) { shapeellipse_t* ellipse = ( shapeellipse_t* )shape; ellipse->oval->destruct( ellipse->oval ); printf( " %s.%d: shapeDestructEllipse()\n", __FILE__, __LINE__ ); }


Step 7

Extend oval's behavior in the ellipse's version of shapeDescribe().

We observe that a child can augment its parent's behavior by acting before, after or before and after invoking the corresponding behavior of its parent. A child may also choose to not invoke its parent corresponding behavior at all.

In our case we first invoke parent's behavior and then we augment it. Observe that to exercise the oval-specific behavior we invoke the oval-specific function - not its generic equivalent, shapeDescribe().

libshapeellipse.c:

static void shapeDescribeEllipse( shape_t* shape ) { shapeellipse_t* ellipse = ( shapeellipse_t* )shape; ellipse->oval->describe( ellipse->oval ); printf( " %s.%d: shapeDescribeEllipse(): I am an ellipse.\n", __FILE__, __LINE__ ); }


Step 8

Implement a circle geometric shape.

A circle is the third geometric shape in this particular branch of geometric shapes. Make an ellipse - the circle's parent - an integral part of the circle's implementation data type.

Observe that when a variable of data type circle is created then effectively two extra variables are created also - one of data type ellipse and one of data type circle.

libshapecircle.c:

extern size_t shapeSizeOfcircle() { size_t rv; rv = sizeof( shapecircle_t ) + paddingAmount( sizeof( shapecircle_t ), shapeSizeOf( "ellipse" ) ) + shapeSizeOf( "ellipse" ); return rv; } extern shape_t* shapeConstructcircle( void* mem ) { void* adrs; shapecircle_t* circle = ( shapecircle_t* )mem; circle->shapeadt = circleAdt; adrs = nextAddress( mem, sizeof( shapecircle_t ), shapeSizeOf( "ellipse" ) ); circle->ellipse = shapeConstruct( adrs, "ellipse" ); return &circle->shapeadt; } static void shapeDestructCircle( shape_t* shape ) { shapecircle_t* circle = ( shapecircle_t* )shape; circle->ellipse->destruct( circle->ellipse ); printf( " %s.%d: shapeDestructCircle()\n", __FILE__, __LINE__ ); }


Step 9

Extend ellipse's behavior by adding circle-specific actions to it in the circle's version of shapeDescribe().

libshapecircle.c:

static void shapeDescribeCircle( shape_t* shape ) { shapecircle_t* circle = ( shapecircle_t* )shape; circle->ellipse->describe( circle->ellipse ); printf( " %s.%d: shapeDescribeCircle(): I am a circle.\n", __FILE__, __LINE__ ); }


Step 10

Build the Geometric Shapes Extender, GSE.

SLBTF:

gcc -D_GNU_SOURCE -g -c -fPIC -I . libshape.c gcc -g -c -fPIC -I . libshapeoval.c gcc -g -c -fPIC -I . libshapeellipse.c gcc -g -c -fPIC -I . libshapecircle.c gcc -g -shared -o libshape.so \ libshape.o \ libshapeoval.o \ libshapeellipse.o \ libshapecircle.o -ldl


MLBTF:

gcc -D_GNU_SOURCE -g -c -fPIC -I . libshape.c gcc -g -shared -o libshape.so libshape.o -ldl gcc -g -c -fPIC -I . libshapeoval.c gcc -g -shared -o libshapeoval.so libshapeoval.o gcc -g -c -fPIC -I . libshapeellipse.c gcc -g -shared -o libshapeellipse.so libshapeellipse.o gcc -g -c -fPIC -I . libshapecircle.c gcc -g -shared -o libshapecircle.so libshapecircle.o


MLRTF:

Add the -DGSE_MLRTF option to the libshape.so library's build line:

gcc -D_GNU_SOURCE -DGSE_MLRTF -g -c -fPIC -I . libshape.c


Step 11

Write a sample application to exercise the Extender pattern.

Our version accepts a concrete name of a geometric shape as a command line argument and exercises its behavior.

shape.c:

#include <stdio.h> #include <stdlib.h> #include "libshape.h" extern int main( int argc, char* argv[] ) { shape_t* shape; if ( argc < 2 ) { return -1; } shape = shapeNew( argv[ 1 ] ); shapeDescribe( shape ); shapeDelete( &shape ); return 0; }


Step 12

Build the sample application:

SLBTF and MLRTF:

gcc -g -c -I . shape.c gcc -g -L . -o shape shape.o -lshape


MLBTF:

gcc -g -c -I . shape.c gcc -g -L . -o shape shape.o -lshape \ -lshapeoval \ -lshapeellipse \ -lshapecircle


Step 13

Run the sample program with various inputs:

./shape oval libshape.c.83: shapeDescribe(): I am a geometric shape. libshapeoval.c.40: shapeDescribeOval(): I am an oval. libshapeoval.c.49: shapeDestructOval() ./shape ellipse libshape.c.83: shapeDescribe(): I am a geometric shape. libshapeoval.c.40: shapeDescribeOval(): I am an oval. libshapeellipse.c.61: shapeDescribeEllipse(): I am an ellipse. libshapeoval.c.49: shapeDestructOval() libshapeellipse.c.72: shapeDestructEllipse() ./shape circle libshape.c.83: shapeDescribe(): I am a geometric shape. libshapeoval.c.40: shapeDescribeOval(): I am an oval. libshapeellipse.c.61: shapeDescribeEllipse(): I am an ellipse. libshapecircle.c.61: shapeDescribeCircle(): I am a circle. libshapeoval.c.49: shapeDestructOval() libshapeellipse.c.72: shapeDestructEllipse() libshapecircle.c.72: shapeDestructCircle()


Exercises

1) Explain the sequence of function calls produced in the output of the sample program.


Files

libshape.h libshape.c libshapeoval.c libshapeellipse.c libshapecircle.c mklib.sh

shape.c mkapp.sh

\(\blacksquare\)

top prev next