18

我有一个案例,“告诉,不要问”似乎与“单一责任”原则相冲突。我已经查看了有关该主题的其他讨论,但尚未能够为这种情况制定最合适的面向对象方法。

我有一个程序可以读取和操作来自各种来源的数据集合。我创建了一个类来保存和操作数据(“DataSet”类)。它包括对数据集执行各种操作的方法,例如比较两个数据集以生成包含差异的新数据集,以及将数据集写入文件。

我现在想对数据集执行一些分析并将结果输出到报告中。我第一次尝试对此进行编码询问数据集以从中提取信息,然后构建报告,但这似乎违背了“告诉,不要问”的原则。那么:我是否应该将分析方法放在 DataSet 类中并告诉数据集进行自我分析并生成报告?这是否违反了单一职责原则?如果我想在将来执行其他类型的分析怎么办 - DataSet 类可能会变得非常臃肿,其中包含许多与其核心目的无关的不同分析例程。

谁能在这里建议最好的方法?是否有解决此问题的特定设计模式?

4

4 回答 4

19

每当您设计软件时,您总是必须平衡不同的原则,因为其中许多是相互冲突的。例如,DRY(不要重复自己)原则经常与单一职责原则相冲突,特别是当两件事做相似但不完全相同的事情时。

很多时候,你必须决定哪个原则更重要,并强调那个原则而不是另一个原则(尽管你应该尽可能多地坚持)。很多时候,原则是一起工作的,有时它们会相互矛盾。

在这种情况下,Tell Don't Ask 与其他原则一起使用,例如Demeter 定律(尽管它的名称仍然是软件的原则,更好地描述为最少知识原则)。

LoD 告诉我们的是一个对象的方法应该只调用其他方法

  • 就其本身
  • 在作为参数传递给它的对象上
  • 使用参数传递的对象创建/实例化的任何对象
  • 对象直接组件对象
  • 或者一个全局变量

没有具体说,但我觉得选择调用方法的优先顺序也应该是这个顺序,全局变量是最后的手段。但是,这既不是这里也不是那里。

因此,如果我们将 Tell, Don't Ask 与 LoD 结合使用,那么将对象传递给另一个对象进行“询问”是完全可以的。意思是,您有一个 Analysis 对象,您“告诉”它做某事,将 DataSet 对象作为参数传递。这就是遵守 TDA。在 Analysis 对象的方法中,您仅通过访问“亲密朋友”数据来遵守 LoD。

这也符合 SRP,因为您的 DataSet 仍然只是一个 DataSet 而您的 Analysis 对象是一个 Analysis 对象。

这里的关键是这些原则通常是“相对论的”。这意味着,从获取数据并想要执行分析的父对象的角度来看,您是在“告诉”分析对象做某事。

TDA 的目的是您的父代码不应查询您的 DataSet 的状态,然后根据它做出决定。相反,它应该将对象传递给其他对象并让这些对象执行它们的职责,这可能包括查询这些对象的状态,但这没关系,因为这是在它们的职责范围内。

此处进一步参考:

http://pragprog.com/articles/tell-dont-ask

编辑:

如果您想要更权威的来源,没有人比 Martin Fowler 本人更好(阅读到最后,您会找到此评论)

http://martinfowler.com/bliki/TellDontAsk.html

但就个人而言,我不使用告诉-不要-问。我确实希望将数据和行为放在一起,这通常会导致类似的结果。我发现tell-dont-ask 令人不安的一件事是,我看到它鼓励人们成为GetterEradicators,寻求摆脱所有查询方法。但是有时对象通过提供信息来有效地协作。一个很好的例子是接受输入信息并对其进行转换以简化其客户端的对象,例如使用 EmbeddedDocument。我已经看到代码陷入了只告诉适当负责的查询方法在哪里可以简化问题的卷积1。对我来说,告诉-不要-询问是共同定位行为和数据的垫脚石,但我不认为这是值得强调的一点

于 2014-06-05T15:36:44.517 回答
1

我将创建一个 DataAnalyzer 对象,该对象的职责是根据对输入数据的一些分析生成报告。

interface DataAnalyzer {

    public function analyze($input);

    public function report();
}

现在我们可以进行我们需要的不同类型的分析

class AnalyzerOne implements DataAnalyzer {
    //one way of analyzing and reporting
}

class AnalyzerTwo implements DataAnalyzer {
   //other way of analyzing and reporting
}

我可以让我的 DataSet 对象使用一些输入填充分析器以进行分析,然后将报告委托给它。

class DataSet {

    private $data;

    //... other methods

    public function report(DataAnalyzer $analyzer) {
        //prepare input for the analyzer from the current state
        $analyzer->analyze($input);
        return $analyzer->report();
    }

}

最后客户看起来像这样

$dataSet = new DataSet();
//...

$firstReport = $dataSet->report(new AnalyzerOne());
$secondReport = $dataSet->report(new AnalyzerTwo());

所以每个对象负责单独的任务,dataSet 做自己的业务,analyzer 负责报告。但是,我们确实告诉 DataSet 使用分析器来生成报告。DataSet 然后告诉分析器使用什么样的输入并返回报告。

当然这不是唯一的方法,但总的来说,有了这么多信息,我认为这是正确的想法。

于 2014-06-05T15:12:25.690 回答
0

可能一个很简单的继承应用就可以解决你的问题。

因此,您创建了一个 Dataset 的子类来执行分析。将来如果需要,您可以创建另一个子类来执行该分析。

这里的主要好处是子类继承数据集,内部,因此它们来自同一家族并且可以访问数据数据集。

我可以给出一些代码示例。但让我们先看看这里有什么评论。

于 2014-06-10T12:39:41.777 回答
0

听起来也许 aViewModel是你想要的。您有一个Model(the DataSet),它负责维护数据的状态以及该数据所代表的内容。您有一个View(the Report),它负责向用户显示各种数据,但您想将其转换DataSet为适合查看的数据表示?

例如,您可以封装准备DataSet观看的责任DataSetViewModel。它可以在诸如GetDataInReportFormat().

我认为主要的变化是改变你的想法,将准备数据视为一项单独的责任。

于 2012-11-16T17:50:05.477 回答