传递参数的(反)模式是否有名称,将仅在调用链的多个级别使用?


208

我试图在某些旧代码中找到使用全局变量的替代方法。但是这个问题与技术选择无关,我主要关注的是术语

显而易见的解决方案是将参数传递给函数,而不是使用全局变量。在此传统代码库中,这意味着我必须更改长调用链中最终将使用该值的点与首先接收该参数的函数之间的所有函数。

higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)

newParam在我的示例中,where 以前是全局变量,但可能是以前的硬编码值。关键是,现在newParam的值是从获得的,higherlevel()并且必须一直“移动”到level3()

我在想,如果有一个姓名(或名称)的这种情况下,你需要一个参数添加到许多功能只是“传”未经修改的值/模式。

希望使用正确的术语将使我能够找到有关重新设计解决方案的更多资源,并向同事描述这种情况。


94
这是对使用全局变量的改进。它可以清楚地说明每个功能所依赖的状态(这是迈向纯功能的一步)。我听说过它称其为“通过”参数,但是我不知道该术语的普遍性。
gardenhead

8
这种范围太广,无法给出具体答案。在这个级别上,我称其为“编码”。
马查多

38
我认为“问题”只是简单而已。这基本上是依赖注入。我猜想,如果有一些嵌套更深的成员拥有某种机制,而不会膨胀函数的参数列表,那么可能会有机制通过该链自动注入依赖性。如果有的话,也许寻找具有不同复杂程度的依赖项注入策略可能会导致您要寻找的术语。
空值

7
尽管我很欣赏有关是否是一个好的模式/反模式/概念/解决方案的讨论,但我真正想知道的是是否有一个名称。
ecerulm

3
我也听说过它最通常地称为线程,但也称为管道,就像降低整个调用堆栈中的铅垂线一样
wchargin '16

Answers:


202

数据本身称为“流浪数据”。这是一种“代码气味”,表示一个代码段正在通过中介与另一段代码进行远程通信。

  • 提高代码的刚性,尤其是在调用链中。您在如何重构调用链中的任何方法上都受到更多限制。
  • 将有关数据/方法/体系结构的知识分发到最不在乎的地方。如果您需要声明刚刚传递的数据,并且声明需要重新导入,那么您已经污染了名称空间。

重构以除去全局变量很困难,流浪数据是这样做的一种方法,并且通常是最便宜的方法。它确实有其成本。


73
通过搜索“流派数据”,我可以在Safari订阅中找到“代码完成”书。该书中有一节称为“使用全局数据的原因”,原因之一是“使用全局数据可以消除流失数据”。:) 我觉得“流浪汉数据”将使我能够找到更多有关与全球人打交道的文献。谢谢!
ecerulm

9
@JimmyJames,这些功能当然可以做。只是不使用以前只是全局的特定新参数。
ecerulm

174
在20年的编程生涯中,我从没听说过这个术语,也没有立即明白它的含义。我并不是在抱怨这个答案,只是暗示这个词没有被广泛使用/知名。也许只是我。
德里克·埃尔金斯

6
某些全局数据很好。您可以将其称为“环境”,而不是将其称为“全局数据”-因为它就是它。例如,环境可能包括appdata的字符串路径(在Windows上),或者在我当前的项目中包括所有组件使用的整套GDI +画笔,钢笔,字体等。
罗宾逊

7
@罗宾逊不太。例如,您是否真的希望您的图像编写代码触摸%AppData%,还是希望它带有一个写在哪里的参数?那就是全局状态和争论之间的区别。“环境”也很容易成为注入的依赖性,仅对负责与环境交互的人员存在。GDI +笔刷等更为合理,但这实际上是在无法为您完成的环境中进行资源管理的情况-底层API和/或您的语言/库/运行时几乎不足。
a安

101

我不认为这本身就是反模式。我认为问题在于,当您实际上应该将每个功能都视为一个独立的黑匣子时,您正在将这些功能视为一个链(:递归方法是此建议的一个明显例外。)

例如,假设我需要计算两个日历日期之间的天数,然后创建一个函数:

int daysBetween(Day a, Day b)

为了做到这一点,我然后创建一个新函数:

int daysSinceEpoch(Day day)

然后,我的第一个功能就变成了:

