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

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

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


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

我想知道这种情况/模式是否存在名称,您需要在其中的许多函数中添加一个参数,这些参数只是“传递”未修改的值。

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

评论

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

这种范围太广,无法提供具体答案。在这个级别上,我称其为“编码”。

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

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

我也听说过它最常称为线程化,但也称为线程化,就像降低整个调用堆栈中的垂线一样。

#1 楼

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


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

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

评论


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

–RubenLaguna
16-10-31在16:49

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

–RubenLaguna
16-10-31在20:20

在20年的编程工作中,我从没听说过这个术语,也没有立即明白它的含义。我并不是在抱怨这个答案,只是在暗示这个词没有被广泛使用/知名。也许就是我。

–德里克·埃尔金斯离开东南
16年11月1日在4:04

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

–罗宾逊
16年11月1日在10:39

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

–罗安
16年11月1日在13:06

#2 楼

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

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



 int daysBetween(Day a, Day b)
 


为此,我创建了一个新函数:

 int daysSinceEpoch(Day day)
 


然后我的第一个功能变得简单:

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


没有任何反模式。 daysBetween方法的参数将传递给另一个方法,并且永远不会在该方法中引用,但是该方法仍需要它们来执行其需要执行的操作。

我建议的是查看每个功能并从几个问题入手:


此功能是否有明确且明确的目标,还是“做某事”的方法?通常,函数名称在这里有帮助,如果函数中没有名称描述的内容,那就是一个红色标记。
参数太多吗?有时,一种方法可以合法地需要大量输入,但是具有如此多的参数使其难以使用或理解。

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

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

评论


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

–RubenLaguna
16-10-31在15:28



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

– JimmyJames
16-10-31在15:31

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

–RubenLaguna
16-10-31在15:35

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

– JimmyJames
16-10-31在15:39



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

–user253751
16-10-31在21:33

#3 楼

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)之类的方法被明确指定为地图的参数。

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

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

评论


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

– JimmyJames
16年11月1日在15:27

@JimmyJames:您关于多态性的观点是一个很好的观点,我曾考虑过自己制作,但是为了避免答案变得更长而将其遗漏了。我试图(也许做得不好)要指出的是,虽然就数据流而言,foo.method(bar,baz)和method(foo,bar,baz)之间几乎没有区别,但还有其他原因(包括多态性) ,封装,位置等),以偏爱前者。

–伊尔马里·卡洛宁(Ilmari Karonen)
16年11月1日在16:14

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

–smci
16年11月1日在22:08

#4 楼

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


作为全局变量,当程序达到一定大小时作用域太大
作为纯本地参数,作用域可能太小,当它导致调用链中有很多重复的参数列表时
因此,要权衡取舍,通常可以使一个或多个类中的此类参数成为成员变量,这就是我所说的适当的类设计。


评论


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

–l0b0
16-10-31在18:11

#5 楼

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

我也同意@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函数! (您可能需要以这种方式进行单元测试,因为您的单元测试环境可能无法访问任何PhysicalRefrigerator。)

我确实了解到,并非所有依赖注入的使用都需要使用名称相似的管道参数在调用堆栈中的许多层次下进行了排列,因此我无法完全回答您所问的问题...但是,如果您要进一步阅读该主题,“依赖注入”绝对是与您相关的关键字。

评论


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

– Dewi Morgan
16年11月1日在5:50

@DewiMorgan:显然,您可以进一步重构以将冰箱广义化为一个成分源,或者甚至将“三明治”的概念泛化为template StackedElementConstruction make_sandwich(ElementSource&);这就是所谓的“通用编程”,并且功能相当强大,但是可以肯定的是,它比OP真正想要进入的方式更加神秘。随意就三明治程序的适当抽象级别提出一个新问题。 ;)

– Quuxplusone
16-11-1在7:49



毫无疑问,没有特权的用户不应访问make_sandwich()。

– dotancohen
16年11月1日在11:28

@Dewi-XKCD链接

–加文·洛克(Gavin Lock)
16-11-1在15:26



您的代码中最严重的错误是您将花生酱放在冰箱中。

–马尔沃里奥
16年2月2日,下午3:36

#6 楼

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

评论


如果level3()需要newParam,则可以肯定是耦合,但是某种程度上代码的不同部分必须彼此通信。如果该函数使用参数,则不必将其称为函数参数错误耦合。我认为该链的问题在于为level1()和level2()引入了额外的耦合,它们除了传递给newParam外没有用。好的答案,耦合+1。

–空
16-10-31在16:41



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

–Random832
16年11月1日在5:57

#7 楼

尽管此答案不能直接回答您的问题,但我觉得我很乐意让它通过而未提及如何改进(因为正如您所说,这可能是一种反模式)。我希望您和其他读者可以从有关如何避免“流失数据”的附加注释中获得价值(因为鲍勃·达格利什(Bob Dalgleish)为我们起了有益的命名)。

我同意建议做某事的答案更多的OO来避免这个问题。但是,另一种方法也可以帮助深深地减少参数传递,而不仅仅是跳到“仅传递您以前传递许多参数的类!”是为了重构,以便过程中的某些步骤发生在较高级别,而不是较低级别。例如,下面是一些之前的代码:



 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。最好的。

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

#8 楼

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

评论


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

–康拉德·鲁道夫(Konrad Rudolph)
16-10-31在23:14

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

– Quuxplusone
16年11月1日,在1:22

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

–埃里克·金(Eric King)
16-11-1在15:04



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

–康拉德·鲁道夫(Konrad Rudolph)
16年2月2日,14:53

该问题与LoD违规非常相似,因为通常建议的处理LoD违规的重构方法是引入流氓数据。恕我直言,这是减少耦合的良好起点,但还不够。

–Jørgen Fogh
16年11月4日,11:53

#9 楼

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

但是,如果人们正在执行函数式编程,OP所陈述的方法将非常有用。

#10 楼

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

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

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