将对象传递到更改对象的方法中,这是常见的(反)模式吗?


17

我正在阅读Martin Fowler的《重构》一书中的常见代码气味。在这种情况下,我想知道我在代码库中看到的一种模式,并且可以客观地将其视为一种反模式。

模式是一种将对象作为参数传递给一个或多个方法的模式,所有这些方法都会更改对象的状态,但是没有一个返回对象。因此,它依赖于C#/。NET(在这种情况下)的按引用传递特性。

var something = new Thing();
// ...
Foo(something);
int result = Bar(something, 42);
Baz(something);

我发现(尤其是方法名称不正确时),我需要研究此类方法以了解对象状态是否已更改。由于我需要跟踪调用堆栈的多个级别,因此这会使代码理解更加复杂。

我想建议改进此类代码,以返回具有新状态的另一个(克隆的)对象,或在调用站点更改该对象所需的任何操作。

var something1 =  new Thing();
// ...

// Let's return a new instance of Thing
var something2 = Foo(something1);

// Let's use out param to 'return' other info about the operation
int result;
var something3 = Bar(something2, out result);

// If necessary, let's capture and make explicit complex changes
var changes = Baz(something3)
something3.Apply(changes);

在我看来,第一个模式是根据假设选择的

  • 减少了工作量或减少了代码行
  • 它使我们既可以更改对象,又可以返回其他一些信息
  • 效率更高,因为我们的实例更少Thing

我举例说明了一种替代方法,但要提出它,则需要对原始解决方案提出争议。如果有什么论据可以证明原始解决方案是反模式?

我的替代解决方案有什么问题(如果有的话)呢?


1

1
@DaveHillier谢谢,我熟悉这个词,但是没有建立联系。
2013年

Answers:


9

是的,出于您描述的原因,原始解决方案是一种反模式:它使您难以推理正在发生的事情,该对象不负责其自身的状态/实现(破坏封装)。我还要补充一点,所有这些状态更改都是该方法的隐式契约,面对不断变化的需求,该方法非常脆弱。

就是说,您的解决方案有其自身的一些缺点,其中最明显的是克隆对象不是很好。对于大型物体可能会很慢。在代码的其他部分保留旧引用的地方,这可能会导致错误(您描述的代码库中可能就是这种情况)。使这些对象显式不可变至少可以解决其中的一些问题,但这是一个更为重大的改变。

除非对象很小并且有些瞬态(这使它们成为不变性的良好候选者),否则我倾向于将更多的状态转换移到对象本身中。这样一来,您可以隐藏这些转换的实现细节,并针对发生这些状态转换的人员/地点/地点设置更严格的要求。


1
例如,如果我有一个“文件”对象,则不会尝试将任何状态更改方法移到该对象中-这会违反SRP。即使当您拥有自己的类而不是诸如“ File”之类的库类时,这也仍然有效-将每个状态转换逻辑放入对象的类中实际上没有任何意义。
布朗

@Tetastyn我知道这是个老答案,但是我很难以具体的方式将您在上一段中的建议形象化。您能详细说明还是举例?
AaronLS

@AaronLS-代替Bar(something)(并修改的状态something),使Bar成员成为something的类型。something.Bar(42)更可能发生变异something,同时还允许您使用OO工具(私有状态,接口等)保护something状态
-Telastyn

14

方法名称不正确时

实际上,就是真正的代码味道。如果您有一个可变的对象,则它提供更改其状态的方法。如果您对嵌入在更多语句任务中的这种方法进行调用,则可以将该任务重构为自己的方法,这完全可以满足您所描述的情况。但是,如果您没有像Foo和这样的方法名称Bar,但是您可以清楚地知道它们更改了对象,则在这里不会出现问题。考虑到

void AddMessageToLog(Logger logger, string msg)
{
    //...
}

要么

void StripInvalidCharsFromName(Person p)
{
// ...
}

要么

void AddValueToRepo(Repository repo,int val)
{
// ...
}

要么

void TransferMoneyBetweenAccounts(Account source, Account destination, decimal amount)
{
// ...
}

或类似的东西-我在这里看不到有任何理由为这些方法返回克隆的对象,也没有理由调查它们的实现以了解它们将更改所传递对象的状态。

如果您不希望出现副作用,则使您的对象不可变,它将强制执行上述方法,以返回更改(克隆)的对象而不更改原始对象。