int daysBetween(Day a, Day b)
{
    return daysSinceEpoch(b) - daysSinceEpoch(a);
}

对此没有反模式。daysBetween方法的参数将传递给另一个方法,并且在该方法中从未进行其他引用,但是该方法仍需要它们来执行其需要执行的操作。

我建议您看一下每个功能,并从几个问题开始:

  • 此功能是否有明确且明确的目标,或者是“执行某些操作”方法?通常情况下,函数的名称会有所帮助,如果其中包含名称未描述的内容,则为一个危险标记。
  • 参数太多吗?有时,一种方法可以合法地需要大量输入,但是具有如此多的参数使其难以使用或理解。

如果您看的是一堆杂乱无章的代码,而没有将其绑定到一个方法中,则应首先进行分解。这可能是乏味的。从最简单的事情开始,然后移出一个单独的方法,然后重复进行,直到您拥有连贯的内容。

如果参数太多,请考虑“ 方法到对象重构”


2
好吧,我并不是想和(anti-)引起争议。但是我仍然想知道是否有必须更新许多功能签名的“情况”的名称。我想更多的是“代码气味”而不是反模式。它告诉我,如果我必须更新6个函数签名以适应全局消除,则此旧版代码中有一些问题需要解决。但是我确实认为传递参数通常是可以的,并且我很感谢关于如何解决潜在问题的建议。
ecerulm

4
@ecerulm我不知道,但是我会说我的经验告诉我,将全局变量转换为参数绝对是开始消除它们的正确方法。这消除了共享状态,以便您可以进一步重构。我猜想这段代码还有更多问题,但是您的描述中没有足够的信息来知道它们是什么。
JimmyJames '16

2
我通常也遵循这种方法,并且在这种情况下也可能这样做。我只是想以此来改善我的词汇/术语,以便我可以对此进行更多研究,并在将来做更好,更集中的问题。
ecerulm

3
@ecerulm我认为没有这个名字。这就像许多疾病以及非疾病状况(如“口干”)的常见症状一样。如果您充实了代码结构的描述,则可能指向特定的内容。
JimmyJames

@ecerulm它告诉您有一些要修复的东西-现在,比起全局变量,要修复的东西要明显得多。
immibis

61

BobDalgleish已经注意到,这种(反)模式称为“ 流失数据 ”。

以我的经验,流浪汉数据过多的最常见原因是有一堆链接状态变量,这些状态变量实际上应该封装在对象或数据结构中。有时,甚至有必要嵌套一堆对象以正确组织数据。

举个简单的例子,考虑一个游戏,有一个可定制的玩家角色,具有相似特性playerNameplayerEyeColor等等。当然,玩家在游戏地图上也有实际位置,还有其他各种属性,例如当前和最大健康水平等等。

在此类游戏的第一次迭代中,将所有这些属性都设置为全局变量可能是一个完全合理的选择-毕竟只有一个玩家,并且游戏中的几乎所有东西都以某种方式涉及到了玩家。因此,您的全局状态可能包含如下变量:

playerName = "Bob"
playerEyeColor = GREEN
playerXPosition = -8
playerYPosition = 136
playerHealth = 100
playerMaxHealth = 100

但是到某个时候,您可能会发现需要更改此设计,可能是因为您想在游戏中添加多人游戏模式。第一次尝试,您可以尝试将所有这些变量都设置为局部变量,并将其传递给需要它们的函数。但是,您可能随后发现游戏中的某个特定动作可能涉及一个函数调用链,例如:

mainGameLoop()
 -> processInputEvent()
     -> doPlayerAction()
         -> movePlayer()
             -> checkCollision()
                 -> interactWithNPC()
                     -> interactWithShopkeeper()

...并且该interactWithShopkeeper()功能使店主可以按名称向播放器地址,因此您现在突然需要playerName通过所有这些功能作为流氓数据进行传递。而且,当然,如果店主认为蓝眼睛的玩家是幼稚的,并且会向他们收取更高的价格,那么您就需要playerEyeColor遍历整个功能链,依此类推。

适当的溶液中,在这种情况下,当然,以限定封装名称的选手对象,眼睛的颜色,位置,健康和游戏者角色的任何其他属性。这样,您只需要将单个对象传递给涉及播放器的所有功能。

