Affordances: Specialized Collections

Most programming languages provide a library of collection types for
use in our programs, such as arrays, lists, sets, and hashes/maps.

Often, we need to implement a domain class that acts like a built-in
collection, but also provides some domain-specific behavior.

The first solution a new programmer might reach for is to subclass
from the desired collection type. This can work, but is generally not
the best design. Many times, these collection-like domain classes
need to inherit from some other domain class instead, and in most
languages, you only get to play the inheritance card once.

A better solution is for the domain class to have an instance variable
of the desired collection type and to delegate any collection messages
to the internal collection.

As much as I hate to admit it, Smalltalk doesn’t provide affordances
for this. It forces us to manually implement any of the collection
messages we want to expose to clients of the domain class:

Collection-Like Object in Smalltalk

SmalltalkdefineClass:#Points

superclass:#{Core.Object}

indexedType:#none

private:false

instanceVariableNames:'points'

classInstanceVariableNames:''

imports:''

category:'Examples'

...

Points>>addPoint:aPoint

^pointsadd:aPoint

Points>>do:aBlock

^pointsdo:aBlock

Points>>collect:aBlock

^pointscollect:aBlock

etc.

C++ fares better because collection operations are template functions
that operate on a pair of iterators, rather than methods on a
particular class. All we have to do in our domain class is expose
the iterators:

Collection-Like Object in C++

classPoints

{

public:

typedefstd::vector<Point>Collection;

voidaddPoint(constPoint&point){points.push_back(point);}

Collection::iteratorbegin(){returnpoints.begin();}

Collection::iteratorend(){returnpoints.end();}

Collection::const_iteratorbegin()const{returnpoints.begin();}

Collection::const_iteratorend()const{returnpoints.end();}

// ...

private:

Collectionpoints;

};

Having to define both const and non-const versions of the methods
is somewhat painful, but less so than having to implement all of the
collection methods.

Ruby, with its mixins and the Enumerable module,
provides really nice affordances for this pattern:

Collection-Like Object in Ruby

classPoints

includeEnumerable

definitialize

@points=Array.new

end

def<<(point)

@points<<point

end

defeach(&block)

@points.each(&block)

end

Enumerable implements all of the collection methods in terms of
each, so as long as we provide a reasonable implementation of
each, Enumerable takes care of the rest for us.

If we also make use of the Forwardable module, we can allow it to
implement our delegating methods for us too:

Even Simpler with Forwardable

classPoints

includeEnumerable

extendForwardable

def_delegators:@points,:each,:<<

definitialize

@points=Array.new

end

Because of the affordances that Ruby and C++ provide, it is easier to
provide a complete solution where there are no surprises about which
collection methods are available on the domain class. Since Smalltalk
doesn’t provide these affordances, it is likely that we’ll only
implement a few of the collection methods rather than the full set.