This post is part of an ongoing series about Affordances and Programming Languages.

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
Smalltalk defineClass: #Points
superclass: #{Core.Object}
indexedType: #none
private: false
instanceVariableNames: 'points'
classInstanceVariableNames: ''
imports: ''
category: 'Examples'
...
Points>>addPoint: aPoint
^points add: aPoint
Points>>do: aBlock
^points do: aBlock
Points>>collect: aBlock
^points collect: 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++
class Points
{
public:
typedef std::vector<Point> Collection;
void addPoint(const Point& point) { points.push_back(point); }
Collection::iterator begin() { return points.begin(); }
Collection::iterator end() { return points.end(); }
Collection::const_iterator begin() const { return points.begin(); }
Collection::const_iterator end() const { return points.end(); }
// ...
private:
Collection points;
};

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
class Points
include Enumerable
def initialize
@points = Array.new
end
def <<(point)
@points << point
end
def each(&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
class Points
include Enumerable
extend Forwardable
def_delegators :@points, :each, :<<
def initialize
@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.