I have a set of classes something like this:
abstract class CollectionAbs implements Iterator {
public function GetListAsXml() {...}
public function GetItemsByFilter(criteria: array) {...}
public function Sort(comparisonFunction) {...}
public function AddItem(newItem: CollectionItemAbs);
public function RemoveItem(newItem: CollectionItemAbs);
public function UpdateItem(newItem: CollectionItemAbs);
protected itemList: array of CollectionItemAbs;
}
abstract class CollectionItemAbs {
abstract public function Save();
abstract public function Load();
abstract public function Delete();
public function GetAsXml(): string {...}
public function ItemMatchesFilter(criteria: array): boolean {}
protected property1;
protected property2;
}
The idea is that a concrete instance of a CollectionItemAbs
implementation represents an item whose data is from a row in a database table, and corresponding concrete instances of CollectionAbs
provide operations on a collection of those item instances, such as providing an interator implementation. The abstract classes provide most of the functionality, but the concrete instances will provide data type-specific additions, such as declaring extra properties corresponding to fields within its respective database table. The two classes then work together to perform whatever operations is desired.
So, for example, if you call GetListAsXml(), it will loop through the items in the list, calling GetAsXml() on each, concatenate the results, and return it all inside an appropriate XML container. Similarly, calling AddItem() takes an unsaved new item object and calls its Save() method to commit it to the database. To sort the collection, you simply call Sort(), passing in a comparison function that compares two items (the abstract collection class itself provides a couple of defaults, while implementation classes can define additional ones that work with their unique collection item types).
Right now, all of this assumes that the entire collection is loaded, and that's handled by the constructor in CollectionAbs
implementation instances.
So, pausing here, is this design decent? Is there a pattern out there that might be better? I like that functionality for managing single items is encapsulated within the item classes, while functionality for manipulation of a collection of items is encapsulated within the collection class. And, I like that the CollectionAbs
class can provide so much functionality for its children because it requires minimal 'insider knowledge' about the items.
However, I'm not so sure about this design in those cases where the entire collection cannot be loaded at once, since that situation mandates a lot more, and lot closer, communication between the collection and item classes, as well as things like a lot of extra queries to load single records at a time. What's the best way to modify this design to handle partial collections? Is there a certain pattern I should be looking at?
I'm doing this in PHP 5.3, if it matters.
[Edit: adding example below; also clarified a misstated point above about comparison functions.]
Since someone asked for an example of how this is used in a comment:
These classes are going to form the basis of a large number of data collections of various types. An example is for tracking sets of status codes that are used by other parts of the system. Different parts of the system use slightly different status codes that are mapped to distinct database tables. So I set up something like this:
abstract class StatusCodeCollectionAbs extends CollectionAbs {
protected positionCompareFunction(item1, item2: StatusCodeAbs): integer {...};
protected descriptionCompareFunction(item1, item2: StatusCodeAbs): integer {...};
}
abstract class StatusCodeAbs extends CollectionItemAbs {
protected position: integer;
protected description: integer;
}
These two classes will serve as the basis for all status code collections. To add support for a particular collection, I just create concrete children:
class CustomerStatusCodeCollection extends StatusCodeCollectionAbs {
public function constructor() {
//load all items to list
}
//sort comparison closure unique to this status collection type
protected legacyCodeCompareFunction(item1, item2: CustomerStatusCode): integer {...};
}
class CustomerStatusCode extends StatusCodeAbs {
public function Load() {
//load this item from database
}
public function Save() {
//save this item to the database
}
//data unique to this status type
protected legacyCode: integer
}
A requirement of the system is that all of the collection data be managed through a unified set of CRUD methods provided in higher application layers. This hierarchy in the data layer enables that by providing a uniform interface to the collections, but still lets data specific to a collection type to be properly tracked. There's the additional status code layer to track the common data shared by the dozens of status types the system will track; other types of collections may have their own abstraction layers under CollectionAbs/CollectionItemAbs, or they may directly sub-class CollectionAbs and CollectionItemAbs, depending on need.