3

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.

hakre
  • 193,403
  • 52
  • 435
  • 836
mr. w
  • 2,348
  • 1
  • 21
  • 26

2 Answers2

3

A few design points:

  1. How will you load a collection from the database? You have already mentioned this, but it is fundamental in the design. Moreover, I would say that you shouldn't provide a default implementation of this.
    • A common way to solve this problem is to pass an array of integers representing unique IDs in the database. Do not enforce this with typechecking if you want your Item and Items to share the same interface. An Item would take just one id, while Items would take an array. PHP is awful with overloading, so I wouldn't enforce array to make it easier on you.
  2. How will saving work? The way it looks, every node will have to be tightly coupled with the database to know how to save. That can be good and bad, but either way you need to think about it. I'd probably recommend decoupling that a bit more (separating them).
  3. I'd use interfaces AND abstractions. More detail on this below the code.
  4. In addition to having sort, I would add a default way to compare items.
    • I'd define compareTo() in your Item class.
    • I'd also define an equals() method while you are at it.
  5. XmlSerialize should be a separate interface. I've honestly wished PHP had such an interface natively. It has a lot of use on its own.
  6. Implementing Countable lets you use PHP native count(). This is the PHP way to get the size of something. Easily done, may as well throw it in.

Overall, you've done really well, but I have some suggestions.

The code below is not perfect nor complete, because it does not fix some of the questions I've posed above. However, it does offer some more functionality than what you've given. Also note that I've renamed things to be ActiveNode rather than a CollectionItem or the like. It made more sense to me.

<?php
/**
 * Resource exception would represent a problem with a resource such as a
 * database connection or a service like an API.  Not everything uses a database
 * these days.
 */
class ResourceException extends RuntimeException {}

/**
 * A database resource exception.
 */
class DatabaseException extends ResourceException {}

/**
 * Allows you to convert an item to and from XML.
 */
interface XmlSerializeable {
    /**
     * @return string A string in XML format representing the object.
     */
    public function xmlSerialize();

    /**
     * @param string $xml A string in XML format representing the object.
     * @throws InvalidArgumentException if the $xml is not well-formed.
     * @throws InvalidArgumentException if the $xml does not represent the correct object.
     */
    public function xmlUnserialize($xml);
}

/**
 * Allows you to sort an object.
 */
interface Sortable {
    /**
     * Sorts the collection with the function provided.  If none is provided, it
     * will simply use compareTo on each item.
     * @param function $fn The sorting function. 
     */
    function sort($fn=null);
}

/**
 * An active node.  An active node contains methods to save, load, delete,
 * convert to XML, etc.  It is 'active' because it is tied to the resource it
 * represents.
 */
interface IActiveNode extends XMLSerializeable {

    /**
     * Saves the item to the database.
     * @throws DatabaseException if an error occurs during the save.
     */
    public function save();

    /**
     * Loads the item from the database.
     * @throws DatabaseException if an error occurs during the load.
     */
    public function load();

    /**
     * Deletes the item from the database.
     * @throws DatabaseException if an error occurs during the deletion.
     */
    public function delete();

    /**
     * Compares an item to another.
     * @param IActiveNode $node The node to compare to.
     * @return int A negative number for less than, 0 for equality, and a positive number for greater than.
     * @throws InvalidArgumentException if the item provided cannot be compared to.
     */
    public function compareTo(IActiveNode $node);

    /**
     * Tests for equality against the provided item.
     * @param IActiveNode $node The node to compare to.
     * @return boolean if the nodes are equal.
     */
    public function equals(IActiveNode $node);
}

/**
 * A collection of active nodes.  Note that you should override behavior of many
 * off the methods this inherits to ensure that 
 */
interface IActiveNodes extends IActiveNode, Sortable, Countable, Iterator {
    /**
     * Adds a node to the collection.
     * @param IActiveNode $node The IActiveNode to add.
     * @return int The index the node was added into.
     * @throws InvalidArgumentException if the IActiveNode is the wrong type for this collection.
     */
    function addNode(IActiveNode $node);

    /**
     * Removes a node from the collection. Uses the equals method. Nodes will be
     * reordered after a remove.
     * @param IActiveNode $node The IActiveNode to remove.
     * @return IActiveNode The removed node.
     * @throws InvalidArgumentException if the IActiveNode is the wrong type for this collection.
     */
    function removeNode(IActiveNode $node);

    /**
     * Gets an item from the list.
     * @param IActiveNode $node The node to retrieve.
     * @return IActiveNode The IActive node that matches the one provided.
     */
    function getNode(IActiveNode $node);
    /**
     * Checks to see if a node exists in the collection. Uses the equals method.
     * @param IActiveNode $node The IActiveNode to check for containment.
     * @return boolean Returns true if the IActiveNode is in the collection.
     * @throws InvalidArgumentException if the IActiveNode is the wrong type for this collection.
     */
    function contains(IActiveNode $node);

    /**
     * Gets an item from the list.
     * @param int $index The index to retrieve.
     * @return IActiveNode The IActive node at the index provided.
     * @throws InvalidArgumentException if the index is not an integer.
     * @throws OutOfBoundsException if the index is out of bounds.
     */
    function getIndex($index);