同样,上面的几个功能自然可以成为该播放器对象的方法,这将自动使它们访问播放器的属性。从某种意义上讲,这只是语法上的糖,因为在对象上调用方法实际上会将对象实例作为隐藏参数传递给该方法,但是如果使用得当,它的确会使代码看起来更加清晰自然。

当然,典型的游戏将具有比玩家更多的“全局”状态。例如,您几乎肯定会拥有某种进行游戏的地图,以及在地图上移动的非玩家角色列表,以及放置在其上的物品,等等。您也可以将所有这些都作为流浪对象传递,但这又会使您的方法参数混乱。

相反,解决方案是让对象存储对与它们具有永久或临时关系的任何其他对象的引用。因此,例如,玩家对象(可能还包括任何NPC对象)可能应该存储对“游戏世界”对象的引用,该对象将对当前关卡/地图进行引用,从而player.moveTo(x, y)无需使用类似的方法明确给定地图作为参数。

类似地,如果我们的玩家角色有一只宠物狗跟随它,我们自然会将描述该狗的所有状态变量归为一个对象,并为玩家对象提供对该狗的引用(以便玩家可以(例如,用名字叫狗),反之亦然(以便狗知道玩家在哪里)。并且,当然,我们可能希望使播放器和dog对象都成为更通用的“ actor”对象的子类,以便我们可以重用相同的代码,例如在地图上移动。

附言 即使以游戏为例,也有其他类型的程序也会出现此类问题。但是,以我的经验来看,潜在的问题往往总是相同的:您有一堆单独的变量(无论是局部变量还是全局变量),实际上想将它们聚集在一起成为一个或多个互连的对象。侵入函数中的“陷阱数据”是由“全局”选项设置还是由数值模拟中的高速缓存的数据库查询或状态向量组成的,解决方案始终是识别数据所属的自然上下文并将其变成对象(或您选择的语言中最接近的等价词)。


1
该答案为可能存在的一类问题提供了一些解决方案。在某些情况下,可能会使用全局变量来表示不同的解决方案。我对使方法成为播放器类的一部分等同于将对象传递给方法的想法不满意。这忽略了以这种方式不容易复制的多态性。例如,如果我想创建对运动和属性类型具有不同规则的不同类型的播放器,则仅将这些对象传递给一个方法实现将需要大量条件逻辑。
JimmyJames

6
@JimmyJames:您关于多态性的观点是一个很好的观点,我曾考虑过自己做,但是为了避免答案变得更长而将其遗漏了。我试图(也许很差劲)提出的观点是,尽管就数据流而言,foo.method(bar, baz)和之间几乎没有区别method(foo, bar, baz),但还有其他原因(包括多态性,封装性,局部性等)更喜欢前者。
Ilmari Karonen '16

@IlmariKaronen:还有一个非常明显的好处,那就是它可以在将来对对象(例如playerAge)中的任何更改/添加/删除/重构进行功能原型验证。仅此一点是无价的。
smci

34

我不知道它的具体名称,但是我想值得一提的是,您描述的问题仅仅是针对此类参数的范围找到最佳折衷的问题:

  • 作为全局变量,当程序达到一定大小时作用域太大

  • 作为纯本地参数,范围可能太小,当它导致调用链中有很多重复的参数列表时

  • 因此,通常需要在一个或多个类中将此类参数作为成员变量进行权衡,这就是我所说的适当的类设计


10
+1用于适当的课程设计。听起来这是等待OO解决方案的经典问题。
l0b0

21

我相信您所描述的模式就是依赖注入。几位评论者认为这是一种模式,而不是一种反模式,我倾向于同意。

我也同意@JimmyJames的回答,他认为@JimmyJames的回答是将每个函数当作一个黑匣子,将其所有输入作为显式参数的黑匣子。也就是说,如果您要编写一个制作花生酱和果冻三明治的函数,则可以将其编写为

Sandwich make_sandwich() {
    PeanutButter pb = get_peanut_butter();
    Jelly j = get_jelly();
    return pb + j;
}
extern PhysicalRefrigerator g_refrigerator;
PeanutButter get_peanut_butter() {
    return g_refrigerator.get("peanut butter");
}
Jelly get_jelly() {
    return g_refrigerator.get("jelly");
}

但它会是更好的做法,适用依赖注入,它这样写,而不是:

Sandwich make_sandwich(Refrigerator& r) {
    PeanutButter pb = get_peanut_butter(r);
    Jelly j = get_jelly(r);
    return pb + j;
}
PeanutButter get_peanut_butter(Refrigerator& r) {
    return r.get("peanut butter");
}
Jelly get_jelly(Refrigerator& r) {
    return r.get("jelly");
}

现在,您有了一个可以在其功能签名中清楚记录其所有依赖性的功能,这对于可读性而言非常有用。毕竟,这是真实的,为了make_sandwich你需要访问Refrigerator; 因此,通过不将冰箱作为其输入的一部分,旧功能签名基本上是无关紧要的。

另外,如果您正确执行类层次结构,避免切片等操作,甚至可以make_sandwich通过传入MockRefrigerator!来对功能进行单元测试。(您可能需要以这种方式进行单元测试,因为您的单元测试环境可能无法访问任何PhysicalRefrigerators。)

我确实了解到,并非所有依赖注入的用法都需要在调用堆栈的多个层次上遍历一个类似名称的参数,因此我没有完全回答您所问的问题...但是,如果您正在寻找有关此主题的进一步阅读, “依赖注入”绝对是与您相关的关键字。


10
这显然是一种模式。绝对没有通过冰箱的要求。现在,传递通用的IngredientSource可能会起作用,但是如果您从面包箱中获取面包,从储藏室中获取金枪鱼,从冰箱中获取奶酪,则通过将对成分来源的依赖性注入到形成这些成分的无关操作中,该怎么办?将食材放入三明治中,就违反了关注点分离并发出了代码气味。
Dewi Morgan

8
@DewiMorgan:显然,您可以进一步进行重构,以将推广RefrigeratorIngredientSource,甚至推广到“三明治”到template<typename... Fillings> StackedElementConstruction<Fillings...> make_sandwich(ElementSource&);这就是所谓的“通用编程”,并且功能相当强大,但是可以肯定的是,它比OP真正想要进入的方式更加神秘。随意就三明治程序的适当抽象级别提出一个新问题。;)
Quuxplusone

