需要面向对象设计的建议:项目集合

时间:2011-11-09 19:21:24

标签: php oop design-patterns

我有一组这样的类:

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;
}

这个想法是CollectionItemAbs实现的具体实例表示一个项,其数据来自数据库表中的一行,CollectionAbs的相应具体实例提供对这些项的集合的操作实例,例如提供interator实现。抽象类提供了大部分功能,但具体实例将提供特定于数据类型的添加,例如声明与其各自数据库表中的字段对应的额外属性。然后这两个类一起工作以执行所需的任何操作。

因此,例如,如果调用GetListAsXml(),它将循环遍历列表中的项,在每个项上调用GetAsXml(),连接结果,并将其全部返回到适当的XML容器中。类似地,调用AddItem()接受未保存的新项对象并调用其Save()方法将其提交到数据库。要对集合进行排序,只需调用Sort(),传入比较两个项的比较函数(抽象集合类本身提供了几个默认值,而实现类可以定义使用其唯一集合项类型的其他类)。

现在,所有这些都假设整个集合已加载,并且由CollectionAbs实现实例中的构造函数处理。

所以暂停,这个设计是否合适?那里有一种可能更好的模式吗?我喜欢用于管理单个项目的功能封装在项目类中,而用于处理项目集合的功能封装在集合类中。并且,我喜欢CollectionAbs类可以为其子项提供如此多的功能,因为它需要最少的关于项目的“内部知识”。

然而,在整个集合无法一次加载的情况下,我不太确定这种设计,因为这种情况需要更多,更紧密,集合和项目类之间的通信,以及类似于一次加载单个记录的大量额外查询。修改此设计以处理部分集合的最佳方法是什么?我应该关注某种模式吗?

如果重要的话,我在PHP 5.3中这样做。

[编辑:在下面添加示例;还澄清了有关比较函数的错误观点。]

由于有人要求提供一个例子,说明如何在评论中使用它:

这些类将构成各种类型的大量数据集合的基础。一个示例是跟踪系统其他部分使用的状态代码集。系统的不同部分使用略微不同的状态代码,这些代码映射到不同的数据库表。所以我设置了这样的东西:

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;
}

这两个类将作为所有状态代码集合的基础。要添加对特定集合的支持,我只需创建具体的子集:

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
}

系统的要求是通过更高应用程序层中提供的统一CRUD方法集来管理所有集合数据。数据层中的此层次结构通过为集合提供统一接口来实现,但仍允许正确跟踪特定于集合类型的数据。还有额外的状态代码层来跟踪系统将跟踪的数十种状态类型共享的公共数据;其他类型的集合可能在CollectionAbs / CollectionItemAbs下有自己的抽象层,或者它们可以根据需要直接对CollectionAbs和CollectionItemAbs进行子类化。

2 个答案:

答案 0 :(得分:3)

一些设计要点:

  1. 您将如何从数据库加载集合?您已经提到过这一点,但它在设计中是基础。而且,我想说你不应该提供这个的默认实现。
    • 解决此问题的常用方法是传递表示数据库中唯一ID的整数数组。如果您希望ItemItems共享相同的界面,请不要使用类型检查强制执行此操作。 Item只需一个ID,而Items则需要一个数组。 PHP重载很糟糕,所以我不会强制array让你更轻松。
  2. 如何保存工作?它的外观,每个节点都必须与数据库紧密结合才能知道如何保存。这可能是好的也可能是坏的,但无论哪种方式,你都需要考虑它。我可能会建议将它们分离一些(将它们分开)。
  3. 我会使用接口和抽象。有关此代码的详细信息。
  4. 除了sort之外,我还会添加一种比较项目的默认方式。
    • 我在compareTo()课程中定义了Item
    • 当你在这里时,我也会定义一个equals()方法。
  5. XmlSerialize应该是一个单独的界面。我真诚地希望PHP本身具有这样的界面。它本身有很多用途。
  6. 实现Countable允许您使用PHP本机count()这是获取某些内容大小的PHP方法。轻松完成,也可以扔进去。
  7. 总的来说,你做得很好,但我有一些建议。

    下面的代码并不完美也不完整,因为它没有解决我上面提出的一些问题。但是,它确实提供了比您给出的功能更多的功能。另请注意,我已将事物重命名为ActiveNode而非CollectionItem等。这对我来说更有意义。

    <?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);
    
    }
    
    ?>
    

    备注:

    • 定义实现接口的抽象类以提供一些默认行为。在某些情况下,将接口与抽象分开对我有用。如果这是每一个开源的,那就会变得越来越重要。
    • 与返回的布尔值相比,抛出异常非常好。看起来您可能已经理解了这一点,但值得注意。请注意,我使用异常也表示资源不可用。当出现问题时,定义行为很有用。
    • 我对以IActiveNode作为参数的任何内容抛出InvalidArgumentExceptions。这允许您扩展IActiveNodes,扩展名只适用于某些类型。非常有用。
    • 我的解决方案正在接近非常类似Java的东西。我想说,根据您想要采用的程度,我建议使用不同的语言(尽管不一定是Java)。
    • 我还经常实现ArrayAccess,如果您希望您的列表能够在语法上像数组一样对待。
    老实说,我不介意再和你谈这件事。不要犹豫与我联系。

答案 1 :(得分:1)

tl; dr

public function AddItem(newItem: CollectionItemAbs);

而不是那样,如果您将集合基于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);
  }
}

SPL中有更多非常好的例子供您使用。