理解面向对象编程原则中的对象/值传递

3

我对面向对象编程中的一些问题还不是很明白,我将使用虚构的 Stack Overflow 理解来看看是否能得到帮助。

所以,在这个页面上我们有一个问题。您可以对问题发表评论。还有答案。您可以对答案发表评论。

Question
 - comment
 - comment
 - comment

 Answer
  -comment

 Answer
  -comment
  -comment
  -comment

 Answer
  -comment
  -comment

所以,我想象中这种系统的高级理解(在PHP中,而不是.Net,因为我还不熟悉.Net)应该是这样的:

$question = new Question;
$question->load($this_question_id); // from the URL probably
echo $question->getTitle();

为了加载答案,我想应该是这样的(“ A”):
$answers = new Answers;
$answers->loadFromQuestion($question->getID()); // or $answers->loadFromQuestion($this_question_id);
while($answer = $answers->getAnswer())
{
    echo $answer->showFormatted();
}

或者,您可以选择执行“B”操作:
$answers->setQuestion($question); // inject the whole obj, so we have access to all the data and public methods in $question
$answers->loadFromQuestion(); // the ID would be found via $this->question->getID() instead of from the argument passed in
while($answer = $answers->getAnswer())
{
    echo $answer->showFormatted();
}

我想我的问题是,我不知道何时或是否应该传递整个对象,以及何时只需传递值。传递整个对象给我很大的灵活性,但它需要更多的内存,并且可能会受到更改的影响(例如属性或方法重命名)。如果“ A”风格更好,为什么不使用函数?面向对象编程在这里似乎是无意义的。
谢谢, 汉斯
5个回答

4
为什么要传递其中任何一个?考虑如下方法:
<?php
$question = new Question($id);
$comments = $question->getComments();
$answers = $question->getAnswers();

echo $question->getTitle();
echo $question->getText();

foreach ($comments as $comment)
    echo $comments->getText();

foreach ($answers as $answer)
{
    $answer_comments = $answer->getComments();
    echo $answer->getText();

    foreach ($answer_comments as $comment)
        echo $comment->getText();
}

getComments()getAnswers()使用$this->id来检索并返回注释或答案对象的数组。你可以在注释和答案对象中构建实用程序方法,以允许您按父ID加载。在这种情况下,仅采用ID作为参数即可。

$question = new Question($id);
$answers = Answer::forQuestion($question->id);

$comments = Comment::forQuestion($question->id);
$ans_comments = Comment::forAnswer($answer->id);  // or some way to distinguish what the parent object is.

编辑:可能子模型(在本例中为评论或回答)除了使用id进行数据库查询外,不需要来自父级的任何内容。传递整个父对象将是过度的。 (此外,PHP在处理循环引用对象时有很大问题,这可能已经在5.3系列中得到解决。)


1
+1. 在高级语言(如PHP)中,返回可迭代集合是常见的做法。 - Camilo Díaz Repka
这就是我原本的做法,但由于回答和评论在不同的数据库表中,因此我需要使用不同的模型来获取它们,并且必须将这些模型传递到对象中(否则最终会出现很难测试的嵌套依赖项,对吧)? - Hans
我也会这样做。你只需要问题ID作为答案,所以不需要传递整个Question对象。如果你觉得自己需要传递一个引用,那就传递一个引用,这样就不会创建一个新的副本。只是要注意在之后不要销毁对象。 - Fanis Hatzidakis
@Hans,Answer和Comment模型提供了“getFromParentId”类型功能的静态方法。$question->getComments()是一个很好的实用工具,可能只是调用了Comment::forQuestion($this->id)。当然,你应该测试这两种方法。 - jasonbar
@Dave Jarvis,不是这样的。然而,提供抽象化、封装、模块化和继承的对象都是面向对象的概念。每个问题、评论和答案模型都实现了这些概念。 - jasonbar
显示剩余3条评论

4

虽然我喜欢Jason的回答,但严格来说它并不是面向对象的。

$question = new Question($id);
$comments = $question->getComments();
$answers = $question->getAnswers();

echo $question->getTitle();
echo $question->getText();

foreach ($comments as $comment)
    echo $comments->getText();

问题如下:
  1. 没有信息隐藏,这是面向对象编程的基本原则。
  2. 如果答案的格式需要更改,则必须在与存储数据的对象不相关的地方进行更改。
  3. 解决方案不可扩展。(没有可继承的行为。)
您必须将行为(紧密耦合)与数据保持一致。否则,您就不是在编写面向对象的代码。
$question = new Question($id);
$questionView = new QuestionView( $question );

$questionView->displayComments();
$questionView->displayAnswers();

现在信息的显示是一个实现细节,并且可以重复使用。

请注意,这打开了以下可能性:

$question = new Question( $id );
$questionView = new QuestionView( $question );
$questionView->setPrinterFriendly();

$questionView->displayComments();
$questionView->displayAnswers();

这个想法是,现在你可以从代码库的一个单一位置更改问题的格式。你可以支持多种评论和答案格式,而不需要调用代码(a)知道;(b)需要进行重大更改。

如果你因为滥用访问器方法而在多个位置编写文本格式详细信息,那么任何未来维护者的生活都将是痛苦的。如果维护者是一个知道你住在哪里的精神病患者,你会有麻烦。

对象,数据和视图

以下是我所理解的问题:

Database -> Object -> Display Content