    /**
     * Removes an item from the list by index.
     * @param int $index The index to remove.
     * @return IActiveNode The IActive node at the index provided.
     * @throws InvalidArgumentException if the index is not an integer.
     * @throws OutOfBoundsException if the index is out of bounds.
     */
    function removeIndex($index);

    /**
     * Filters the collection with a function.  It calls the filter function on
     * each item in the collection, and if the filter function returns true, then
     * it will add that to a new IActiveNodes collection, and return it.
     * @param function $fn A filter function.
     * @return IActiveNodes The filtered nodes.
     */
    function filter($fn);

}

?>

Notes:

  • Define abstract classes that implement the interfaces to provide some default behavior. Having the interface separate from the abstraction has been useful to me in some cases. If this is every open-sourced, it would be come increasingly important.
  • Throwing exceptions is really nice compared to returning booleans. It seems like you probably understand this, but it's worth noting. Notice that I've used exceptions to also denote resource unavailability. It's useful to have a defined behavior when things go wrong.
  • I throw InvalidArgumentExceptions on anything that takes an IActiveNode as a parameter. This allows you to extend IActiveNodes and the extension will only work with certain types. Very useful.
  • My solution is approaching something very Java like. I would say that depending on how far you want to take this, I would recommend using a different language (though not necessarily Java).
  • I often implement ArrayAccess as well if you want your list to be able to be syntactically treated like an array.

Honestly, I wouldn't mind talking to you about this more. Don't hesitate to contact me.

Levi Morrison
  • 19,116
  • 7
  • 65
  • 85
  • Breaking out XML serializer is a great idea: added. I already had `Countable` in there, and have considered ArrayAccess. I also already had comparison/sorting/filtering functionality very similar to what you suggested. My biggest concern is persistence. Everything I have works great if the entire collection is always loaded, but that leads to inefficiencies with collection-level operations that manipulate a small number of items. I think I'll just move all persistence bits into the concrete collection classes, and use the item classes strictly to store item properties. Seem like a good idea? – mr. w Nov 10 '11 at 20:31
  • @mr.w I'm not sure I followed you. What inefficiencies are you worried about? Also, how do you magically have everything in the collection already? That seems very odd and inefficient to me. – Levi Morrison Nov 10 '11 at 20:44
  • Items are loaded in the implementations of `CollectionItemAbs` by the constructor. Basically, it grabs a list of the IDs in the collection from the database, then instantiates an item for each and tells it to load by ID. This works great for collection-wide operations, but sucks for item-specific operations. For example, to retrieve a list of items that match certain criteria, the entire collection must be loaded and filtered, versus just loading only those items that match in the first place. Thus, I think it'll be good to move persistence out of the item - just not sure of the best way yet. – mr. w Nov 10 '11 at 21:33
  • @mr.w How much filtering is going on here? You may as well invest in a database abstraction layer or something of the likes if it is complex filtering. One that I really like but is still young and active (IE, bad for business applications) is Hydrogen: http://webdevrefinery.com/forums/topic/1440-hydrogen-overview . Hydrogen has a lot of other things it does too, so you may want to look at it anyway. Or, if you need a more mature library, see http://stackoverflow.com/questions/108699/good-php-orm-library – Levi Morrison Nov 10 '11 at 21:53
1

tl;dr but

public function AddItem(newItem: CollectionItemAbs);

instead of that, you have a lot of functionality already defined if you instead base collections off of ArrayObject:

class CollectionAbs extends ArrayObject {
  public function offsetSet($index, $value) {
    if(!$value instanceof CollectionItemAbs) {
        throw new InvalidArgumentException(__CLASS__." only contains instances of CollectionItemAbs");
    }
    return parent::offsetSet($index, $value);
  }
}

There are more, pretty nice examples in the SPL for you to use.

chelmertz
  • 20,399
  • 5
  • 40
  • 46
  • I agree with extending the same interfaces as `ArrayObject`, but extending it? I often don't want other people treating my code as `ArrayObject`s, but that's a personal preference. It does cut down a lot of potential code. – Levi Morrison Nov 09 '11 at 22:34
  • What is the downside of treating object collections as arrayobjects, in your opinion? Personally i dont think they are used by themself, ever, since they do the less things than the standard array. – chelmertz Nov 10 '11 at 06:38
  • Honestly, `ArrayObjects` are really confused about what they are. Sometimes you can use object notation on them, sometimes you can't. They can be finicky, sometimes. Also, `ArrayObject` is not commonly seen in PHP. Hiding away things in an unfamiliar abstraction can be harmful. Composition and implementation are far more clear than inheritance in this instance. – Levi Morrison Nov 10 '11 at 18:32
  • I disagree, I guess. Haven't had any problems with using this approach for collections and probably saved *a lot* of time while doing it too. Regarding the inheritance, I don't really get anything I mind having but I do gain pretty much (as seen in the code sample). – chelmertz Nov 11 '11 at 11:35