【问题标题】:Need advice for object-oriented design: a collection of items需要面向对象设计的建议:项目集合
【发布时间】:2011-11-09 19:21:24
【问题描述】:

我有一组类似这样的类:

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,视需要而定。

【问题讨论】:

  • 如果我有一个用例,我可以告诉你更多关于你的设计的信息。它似乎过于复杂,无法看到它的预期用途。
  • 我添加了一个具体的例子。
  • 在我下面的帖子中添加了一堆建议。

标签: php oop design-patterns


【解决方案1】:

几个设计要点:

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

总的来说,你做得很好,但我有一些建议。

下面的代码并不完美也不完整,因为它没有解决我上面提出的一些问题。但是,它确实提供了比您提供的功能更多的功能。另请注意,我已将事物重命名为 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
  • 如果您希望您的列表在语法上能够像数组一样处理,我也经常实现 ArrayAccess

老实说,我不介意和你多谈这个。不要犹豫与我联系。

【讨论】:

  • 分离出 XML 序列化器是个好主意:添加。我已经在那里有了Countable,并考虑了ArrayAccess。我也已经有了与您建议的非常相似的比较/排序/过滤功能。我最大的担忧是坚持。如果始终加载整个集合,我所拥有的一切都很好,但这会导致操作少量项目的集合级操作效率低下。我想我只是将所有持久性位移动到具体的集合类中,并严格使用项目类来存储项目属性。看起来是个好主意?
  • @mr.w 我不确定我是否关注了你。您担心什么效率低下?另外,你是如何神奇地拥有收藏中的所有东西的?这对我来说似乎很奇怪且效率低下。
  • 项目由构造函数加载到CollectionItemAbs 的实现中。基本上,它从数据库中获取集合中的 ID 列表,然后为每个实例实例化一个项目并告诉它按 ID 加载。这适用于集合范围的操作,但不适用于特定于项目的操作。例如,要检索符合特定条件的项目列表,必须加载和过滤整个集合,而不是只加载最初匹配的项目。因此,我认为将持久性从项目中移出会很好——只是还不确定最好的方法。
  • @mr.w 这里进行了多少过滤?如果它是复杂的过滤,你也可以投资一个数据库抽象层或类似的东西。我真正喜欢但仍然年轻且活跃的一个(IE,对商业应用程序不利)是 Hydrogen:webdevrefinery.com/forums/topic/1440-hydrogen-overview。氢还有很多其他的功能,所以你可能还是想看看它。或者,如果您需要更成熟的库,请参阅stackoverflow.com/questions/108699/good-php-orm-library
【解决方案2】:

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 中有更多非常好的示例供您使用。

【讨论】:

  • 我同意扩展与ArrayObject 相同的接口,但是扩展呢?我经常不希望其他人将我的代码视为ArrayObjects,但这是个人喜好。它确实减少了很多潜在的代码。
  • 在您看来,将对象集合视为数组对象有什么缺点?我个人认为它们永远不会被自己使用,因为它们做的事情比标准数组少。
  • 老实说,ArrayObjects 对它们是什么感到非常困惑。有时您可以对它们使用对象表示法,有时则不能。有时,它们可能很挑剔。此外,ArrayObject 在 PHP 中并不常见。在不熟悉的抽象中隐藏事物可能是有害的。在这种情况下,组合和实现比继承要清晰得多。
  • 我不同意,我猜。使用这种方法进行收藏没有任何问题,并且可能在这样做的同时也节省了很多时间。关于继承,我并没有真正得到任何我介意的东西,但我确实获得了很多(如代码示例所示)。
猜你喜欢
  • 1970-01-01
  • 2011-10-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-13
  • 1970-01-01
  • 1970-01-01
  • 2013-09-11
相关资源
最近更新 更多