您希望对象的行为围绕着与对象本身内在逻辑相关的内容。换句话说,您不希望对象必须执行与其核心职责无关的任务。最常见的包括加载、保存和打印功能。您希望将这些功能与对象本身分开,因为如果您需要更改数据库或输出格式,您希望在系统中进行尽可能少的更改,并限制波及范围。
为了简化这个问题,让我们只看看如何加载评论;所有内容也适用于问题和答案。
评论类
评论类可能提供以下行为:
- 回复 - 删除 - 更新(需要权限) - 恢复(从删除中恢复) - 等等。
评论DB类
我们可以创建一个CommentDB对象,该对象知道如何操作数据库中的评论。CommentDB对象具有以下行为:
- 创建 - 加载 - 保存 - 更新 - 删除 - 恢复
请注意,这些行为很可能适用于所有对象,并且因此可以进行重构。这还将使您能够轻松更改数据库,因为连接信息将隔离到单个类(所有数据库对象的祖先)中。
示例用法:
  $commentDb = new CommentDB();
  $comment = $commentDb->create();

稍后:
  $comment->update( "new text" );

请注意,实现这一点有许多可能的方法,但您始终可以在不违反封装和信息隐藏的情况下这样做。

CommentView类

最后,CommentView类将与Comment类紧密耦合。它可以通过访问器获取Comment类的属性是可以预期的。该信息仍然被隐藏 来自系统的其余部分Comment及其CommentView紧密耦合。想法是将格式保留在一个单独的位置,而不是散布在需要随意使用数据的类中。

任何需要以稍微不同的格式显示评论的类都可以从CommentView继承。

另请参见:Allen Holub写道“您永远不应该使用get/set函数”,他正确吗?


2
将输出直接绑定到模型是一种可怕的做法。您应该更改数据在视图中的格式,而不是直接在模型中更改。 - jasonbar
谢谢Dave。我很喜欢这个方法,它很有帮助。关于获取评论和答案,它们理论上在不同的数据库表中,你会怎么做呢?像Jason那样使用静态方法,还是从模型类传入创建的对象? - Hans
  1. 你没有隐藏信息。现在它只是传递给视图了。你仍然需要获取问题的所有评论和答案。如何完成这个任务?
  2. 现在可以在视图中更改格式。很好。
  3. 这里也没有什么可继承的东西。
- jasonbar
@Dave 感谢您编辑的答案。我认为事情变得更加清晰了。我正在将一些剩余的疑问提炼成一个新问题,即将发布(因为它与此不直接相关)。 - Hans

0

除了Dave Jarvisjasonbar的回答之外,我通常使用DataMappers来在关系数据和对象之间进行转换,而不是使用ActiveRecord方法。因此,按照您的示例,我们将拥有以下类:

  • 问题
  • 答案
  • 评论

以及它们的数据映射器:

  • QuestionMapper
  • AnswerMapper
  • CommentMapper

每个映射器都会实现一个类似的接口:

  • save(object)//在数据库中创建或更新记录(或文本文件等)
  • delete(id)
  • get(id)

然后,我们会这样做:

$q = QuestionMapper::get( $questionid );

// here we could either (a) just return a list of Answers 
// previously eagerly-loaded by the
// QuestionMapper, or (b) lazy load the answers by 
// calling AnswerMapper::getByQuestionID( $this->id ) or similar.
$aAnswers = $q->getAnswers();
foreach($aAnswers as $oAnswer){
    echo $oAnswer->getText();

    $aComments = $oAnswer->getComments();
    foreach($aComments as $oComment){
        echo $oComment->getText();
    }
}

关于使用类似QuestionView->render($question)的东西,我更喜欢使用视图来显示使用域对象的getter获取的数据。如果将问题传递给HTMLView,则会将其呈现为HTML;如果将其传递给JSONView,则会获得JSON格式的内容。这意味着域对象需要有getter。
PS:我们还可以考虑QuestionMapper来加载与问题、答案和评论相关的所有内容。由于评论始终属于答案或问题,而答案始终属于问题,因此QuestionMapper加载所有内容可能是有意义的。当然,我们必须考虑不同的策略来延迟加载问题的一组答案和评论,以避免占用服务器资源。

0

两种风格都是可接受的。有时您只需要值,有时您将需要对象。在这个例子中,我个人会按照您的第一个示例进行操作,但像这样的琐碎程序很少出现在野外,所以也许您想要第二个选项。

我的经验法则是尽可能用最少的行数完成任务,同时清晰地展示给任何接手你工作的人。大多数情况下,对象创建与值传递的开销是您永远不必处理的现代架构中的事情。


我猜这是我的问题的一部分。我对创建几个函数并使用它们来清楚地表明我的操作感到非常满意,将事情限制在单个作业中并保持代码模块化。我在尝试将面向函数的思维方式适应面向对象编程的框架时遇到了太多麻烦。 - Hans

0

补充 @jasonbar 已经提到的内容:

我不知道什么时候或者是否应该传递整个对象,什么时候只需要传递一个值。

这取决于您所需的耦合内聚程度。

传递整个对象可以使我更具灵活性,但是会占用更多内存并且容易发生更改。

PHP在将对象用作函数参数时不会复制该对象。大多数其他语言也是如此(默认情况下,例如C#和Java,或者在显式请求时,例如C和C ++)


好的,所以我的重点不应该太在内存使用上,而是“我想让它如何行为”。我不确定答案,但至少现在我知道了正确的问题。谢谢。 - Hans

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接