全球国家:为什么以及如何避免它们


全球条件。 这个短语在每个不幸面对这种现象的开发人员的心中引起恐惧和痛苦。 您是否已经遇到过意外的应用程序行为,无法理解其原因,例如一个不幸的骑士试图用许多头杀死Hydra? 您最终会经历无休止的反复试验,90%的时间想知道正在发生什么?

所有这些可能会成为全局变量的令人讨厌的结果:由于您尚未弄清原因,隐藏变量会在未知位置更改其状态。

您是否想在尝试更改应用程序时在黑暗中徘徊? 我当然不喜欢 幸运的是,我为您准备了蜡烛:

  1. 首先,我将描述我们最常称为的全球状态。 该术语并非总是准确使用,因此需要澄清。
  2. 接下来,我们将找出为什么全局变量对我们的代码库有害。
  3. 然后,我将说明如何调整全局变量的范围,以将其转换为局部变量。
  4. 最后,我将讨论封装以及为什么对全局变量的争夺只是大问题的一部分。

我希望本文能够解释您需要了解的有关全球状态的所有信息。 如果您认为我想念很多,并且为此而讨厌我并且不想再见到我,请在评论中写下。 对于我,我的读者以及突然出现在此页面上的每个人来说,这都是一件令人愉快的事情。

亲爱的读者,您准备好骑马并且认识您的敌人了吗? 去找到这些地球仪,让他们尝尝我们的剑剑!

什么情况?




让我们从基础开始,以便开发人员彼此了解。

状态是系统或实体的定义。 在现实生活中发现状态:

  • 关闭计算机后,其状态将关闭。
  • 当一杯茶很热时,她的状况很热。

在软件开发中,某些构造(例如变量)可能具有状态。 假设字符串“ hello”或数字11不被视为状态,它们是值。 将它们附加到变量并放入内存后,它们便变为状态。

<?php echo "hello"; // No state here! $lala = "hello"; // The variable $lala has the state 'hello'. 

可以区分两种状态:

可变状态:初始化后,它们可以在应用程序执行期间随时更改。

 <?php $lala = "hello"; // Initialisation of the variable. $lala = "hallo"; // The state of the variable $lala can be changed at runtime. 

不可变状态:执行期间无法更改。 您将第一个状态分配给变量,并且其值随后不会更改。 日常生活中的“常量”被称为不可变状态的示例:

 <?php define("GREETING", "hello"); // Constant definition. echo GREETING; GREETING = "hallo"; // This line will produce an error! 

现在,让我们听听Denis和您的开发人员Vasily之间的假设对话:

丹! 您已经在各处创建了全局变量! 不破坏一切就无法更改它们! 我会杀了你!
-Nifiga,Vasek! 我的全球财富真棒! 我将自己的灵魂投入其中,这些都是杰作! 我崇拜我的全球人!

开发人员通常将全局状态,全局变量或全局变量称为应称为全局可变状态的全局变量。 也就是说,可以在您可用的最大范围内修改状态:在整个应用程序中。

当变量没有将整个应用程序作为范围时,我们在谈论局部变量或区域设置。 它们存在于某些给定的可见性区域中,而不是整个应用程序的区域。

 <?php namespace App\Ecommerce; $global = "I'm a mutable global variable!"; // global variable class Shipment { public $warehouse; // local variable existing in the whole class public function __construct() { $info = "You're creating a shipment object!"; // local variable bound to the constructor scope echo $info; } } class Product { public function __construct() { global $global; $global = "I change the state now 'cause I can!"; echo "You're creating a product object!"; // no state here } } 

您可能会想:拥有可从任何地方访问并更改它们的变量有多方便! 我可以将状态从应用程序的一部分转移到另一部分! 无需通过函数传递它们并编写太多代码! 冰雹,全球可变状态!

如果您确实如此,我强烈建议您继续阅读。

全球各州比瘟疫和霍乱还糟吗?


最大的链接图


事实:如果只包含范围较小且已定义范围的语言环境,而不包含任何全局变量,则可以轻松创建准确的应用程序连接图。

怎么了

假设您有一个带有全局变量的大型应用程序。 每次需要更改某些内容时,都必须:

  • 回想一下,存在这些可变的全局状态。
  • 估计它们是否会影响您要更改的范围。

通常,您无需考虑其他作用域中的局部变量,但是无论您做什么,都始终需要在疲惫的大脑中留出全局可变状态的位置,因为它们会影响所有作用域。