11
在那里没有错,没有特权的用户应该不会访问make_sandwich()
dotancohen '16

2
@Dewi-XKCD 链接
Gavin Lock

19
代码中最严重的错误是您将花生酱放在冰箱中。
马尔沃里奥

15

这几乎是耦合的教科书定义,一个模块的依赖关系会深深地影响另一个模块,并且在更改时会产生连锁反应。其他注释和答案是正确的,因为这是对全局变量的一种改进,因为这种耦合现在对于程序员来说更加明确和容易,而不是颠覆性的。这并不意味着它不应该被修复。您应该能够重构以删除或减少耦合,尽管如果耦合已经存在了一段时间会很痛苦。


3
如果level3()需要的话newParam,那肯定是耦合的,但是某种程度上,代码的不同部分必须彼此通信。如果该函数使用了参数,则我不一定会称其为函数参数错误耦合。我认为该链的有问题的方面是额外引入用于耦合level1()level2()具有没有使用newParam除了通过它。好的答案,耦合+1。
空值

6
@null如果他们真的没有用,他们可以补足一个值,而不用从调用者那里接收。
Random832 '16

3

尽管此答案不能直接回答您的问题,但我感到不愿在不提及如何改进的情况下让它通过(因为正如您所说,这可能是一种反模式)。我希望您和其他读者可以从有关如何避免“流氓数据”的其他注释中获得价值(因为Bob Dalgleish为我们如此有用地命名了它)。

我同意建议采取更多面向对象的方法来避免此问题的答案。但是,另一种方法也可以帮助重构,以使过程的某些步骤发生在较高级别而不是较低级别,而不必跳到“ 仅传递您曾经传递许多参数的类!一。例如,这是一些之前的代码:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   FilterAndReportStuff(stuffs, desiredName);
}

public void FilterAndReportStuff(IEnumerable<Stuff> stuffs, string desiredName) {
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   ReportStuff(stuffs.Filter(filter));
}

public void ReportStuff(IEnumerable<Stuff> stuffs) {
   stuffs.Report();
}

请注意,必须执行的更多操作会使情况变得更糟ReportStuff。您可能必须传递要使用的Reporter实例。以及必须处理的各种依赖关系,都将其作用于嵌套函数。

