【问题标题】:Tell, Don't Ask and Single Responsibility - doing new things with data in a class告诉,不要问和单一职责——在课堂上用数据做新的事情
【发布时间】:2012-05-22 20:34:43
【问题描述】:

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

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

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

任何人都可以在这里提出最佳方法吗?是否有解决此问题的特定设计模式?

【问题讨论】:

  • 嗨@Bob!您能否发布一些示例以更好地理解您所描述的场景?
  • 请举例说明您的分析需要什么样的输入。

标签: oop single-responsibility-principle tell-dont-ask


【解决方案1】:

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

通常您必须决定哪个原则更重要,并强调该原则而不是另一个原则(尽管您应该尝试尽可能多地遵守)。很多时候,原则是相互配合的,但有时它们是相互矛盾的。

在这种情况下,Tell Don't Ask 与其他原则一起使用,例如 Law of 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 的卷积。对我来说,告诉-不要-询问是共同定位行为和数据的垫脚石,但我不认为这是值得强调的一点

【讨论】:

  • 虽然您的Analysis 对象被告知 执行分析,但它需要询问 DataSet 获取一些数据。因此,如果它是“在对象责任的上下文中”,您建议违反 TDA。我做对了吗?
  • @Halst - 不,这不是“违反 TDA”。编写不向其他对象询问事物的代码是不可能的。 TDA 正是这样做的合适时机。查询一个对象然后根据其状态调用该对象的不同方法是不合适的,对象本身应该决定这一点。阅读我最后给你的链接。
  • @Halst - 您感到困惑的部分是 TDA 谈论使用对其进行操作的操作来封装数据。但是请记住,您传递给另一个对象的对象实际上也是数据。在许多情况下,它只是已经以不同形式存在的数据。因此,实际上,创建一个分析对象,并将数据对象传递给它会在遵循 TDA 的新分析对象中创建数据/函数的封装。同样,这完全是关于相对论和透视。
  • 所以最后,不是说你会违反TDA,而是在定义中添加一堆异常......
【解决方案2】:

我将创建一个 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 然后告诉分析器使用什么样的输入并返回报告。

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

【讨论】:

  • 但是AnalyzerOneAnalyzerTwo 很可能需要询问 DataSet 的一些数据来执行它们的操作。所以你的解决方案是违反告诉-不要-问。我理解对了吗?
  • 它不会向 DataSet 索要任何东西。 Analyzer 需要一些输入才能工作。 DataSet 告诉分析器处理一些输入。 DataSet 从自身内部(DataSet 类)提供此信息,因此不存在封装违规。询问信息类似于将整个 DataSet 对象传递给分析器,然后从分析器内部转到 dataSet.getThis、dataSet.getThat ...
  • 如果您有很多分析,那么每个分析都可能需要数据的某些部分......因此$input 可能再次成为整个数据,所以最后添加@ 没有任何优势987654329@函数能够获取全部数据并直接传递给analyse函数。
【解决方案3】:

听起来ViewModel 是您想要的。您有一个ModelDataSet),它负责维护您的数据状态以及该数据所代表的内容。您有一个ViewReport),它负责向用户显示各种数据,但您想将DataSet 转换为适合查看的数据表示?

您可以封装准备DataSet 以供查看的责任 - 例如DataSetViewModel。它可以具有诸如GetDataInReportFormat()之类的功能。

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

【讨论】:

  • 我认为问题不在于生成报告,而在于对DataSet 进行一些分析。如果我们假设这个分析需要几个实例变量来执行,那么有 2 个选项:如果您将分析作为DataSet 的一部分进行,那么对象将确认告诉-不-询问。如果分析将在对象外部进行,则不会确认它,而是会确认单一责任原则。
【解决方案4】:

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

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

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

我可以给出一些代码示例。不过我们先来看看这里的cmets是怎么说的吧。

【讨论】:

  • 继承通常不能很好地工作。当您从数据库中获取对象并假设您使用几乎所有数据时,您可能已经拥有一种类型的对象,那么数据库将具有返回十几个仅因实际类型而异的类似对象的功能并没有多大意义...
  • 可能是可能不是!这里有很多假设。如果您想开始新的对话,我建议您提出一个新问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多