而且,您的全局可变状态可以在应用程序中的任何地方更改。 通常必须怀疑它们的当前状态是什么。 这意味着您不得不搜索整个应用程序,试图计算可修改范围内的全局值。

这还不是全部。 如果您需要更改全局状态,那么您将无法想象它将影响什么范围。 这会导致另一个类,方法或函数的意外行为吗? 搜索成功。

简而言之,您将使用相同全局状态的所有类,方法和函数结合在一起。 不要忘记: 依赖关系大大增加了复杂性 。 它会吓到你吗? 应该的 较小的可见性特定区域非常有用:您无需牢记整个应用程序,仅记住与您一起工作的区域就足够了。

人们不会立即跟踪大量信息。 当我们尝试这样做时,我们很快就会耗尽认知能力的供应,这使我们难以专心,并且我们开始制造错误和愚蠢的事物。 这就是为什么在应用程序的全局范围内执行操作如此令人不愉快的原因。

全球名称冲突


使用第三方库有困难。 想象一下,您想使用该超酷库来为每个字符随机着色并具有闪烁效果。 每个开发人员的梦想! 如果该库还使用与您自己的名字相同的全局变量,那么您将享受名称冲突。 您的应用程序将崩溃,并且您可能会猜测很长一段时间的原因:

  • 首先,您需要确定您的库使用全局变量。
  • 其次,您需要计算执行期间使用了哪个变量-您的变量还是库? 这不是那么简单,名字是一样的!
  • 第三,由于您无法自行更改库,因此必须重命名全局可变变量。 如果在整个应用程序中使用,您将哭泣。

在每个阶段,您都会从愤怒和绝望中拔出头发。 很快,您将不再需要梳子。 这种情况不太可能引诱您。 也许有人会想起,如果未将JavaScript库Mootools,Underscore和jQuery放在较小的范围内,它们总是会发生冲突。 哦,还有著名的jQuery中的global $对象!

测试将成为一场噩梦


如果我还没有说服您,让我们从单元测试的角度来看一下情况:如何在存在全局变量的情况下编写测试? 由于测试可以更改全局变量,因此您不知道状态是什么。 您需要将测试彼此隔离,并且全局状态将它们绑定在一起。

您是否曾经使用过它,以便在隔离测试中正常工作,并且当您运行整个程序包时,它们会失败吗? 不行吗 我有。 每当我记住这一点,我都会受苦。

并发问题


如果需要并发,可变的全局状态可能会导致很多问题。 当您在多个执行线程中更改全局状态时,请在强大的竞争状态下直奔世界

如果您是PHP开发人员,那么除非您使用允许创建并行性的库,否则这不会打扰您。 但是,当您学习一种易于实现并行的新语言时,希望您能回忆起我的散文。

避免全局易变的状态




尽管全球易变国家可能会引起很多问题,但有时还是很难避免。

以REST API为例:端点接收带有参数的某种HTTP请求并发送响应。 在您的应用程序的许多级别,都可能需要这些发送到服务器的HTTP参数。 在接收HTTP请求时将这些参数设为全局,在发送响应之前对其进行修改是非常诱人的。 在每个请求的顶部添加并行性,灾难备案已准备就绪。

语言实现中也可以直接支持全局可变状态。 例如,在PHP中有superglobals

如果全局变量来自某个地方,那么如何处理它们? 由于您在过去20年中对开发工作一无所知,因此您应该如何重构Denis的应用程序,您的开发人员Danis尽可能创建了Globals?

功能参数


