隐藏依赖
by Shalvah
由Shalvah
如何消除(或处理)隐藏的依赖 (How to eliminate (or deal with) hidden dependencies)
In software, a dependency occurs when one module in an application, A, depends on another module or environment, B. A hidden dependency happens when A depends on B in a way that’s not obvious.
在软件中,当应用程序中的一个模块A依赖于另一个模块或环境B时,就会发生依赖关系。 当A以不明显的方式依赖B时 ,就会发生隐藏的依赖关系。
To discover a hidden dependency, you usually have to dig into the source code of the module. A module could refer to anything from an entire service to a class or function to just a few lines of code.
要发现隐藏的依赖关系,通常必须深入研究模块的源代码。 模块可以引用从整个服务到类或函数的所有内容,也可以仅指代几行代码。
Here’s a small example of a dependency, comparing two ways it could be expressed:
这是一个依赖性的小例子,比较了它的两种表达方式:
let customer = Customer.find({id: 1});
// explicit — the customer has to be passed to the cartfunction Cart(customer) { this.customer = customer;}let cart = new Cart(customer);
// hidden — the cart still needs a customer,// but it doesn’t say so outrightfunction Cart() { this.customer = customer; // a global variable `customer`}let cart = new Cart();
Note the subtle difference? Both implementations of the Cart constructor depend on a customer object. But the first requires that you pass in this object, while the second expects that there’s already a customer object available in the environment.
注意细微的差别? Cart构造函数的两种实现均取决于客户对象。 但是第一个要求您传入此对象,而第二个要求环境中已经有一个可用的客户对象。
A dev seeing let cart = new Cart()
would have no way to tell that the cart object depends on a global customer variable, except if they took a look at the Cart constructor.
看到let cart = new Cart()
人员没有办法说出cart对象依赖于全局客户变量,除非他们查看了Cart构造函数。
野外隐藏的依赖 (Hidden dependencies in the wild)
I’ll share a few examples of hidden dependencies I’ve come across in real-world codebases.
我将分享一些我在现实世界代码库中遇到的隐藏依赖性的示例。
PHP
include
filesPHP
include
文件
Let’s take a typical PHP backend app. In our `index.php`, the entry point of our app, we could have something like this:
让我们来看一个典型PHP后端应用程序。 在我们应用的入口“ index.php”中,我们可能会有类似以下内容:
include 'config.php';include 'loader.php';$app = new Application($config);
The code looks suspicious, doesn’t it? Where did the $config
variable come from? Let’s see.
该代码看起来可疑,不是吗? $config
变量来自哪里? 让我们来看看。
The include
directive is similar to HTML <scri
pt> tags. It tells the interpreter to grab the contents of the specified file, execute it and, if it has a return statement, pass the return value to the caller. It’s a way of splitting code across multiple files. Like a &l
t;script>
tag, include can also put variables into the global scope.
include
指令类似于HTML <scri
pt>标签。 它告诉解释器获取指定文件的内容,执行该文件,如果它具有return语句,则将返回值传递给调用方。 这是在多个文件之间拆分代码的一种方式。 ike a &l
script pt>
标记一样,include也可以将变量放入全局范围。
Let’s look at the files we’re including. The config.php
file contains typical configuration settings for a backend app:
让我们看看其中包含的文件。 config.php
文件包含后端应用程序的典型配置设置:
$config = [ 'database' => [ 'host' => '127.0.0.1', 'port' => '3306', 'name' => 'home', ], 'redis' => [ 'host' => '127.0.0.1', ]];
The loader.php
is basically a homemade class loader. Here’s a simplified version of its contents:
loader.php
基本上是一个自制的类加载器。 这是其内容的简化版本:
$loader = new Loader(__DIR__);$loader->configure($config);
See the problem? The code in loader.php
(and the rest of the code in index.php
) depends on some variable named $config
, but it’s not obvious where $config
is defined until you open up config.php
. This coding pattern is actually not uncommon.
看到问题了吗? loader.php
的代码(以及index.php
的其余代码)取决于某个名为$config
变量,但是在打开config.php
之前,在$config
的定义位置并不明显。 实际上,这种编码模式并不罕见。
Including JavaScript files via
<scri
pt>; tags通过
<scri
pt>包含JavaScript文件 ; 标签
This is probably a more common example. Compare the follwing two code snippets (assume cart-fx
and cart-utils
are some random JS libraries):
这可能是一个更常见的例子。 比较下面两个代码片段(假设cart-fx
和cart-utils
是一些随机的JS库):
Exhibit A:
展览A:
<script src="//some-cdn/cart-fx.js"></script><script src="//some-cdn/cart-utils.js"></script>
/* lots and lots of code */
<script>var cart = new Cart(CartManager.default, new Customer());</script>
Exhibit B:
图表B:
import Cart from ‘cart-fx’;import CartManager from ‘cart-utils’;
/* lots and lots of code */
const cart = new Cart(CartManager.default, new Customer());
In the second, it’s obvious that the Cart
and CartManager
variables were brought in (imported) from the cart-fx
and cart-utils
modules respectively. In the first, we’re left to guess as to which module owns the Cart
, and which owns the CartManager
. And don’t forget the Customer
too! (Remember, our own code is also a module.)
在第二个CartManager
,很明显,分别从cart-fx
和cart-utils
模块引入(导入)了Cart
和CartManager
变量。 首先,我们要猜测哪个模块拥有Cart
,哪个拥有CartManager
。 并且也不要忘记Customer
! (请记住,我们自己的代码也是一个模块。)
Reading from the environment
从环境中阅读
I’m the culprit in this one. I built a PHP package some time ago to interact with an API. The package allowed you to pass your API keys to the constructor. Unfortunately, it also allowed you to instead specify your keys as environment variables, and the package would automatically make use of them. Take a peek, and don’t laugh at me.
我是这个罪魁祸首。 我前段时间构建了一个PHP包来与API交互。 该软件包允许您将API密钥传递给构造函数。 不幸的是,它还允许您将键指定为环境变量,并且程序包将自动使用它们。 偷看,别嘲笑我 。
When I did that, I believed I was making the developer’s life easier. In reality, though, I was making assumptions about the environment the code was running in, and tying the package’s functionality to certain conditions being met in the environment.
当我这样做时,我相信我正在使开发人员的生活更轻松。 但是,实际上,我是在假设代码在其中运行的环境,然后将程序包的功能与环境中满足的某些条件联系在一起。
那么,为什么隐藏的依赖项很危险? (So, why are hidden dependencies dangerous?)
I can think of two main reasons:
我可以想到两个主要原因:
It’s easy to unintentionally remove the module depended upon without removing the dependency. For instance, take the case of my package above. Imagine a developer setting up an app which uses the package in a new environment. While deciding what environment variables to carry over from the old environment, the dev might neglect to add the ones needed by the package, because they can’t find them used anywhere in the codebase.
在不删除依赖项的情况下,很容易无意间删除依赖的模块 。 例如,以我上面的包裹为例。 想象一个开发人员设置一个在新环境中使用该软件包的应用程序。 在确定要从旧环境中继承的环境变量时,开发人员可能会忽略添加软件包所需的变量, 因为他们找不到代码库中任何地方使用的变量。
A small change to the dependent code could break the entire application, or make it buggy. Take the case of our
index.php
file above — swapping the first two lines might seem like a harmless change, but it would break the app, because line 2 depends on a variable set in line 1. An even more serious case of this would be something like this:对相关代码进行小的更改可能会破坏整个应用程序,或使其变得有问题。 以上面我们的
index.php
文件为例-交换前两行似乎是无害的更改,但这会破坏应用程序,因为第2行取决于第1行中设置的变量。更严重的情况是是这样的:
$config = […];include 'bootstrap.php';$app = new Application($config);
Suppose our bootstrap.php
file makes some important changes to $config
. If, for some reason, the second line is moved to the bottom, the app would run without throwing any errors, but the crucial configuration changes that bootstrap.php
makes would be unseen by the app.
假设我们的bootstrap.php
文件对$config
进行了一些重要更改。 如果由于某种原因第二行移至底部,则该应用程序将运行而不会引发任何错误,但是bootstrap.php
进行的关键配置更改将不会被该应用程序看到。
摆脱隐藏的依赖关系 (Getting Rid of Hidden Dependencies)
Like much of software engineering, there are no hard rules for dealing with hidden dependencies, but I’ve found a few basic principles that work for me:
像许多软件工程一样,没有用于处理隐藏依赖性的硬性规则,但是我发现了一些对我有用的基本原则:
Write code that’s truly modular, not just split across multiple files. An ideal module aims to be self-contained and have minimal dependence on shared global state. A module should also explicitly state its dependencies.
编写真正模块化的代码 ,而不仅仅是将它们拆分成多个文件。 理想的模块旨在自成一体,并且对共享的全局状态的依赖性最小。 模块还应明确声明其依赖性。
Reduce the number of assumptions a module needs to make about its environment or other modules.
减少模块需要对其环境或其他模块进行假设的数量 。
Expose a clear interface. Ideally, beyond things like function/class signatures, a user of your module should not have to look at the source code to figure out what the module’s dependencies are.
公开一个清晰的界面。 理想情况下,除了函数/类签名之类的东西之外,模块的用户也不必查看源代码即可确定模块的依赖关系。
Avoid littering the environment. Resist the temptation to add variables to the parent scope. As often as possible, prefer explicitly returning or exporting variables to the caller.
避免乱扔垃圾。 抵制将变量添加到父范围的诱惑。 尽可能经常地,最好将变量显式地返回或导出到调用方。
I’ll demonstrate how we could refactor the first example above using these principles. The first thing to do is make the config file return the configuration array, so the caller can explicitly save that in a variable:
我将演示如何使用这些原理来重构上面的第一个示例。 首先要做的是使配置文件返回配置数组,以便调用者可以将其显式保存到变量中:
// config.phpreturn [ 'database' => [ 'host' => '127.0.0.1', 'port' => '3306', 'name' => 'home', ], 'redis' => [ 'host' => '127.0.0.1', ]];
The next thing we can do is change the loader file to return a function. This function will take a config parameter. In this way, we’re making it clear that the process of loading files depends on some preset configuration.
我们可以做的下一件事是更改加载程序文件以返回函数。 此函数将使用config参数。 通过这种方式,我们很清楚地表明,加载文件的过程取决于某些预设配置。
// loader.php
return function (array $config){ $loader = new Loader(__DIR__); $loader->configure($config);}
Putting these together, we end up with our index.php
file looking like this:
将它们放在一起,最终得到如下所示的index.php
文件:
$config = include 'config.php';(include 'loader.php')($config);
$app = new Application($config);
We can even go a bit further by saving the returned function in a variable before calling it:
我们甚至可以通过在调用之前将返回的函数保存在变量中来进一步处理:
$config = include 'config.php';
$loadClasses = include 'loader.php';$loadClasses($config);
$app = new Application($config);
Now, anyone looking at index.php
can tell at a glance that:
现在,任何查看index.php
人都可以一眼看出:
The file
config.php
returns something (we can guess this is some sort of configuration, but that’s not important now).文件
config.php
返回一些信息 (我们可以猜测这是某种配置,但是现在不重要了)。Both the loader file and the
Application
depend on that something in order to do their work.加载程序文件和
Application
依赖于某种东西来完成其工作。
Much better, isn’t it?
好多了,不是吗?
Let’s take a dig at our second example. We could refactor this in a couple of ways: switch to import
/require
for supported browsers, or use build tools that would provide polyfills for that. But there’s a small change we could make that would make things somewhat better:
让我们来看看第二个例子。 我们可以通过两种方式来重构:切换为import
/ require
支持的浏览器,或者使用构建工具来为此提供polyfill。 但是,我们可以进行一些小的更改,使情况有所改善:
<script src="//some-cdn/cart-fx.js"></script><script src="//some-cdn/cart-utils.js"></script>
/* lots and lots of code */
<script>var cart = new CartFx.Cart(CartUtils.CartManager.default, new Customer());</script>
By attaching the CartManager
and Cart
objects onto the global CartFx
and CartUtils
objects, we’ve effectively moved them into namespaces. We would do the same for any other variables those libraries want to make available, reducing the number of potentially hidden dependencies to just one per module.
通过将CartManager
和Cart
对象附加到全局CartFx
和CartUtils
对象,我们已经将它们有效地移动到了名称空间中。 对于那些库想要提供的任何其他变量,我们将执行相同的操作,从而将潜在隐藏的依赖项的数量减少到每个模块一个。
有时候,你就是无能为力 (Sometimes, you just can’t help it)
There are times where you might be constrained by available tools, limited resources and whatnot. It’s important, though, to keep in mind that what seems so obvious to you, the author of the code, might not be so to a newcomer. Look for little optimizations you can make to improve this.
有时候,您可能会受到可用工具,有限资源等的限制。 不过,重要的是要记住,对于您(代码的作者)来说,如此显而易见的事物对于新手而言可能并非如此。 寻找一些可以改进的优化方法。
Have any experiences with hidden dependencies or techniques for handling them? Do share in the comments.
有隐藏的依赖项或处理它们的技术方面的经验吗? 分享评论。
翻译自: https://www.freecodecamp.org/news/eliminating-hidden-dependencies-a95c7b03aa54/
隐藏依赖