Collection wrapper

Problem:

What is the best way to provide custom views of collections of domain objects, when they may occur in many different contexts?

Example

When we explore a website, we have lists of links in a website or a webpage, we have lists of internal links, external links, broken links, or queries returning links that match some text. Providing custom views for each of these is tedious and leads to much duplicated code.

Forces

It is common to have to deal with collections of domain objects, but it is tedious to create custom views for each of these. Collections may form part of the internal state of a domain object, such as a website containing pages, or a page containing links, but a collection may also be returned by a query, such as a Spotter search, forexample the set of web pages containing a certain string in their title or page contents.

Raw collections have uninteresting views. Collections are very similar and have a lot of boilerplate code. Many views are essentially collections, and have similar implementations.

Solution

Wrap collections of domain objects in a dedicated group wrapper, and give it dedicated views, actions and searches.

Steps

Create a dedicated wrapper for a collection of domain entities. An example is WebPageGroup Object subclass: #WebPageGroup uses: TGtGroupWithItems + TWebPageGroup instanceVariableNames: '' classVariableNames: '' package: 'GToolkit-Demo-WebsiteExplorer-Model' , which wraps a collection web pages.

Have the collection wrapper use the TGtGroupWithItems Trait named: #TGtGroupWithItems uses: TGtGroup + TGtGroupItems instanceVariableNames: '' package: 'GToolkit-Utility-System' trait. This trait has an items slot for the wrapped collection, and provides a large number of Collection Object subclass: #Collection instanceVariableNames: '' classVariableNames: '' package: 'Collections-Abstract-Base' methods to make the trait user behave like a Collection.

In case there are other classes that share views, actions and searches with the collection wrapper, consider putting these in a shared trait as well. For example, a website is also a collection of pages. We put the shared views and other behavior in the trait TWebPageGroup Trait named: #TWebPageGroup instanceVariableNames: '' package: 'GToolkit-Demo-WebsiteExplorer-Model' . The trait users are AWebsite AWebLinkGroup subclass: #AWebsite uses: TWebPageGroup instanceVariableNames: 'pages repoDir url linkDict reachable rootPages links repoUrl' classVariableNames: '' package: 'GToolkit-Demo-WebsiteExplorer-Model' and WebPageGroup Object subclass: #WebPageGroup uses: TGtGroupWithItems + TWebPageGroup instanceVariableNames: '' classVariableNames: '' package: 'GToolkit-Demo-WebsiteExplorer-Model' .

Wherever you need to show a collection of these objects, just forward the view to the wrapper to avoid duplicating the view code. See TWebPageGroup>>#gtReachablePagesFor: gtReachablePagesFor: aView "<gtView>" self contentAvailable ifFalse: [ ^ aView empty ]. ^ aView forward title: 'Reachable pages'; priority: 60; object: [ self reachable ]; view: #gtPagesFor: .

If you have gtSearches, be sure they return the collection wrapper and not the raw collection, using the sendCategory: message, for example, TWebPageGroup>>#gtSpotterForPageContents: gtSpotterForPageContents: aSearch <gtSearch> ^ aSearch list title: 'Page content'; priority: 20; items: [ self pages items ]; itemsLimit: Float infinity; itemName: #contents; sendCategory: [ :aCategory :aStep :thisSearch | self webPageGroupClass withAll: aCategory items contents ]; filterBySubstring .

Related patterns

A collection wrapper is similar to a Viewable data wrapper, since it also serves to make raw data moldable. The difference lies in the specifics of making the wrappers behave like collections, and make sure that any collections returned by queries are also wrapped.