避免全局变量的最简单方法是使用函数参数传递变量。 举一个简单的例子:

 <?php namespace App; use Router\HttpRequest; use App\Product\ProductData; use App\Exceptions; class ProductController { public function createAction(HttpRequest $httpReq) { $productData = $httpReq->get("productData"); if (!$this->productModel->validateProduct($productData)) { return ValidationException(sprintf("The product %d is not valid", $productData["id"])); } $product = $this->productModel->createProduct($productData); } } class Product { public function createProduct(array $productData): Product { $productData["name"] = "SuperProduct".$productData["name"]; // This is not what you should do; I talk about it later in the article. try { $product = $this->productDao->find($productData["id"]); return product; } catch (NotFoundException $e) { $product = $this->productDao->save($productData); return $product; } } } class ProductDao { private $db; public function find(int $id): array { return $this->db->find(['product' => $id]); } public function save(array $productData): array { return $this->db->saveProduct($productData); } } 

如您所见,控制器的$productData通过HTTP请求经过不同的级别:

  1. 控制器收到一个HTTP请求。
  2. 参数传递给模型。
  3. 参数传递给DAO
  4. 参数保存在应用程序数据库中。

从HTTP请求中检索此参数数组时,可以将其设为全局。 似乎更简单:无需将数据传输到4个不同的函数。 但是,将参数作为参数传递给函数:

  • 很明显,这些函数将使用$productData
  • 显然,它将显示哪些函数使用哪些参数。 可以看出,对于ProductDao::find$productData ProductDao::find ,只需要$id ,而不是全部。

全局变量使代码难以理解并且彼此关联方法,这对于几乎完全没有优势的情况来说是很高的代价。

您已经听到了丹尼斯(Denis)的抗议:“并且,如果一个函数具有三个或更多参数? 如果您需要添加更多,函数的复杂性将会增加! 那么到处都需要的变量,对象和其他构造呢? 您会将它们传递给应用程序中的每个功能吗?”

亲爱的读者,问题是公平的。 作为一名优秀的开发人员 ,您应该使用自己的沟通技巧向Denis解释,这是什么:

“丹尼斯,如果您的函数有太多参数,那么函数本身可能是一个问题。 他们可能做的太多,对太多的事情负责。 您没有想到将它们划分为较小的功能吗?”

您像在雅典卫城的演说家一样,继续:

“如果您需要在可见性的许多领域中使用变量,那么这将是一个问题,很快我们将讨论它。 但是,如果您真的需要它们,那么通过函数参数传递它们又有什么问题呢? 是的,您将不得不在键盘上键入它们,但是我们是开发人员,编写代码是我们的工作。”

当您有更多参数时,它看起来似乎更复杂(也许是这样),但是我重复一遍,优点胜于缺点:最好代码尽可能清晰,而不使用隐藏的全局可变状态。

上下文对象


上下文对象是那些包含由某些上下文定义的数据的对象。 通常,此数据存储为密钥对构造,例如PHP中的关联数组。 这样的对象没有行为,只有数据,类似于值对象

上下文对象可以替换任何全局可变状态。 返回上一个代码示例。 我们可以使用封装该数据的对象来代替从请求中通过层次传递数据。

上下文将是查询本身:另一个查询-另一个上下文-另一个数据集。 然后,上下文对象将传递给需要此数据的任何方法。

您说:“这真棒,但这有什么用?”

  • 数据封装在一个对象中。 通常,您的任务是使数据不可变,也就是说,您不能更改状态-初始化后对象中数据的值。
  • 显然,上下文需要上下文对象的数据,因为它已传输到需要此数据的所有函数(或方法)。
  • 这就解决了并发性问题:如果每个请求都有自己的上下文对象,则可以在自己的执行线程中安全地写入或读取它们。

但是发展中的一切都有代价。 上下文对象可能有害:

  • 查看函数的参数,您将不知道上下文对象中的数据。
  • 您可以将任何东西放在上下文对象中。 注意不要放置太多内容,例如整个用户会话,甚至大部分应用程序数据。 然后会发生这种情况: $context->getSession()->getUser()->getProfil()->getUsername()违反得墨 the 耳定律 ,您的诅咒将是疯狂的复杂性。
  • 上下文对象越大,则找出其使用的数据和范围就越困难。

通常,我将尽可能避免使用上下文对象。 它们会引起很多疑问。 数据的不变性是一个很大的优点,但我们一定不能忘记这些缺点。 如果使用的是上下文对象,请确保它足够小,然后将其传递到仔细定义的小范围内。

如果在执行程序之前不知道将要传递给函数的状态数(例如,来自HTTP请求的参数),则上下文对象会很有用。 因此,其中一些使用它们,例如, Request记住Symfony中的Request对象。

依赖注入


全局可变状态的另一个很好的选择是在创建对象时将所需的数据直接嵌入到对象中。 这是依赖项注入的定义:一组将对象嵌入组件(类)的技术。

为什么要进行依赖注入?


目的是限制变量,对象或其他构造的使用,并将它们置于有限的范围内。 如果您具有嵌入的依赖项,因此只能在对象范围内运行,那么您将更容易发现它们在哪种上下文中使用以及为什么使用。 没有痛苦和折磨!

依赖注入将应用程序生命周期分为两个重要阶段:

  1. 创建应用程序对象并实现其依赖关系。
  2. 使用对象来实现您的目标。

这种方法使代码更清晰;您无需在随机位置实例化所有内容,甚至不必在任何地方使用全局对象。

许多框架通过配置文件和依赖注入容器(DIC)使用依赖注入,有时以相当复杂的方案使用。 但这并不需要使事情复杂化。 您可以简单地在一个级别上创建依赖关系,并在较低级别上实现它们。 例如,在围棋世界中,我不认识会使用DIC的人。 您只需使用代码(main.go)在主文件中创建依赖项,然后将它们转移到下一个级别。 您还可以实例化不同包中的所有内容,以清楚地指示“依赖注入阶段”应仅在此特定级别上执行。 在Go中,包的范围比在PHP中使事情变得容易,在PHP中DIC在我所知道的每个框架中都广泛使用,包括Symfony和Laravel。

通过构造器或设置器实现


有两种方法来注入依赖关系:通过构造函数或setter。 我建议,如果可能的话,请坚持第一种方法:

  • 如果您需要了解什么是类依赖关系,则要做的就是找到一个构造函数。 无需寻找散布在整个类中的方法。
  • 在安装过程中设置依赖性将使您对使用该对象的安全性充满信心。

让我们谈谈最后一点:这称为“强制不变”。 通过创建对象的实例并实现其依赖关系,您知道无论对象需要什么,它的配置都是正确的。 而且,如果您使用setter,您如何知道在使用对象时已经设置了依赖项? 您可以继续进行尝试,尝试找出是否调用了二传手,但是我敢肯定您不想这样做。

违反封装


毕竟,本地和全局州之间的唯一区别是它们的范围。 它们仅限于本地州,对于全球来说,整个应用程序都可用。 但是,如果使用局部状态,则可能会遇到特定于全局状态的问题。 怎么了

你说封装吗?


使用全局状态最终会破坏封装,就像您可以使用局部状态破坏封装一样。

让我们从头开始。 Wikipedia关于封装的定义告诉了我们什么? 限制直接访问对象某些组件的语言机制。 访问限制? 怎么了

好了,正如我们在上面看到的,在本地范围内进行推理比在全局范围内进行推理要容易得多。 顾名思义,全局可变状态随处可见,这是反对封装的! 没有您的访问限制。

范围扩大和状态泄漏




让我们想象一下一个状态,它的范围很小。 不幸的是,随着时间的流逝,应用程序不断增长,该局部状态作为参数传递给整个应用程序中的函数。 现在,您的语言环境已在许多范围中使用,并且在所有这些范围中,都可以直接访问该语言环境。 现在,您无需查看所有存在的可见性区域以及可以在何处更改的区域,就很难计算出区域的确切状态。 我们已经在全球可变国家中看到了所有这些。

举个例子: 贫血域模型可以扩大可变模型的范围。 实际上,贫血域模型将域对象的数据和行为分为两类:模型(仅具有数据的对象)和服务(仅具有行为的对象)。 这些模型通常会在所有服务中使用。 因此,某种模型可能会不断扩大范围。 您将不了解在哪种上下文中使用哪种模型,它们的状态将发生变化,所有相同的问题都将落在您身上。

我想传达一个重要的想法:如果您避免全局易变的状态,这并不意味着您可以放松,一只手握住鸡尾酒,另一只手按下按钮,享受生活和您的传奇密码。 -, , -, , .

, . , .

? - . , , -.

, - , , . , , — . , : , . . .


. Product , , :

 class Product { public function createProduct(array $productData): Product { $productData["name"] = "SuperProduct".$productData["name"]; // This is not what you should do; I talk about it later in the article. try { $product = $this->productDao->find($productData["id"]); return product; } catch (NotFoundException $e) { $product = $this->productDao->save($productData); return $product; } } } 

$productData . , , , .

, . , - ? , . .

, , . .

:

 class Product { public function createProduct(array $productData): Product { // Since $productData is passed to other variable, it has to be immutable. $name = "SuperProduct".$productData["name"]; try { $product = $this->productDao->find($productData["id"]); return product; } catch (NotFoundException $e) { $product = $this->productDao->save($name, $productData); return $product; } } } 

, , $productData . , . $productData , , HTTP-.

, : «, ».

?




. , .

?

  • , , .
  • , , (, ) .

. , .

, ShipmentDelay , , , . , -, ShipmentDelay , , , . ? , DRY .

, , . : , , . , , , . , , .

?


, (, ), , , . , , , , .

. :

  • .
  • , .
  • — : , , .
  • , .

. , — . , .

, , .

, , . , . , , . , !

Source: https://habr.com/ru/post/zh-CN454946/


All Articles