我的建议是将所有这些都提升到更高的层次,在该层次上,对步骤的了解需要存在于单个方法中,而不是分散在一系列方法调用中。当然,在实际代码中会更加复杂,但这会给您一个想法:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   var filteredStuffs = stuffs.Filter(filter)
   filteredStuffs.Report();
}

注意这里的最大区别是您不必通过长链传递依赖关系。即使您不仅将平面展平到一个级别,而且将深度展平到几个级别,如果这些级别也实现了一些“展平”,以便将该过程视为该级别上的一系列步骤,您也会有所改进。

尽管这仍然是过程性的,并且还没有将任何东西变成对象,但这是朝着确定可以通过将某些东西变成一个类来实现什么样的封装迈出的良好一步。before方案中的深层方法调用隐藏了实际发生的细节,会使代码很难理解。虽然您可以过度执行此操作并最终使高层代码了解不应执行的操作,或者使方法执行的操作太多,从而违反了单一职责原则,但总的来说,我发现将内容弄平会有所帮助为了清楚起见,并朝着更好的代码进行增量更改。

请注意,在执行所有这些操作时,应考虑可测试性。链式方法调用实际上使单元测试更加困难,因为您在要测试的切片的程序集中没有好的入口点和出口点。请注意,通过这种扁平化,由于您的方法不再需要太多的依赖项,因此它们更易于测试,不需要太多的模拟!

我最近尝试将单元测试添加到一个类(我没有写过)中,该类具有类似17个依赖的东西,所有这些都必须被模拟!我还没有全部解决,但是我将类分为三类,每类处理与它有关的一个单独名词,将依赖项列表降到最差的一个,降到12。最好的。

可测试性将迫使您编写更好的代码。您应该编写单元测试,因为您会发现它使您以不同的方式考虑代码,并且可以从一开始就编写更好的代码,而不管编写单元测试之前可能遇到的错误有多少。


2

您实际上并没有违反Demeter定律,但是您的问题在某些方面与之类似。由于您的问题的重点是寻找资源,因此我建议您阅读有关Demeter法则的文章,并查看其中多少建议适用于您的情况。


1
细节上有些虚弱,这可能解释了反对意见。但从本质上讲,这个答案恰好在现场:OP应该仔细阅读Demeter法则-这是相关术语。
康拉德·鲁道夫

4
FWIW,我认为迪米meter律(又称“最低特权”)根本不相关。OP的情况是,如果它的函数没有流氓数据,则他的功能将无法执行其工作(因为调用堆栈中的下一个家伙需要它,因为下一个家伙需要它,依此类推)。最小特权/德米特耳法则仅在参数确实未被使用时才有意义,在这种情况下,解决方法很明显:删除未使用的参数!
Quuxplusone

2
这个问题的情况与Demeter定律完全无关...关于方法调用链的肤浅相似之处,但是在其他方面却非常不同。
埃里克·金

@Quuxplusone可能的,尽管在这种情况下,描述非常混乱,因为在这种情况下,链式调用实际上没有意义:它们应该嵌套
康拉德·鲁道夫

1
这个问题非常相似的毁灭之王侵犯,因为通常的重构建议应对毁灭之王违法行为是引入流浪汉数据。恕我直言,这是减少耦合的良好起点,但还不够。
约根·福

1

在某些情况下,最好(在效率,可维护性和易于实现方面)将某些变量作为全局变量,而不是总是传递所有变量的开销(例如您必须保留15个左右的变量)。因此,找到一种可以更好地支持范围界定的编程语言(作为C ++的私有静态变量)以减轻潜在的混乱(命名空间和事物被篡改)是有意义的。当然,这只是常识。

但是,如果有人正在执行函数式编程,那么OP指出的方法将非常有用


0

这里根本没有反模式,因为调用者不知道下面的所有这些级别,因此不在乎。

有人在调用higherLevel(params),并期望higherLevel可以完成其工作。HigherLevel对参数的作用与调用者无关。HigherLevel以可能的最佳方式处理问题,在这种情况下,将参数传递给level1(params)。绝对没关系。

您会看到一个呼叫链-但是没有呼叫链。顶部有一个功能,它会尽力而为。并且还有其他功能。每个功能都可以随时更换。

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.