没错,重命名方法重构可以通过消除副作用来改善这种情况。但是,如果修改的方式使得简明的方法名称不可行,则可能会变得很难。
Michiel van Oosterhout

2
@michielvoo:如果简明的方法命名似乎不可能,则您的方法将错误的内容组合在一起,而不是为其所执行的任务构建功能抽象(无论有没有副作用,这都是正确的)。
布朗

4

是的,请参阅http://codebetter.com/matthewpodwysocki/2008/04/30/side-effecting-functions-are-code-smells/中的许多人指出意外副作用是不好的例子之一。

通常,基本原理是软件是分层构建的,并且每一层都应向下一层提供尽可能清晰的抽象。干净的抽象是您必须尽量少使用它的一种抽象。这就是所谓的模块化,适用于从单个功能到网络协议的所有内容。


我将描述OP描述为“预期的副作用”的特征。例如,您可以将委托传递给对列表中的每个元素进行操作的某种引擎。基本上就是ForEach<T>这样。
罗伯特·哈维

@RobertHarvey关于方法命名不正确的抱怨,以及关于必须阅读代码以找出副作用的抱怨,这使它们绝对不是预期的副作用。
btilly

我会给你的。但是必然的结果是,一个具有预期站点效果的适当命名的记录方法可能根本不是反模式。
罗伯特·哈维

@RobertHarvey我同意。关键是要知道重要的副作用非常重要,并且需要仔细记录(最好以方法的名义)。
btilly

我想说这是意料之外的和非显而易见的副作用的混合。感谢您的链接。
2013年

3

首先,这不依赖于“按引用传递特性”,而是依赖于对象是可变的引用类型。在非功能语言中,几乎总是这样。

其次,这是否是一个问题,取决于对象以及不同过程中的更改是否紧密地结合在一起-如果您无法对Foo进行更改而导致Bar崩溃,那就是问题。不一定是代码气味,但这是Foo或Bar或Something的问题(可能是Bar,因为它应该检查其输入,但是可能是Something被置于无效状态,应该防止)。

我不会说它上升到了反模式的水平,而是有一点需要注意。


2

我认为A.Do(Something)Modifying somethingsomething.Do()Modifying 之间几乎没有区别something。在任一情况下,它应该是从该被调用的方法的名称明确something将被修改。如果从方法名称中不清楚,无论something是参数this,还是环境的一部分,都不应对其进行修改。


1

我认为在某些情况下可以更改对象的状态。例如,我有一个用户列表,我想在将列表返回给客户端之前对列表应用不同的过滤器。

var users = Dependency.Resolve<IGetUsersQuery>().GetAll();

var excludeAdminUsersFilter = new ExcludeAdminUsersFilter();
var filterByAnotherCriteria = new AnotherCriteriaFilter();

excludeAdminUsersFilter.Apply(users);
filterByAnotherCriteria.Apply(users); 

是的,您可以通过将过滤移至另一种方法来使其美观,因此您将得到以下结果:

var users = Dependency.Resolve<IGetUsersQuery>().GetAll();
Filter(users);

在哪里Filter(users)执行上面的过滤器。

我不记得我以前在哪里遇到过这个问题,但是我认为它被称为过滤管道。


0

我不确定提议的(复制对象)新解决方案是否是一种模式。正如您所指出的,问题在于函数的命名错误。

假设我写了一个复杂的数学运算作为函数f()。我记录了f()是映射NXN到的函数N以及其背后的算法。如果该函数的名称不正确且未进行记录,并且没有随附的测试用例,则您必须了解该代码,在这种情况下,该代码将无用。

关于您的解决方案,一些观察:

  • 应用程序的设计是从不同的方面进行的:当一个对象仅用于保存值或跨组件边界传递时,明智的做法是从外部更改该对象的内部,而不是向其填充如何更改的详细信息。
  • 克隆对象导致腹胀内存需求,而且在许多情况下会导致不兼容的状态相当的对象的存在(X开始Y之后f(),但X实际上Y,)和可能的时间不一致。

您要解决的问题是有效的;但是,即使进行了大量的过度工程,该问题也得以解决,但并未解决。


2
如果您将观察与OP的问题相关联,这将是一个更好的答案。照原样,它不仅仅是评论,还是答案。
罗伯特·哈维

1
@RobertHarvey +1,很好的观察,我同意,将对其进行编辑。
CMR 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.