В программном обеспечении зависимость возникает, когда один модуль в приложении, A, зависит от другого модуля или среды, B. Скрытая зависимость возникает, когда A зависит от B неочевидным образом.

Чтобы обнаружить скрытую зависимость, обычно приходится копаться в исходном коде модуля. Модуль может относиться к чему угодно, от всей службы до класса или функции до нескольких строк кода.

Вот небольшой пример зависимости, в которой сравниваются два способа ее выражения:

let customer = Customer.find({id: 1});
// explicit — the customer has to be passed to the cart
function Cart(customer) {
 this.customer = customer;
}
let cart = new Cart(customer);
// hidden — the cart still needs a customer,
// but it doesn’t say so outright
function Cart() {
 this.customer = customer; // a global variable `customer`
}
let cart = new Cart();

Заметили тонкую разницу? Обе реализации конструктора Cart зависят от объекта клиента. Но первый требует, чтобы вы передали этот объект, а второй предполагает, что в среде уже есть объект клиента.

Разработчик, увидевший let cart = new Cart() , не сможет определить, что объект корзины зависит от глобальной переменной клиента, за исключением случая, когда он взглянул на конструктор корзины.

Скрытые зависимости в дикой природе

Я поделюсь несколькими примерами скрытых зависимостей, с которыми я столкнулся в реальных кодовых базах.

  • include файлы PHP

Возьмем типичное серверное приложение PHP. В нашем index.php, точке входа нашего приложения, мы могли бы иметь что-то вроде этого:

include 'config.php';
include 'loader.php';
$app = new Application($config);

Код выглядит подозрительно, не так ли? Откуда взялась переменная $config? Давайте посмотрим.

Директива include аналогична тегам HTML <script>. Он сообщает интерпретатору взять содержимое указанного файла, выполнить его и, если он имеет оператор возврата, передать возвращаемое значение вызывающей стороне. Это способ разделения кода на несколько файлов. Подобно тегу <script>, include может также помещать переменные в глобальную область видимости.

Давайте посмотрим на файлы, которые мы добавляем. Файл config.php содержит типичные параметры конфигурации для серверного приложения:

$config = [
  'database' => [
    'host' => '127.0.0.1',
    'port' => '3306',
    'name' => 'home',
  ],
  'redis' => [
    'host' => '127.0.0.1',
  ]
];

loader.php - это, по сути, самодельный загрузчик классов. Вот упрощенная версия его содержания:

$loader = new Loader(__DIR__);
$loader->configure($config);

Видите проблему? Код в loader.php (и остальной код в index.php) зависит от некоторой переменной с именем $config, но не очевидно, где определено $config, пока вы не откроете config.php. Этот шаблон кодирования на самом деле не редкость.

  • Включение файлов JavaScript с помощью тегов <script>

Вероятно, это более частый пример. Сравните следующие два фрагмента кода (предположим, что cart-fx и cart-utils - это какие-то случайные библиотеки JS):

Выставка:

<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>

Приложение B:

import Cart from ‘cart-fx’;
import CartManager from ‘cart-utils’;
/* lots and lots of code */
const cart = new Cart(CartManager.default, new Customer());

Во втором случае очевидно, что Cart и CartManager переменные были введены (импортированы) из модулей cart-fx и cart-utils соответственно. В первом нам остается только догадываться, какой модуль владеет Cart, а какой - CartManager. И не забывайте Customer too! (Помните, наш собственный код также является модулем.)

  • Чтение из окружающей среды

Я виноват в этом. Некоторое время назад я создал пакет PHP для взаимодействия с API. Пакет позволяет передавать ваши ключи API конструктору. К сожалению, это также позволило вам вместо этого указать свои ключи как переменные среды, и пакет автоматически их использует. Посмотри, и не смейся надо мной.

Когда я это сделал, я решил, что облегчаю жизнь разработчику. На самом деле, однако, я делал предположения о среде, в которой выполнялся код, и привязывал функциональность пакета к определенным условиям, выполняемым в среде.

Итак, чем опасны скрытые зависимости?

Я могу назвать две основные причины:

  • Легко случайно удалить зависимый модуль, не удаляя зависимость. Например, возьмем случай с моим пакетом выше. Представьте, что разработчик настраивает приложение, которое использует пакет в новой среде. Решая, какие переменные среды перенести из старой среды, разработчик может пренебречь добавлением необходимых для пакета, потому что они не могут найти их использованные где-либо в кодовой базе.
  • Небольшое изменение в зависимом коде может привести к поломке всего приложения или появлению ошибок. Возьмем случай с нашим index.php файлом выше: замена первых двух строк может показаться безобидным изменением, но это приведет к поломке. приложение, потому что строка 2 зависит от переменной, установленной в строке 1. Еще более серьезным случаем этого может быть что-то вроде этого:
$config = […];
include 'bootstrap.php';
$app = new Application($config);

Предположим, наш bootstrap.php файл вносит важные изменения в $config. Если по какой-то причине вторая строка будет перемещена вниз, приложение будет работать без каких-либо ошибок, но важные изменения конфигурации, которые делает bootstrap.php, будут невидимы для приложения.

Избавление от скрытых зависимостей

Как и в большинстве программных разработок, здесь нет жестких правил работы со скрытыми зависимостями, но я нашел несколько основных принципов, которые мне подходят:

  1. Пишите действительно модульный код, а не просто разбивайте его на несколько файлов. Идеальный модуль стремится быть автономным и иметь минимальную зависимость от общего глобального состояния. Модуль также должен явно указывать свои зависимости.
  2. Уменьшите количество предположений, которые модуль должен делать о своей среде или других модулях.
  3. Сделайте понятный интерфейс. В идеале, помимо таких вещей, как сигнатуры функций / классов, пользователю вашего модуля не нужно смотреть на исходный код, чтобы выяснить, каковы зависимости модуля.
  4. Избегайте засорения окружающей среды. Не поддавайтесь искушению добавить переменные в родительскую область. Как можно чаще предпочитайте явно возвращать или экспортировать переменные вызывающей стороне.

Я покажу, как можно реорганизовать первый пример выше, используя эти принципы. Первое, что нужно сделать, это заставить файл конфигурации вернуть массив конфигурации, чтобы вызывающий мог явно сохранить его в переменной:

// config.php
return [
  'database' => [
    'host' => '127.0.0.1',
    'port' => '3306',
    'name' => 'home',
  ],
  'redis' => [
    'host' => '127.0.0.1',
  ]
];

Следующее, что мы можем сделать, это изменить файл загрузчика, чтобы он возвращал функцию. Эта функция примет параметр конфигурации. Таким образом, мы проясняем, что процесс загрузки файлов зависит от некоторой предустановленной конфигурации.

// loader.php
return function (array $config)
{
  $loader = new Loader(__DIR__);
  $loader->configure($config);
}

Собрав их вместе, мы получим наш index.php file, который выглядит так:

$config = include 'config.php';
(include 'loader.php')($config);
$app = new Application($config);

Мы можем даже пойти немного дальше, сохранив возвращаемую функцию в переменной перед ее вызовом:

$config = include 'config.php';
$loadClasses = include 'loader.php';
$loadClasses($config);
$app = new Application($config);

Теперь любой, кто смотрит на index.php, может сразу сказать, что:

  1. Файл config.php возвращает что-то (мы можем предположить, что это какая-то конфигурация, но сейчас это не важно).
  2. И файл загрузчика, и Application зависят от этого чего-то для выполнения своей работы.

Намного лучше, не правда ли?

Давайте рассмотрим наш второй пример. Мы могли бы реорганизовать это несколькими способами: переключиться на _34 _ / _ 35_ для поддерживаемых браузеров или использовать инструменты сборки, которые предоставят для этого полифилы. Но есть небольшое изменение, которое мы могли бы сделать, чтобы кое-что улучшить:

<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>

Прикрепив CartManager и Cart объекты к глобальным CartFx и CartUtils объектам, мы эффективно переместили их в пространства имен. Мы сделали бы то же самое для любых других переменных, которые эти библиотеки хотят сделать доступными, уменьшив количество потенциально скрытых зависимостей до одной на модуль.

Иногда ты просто ничего не можешь с собой поделать

Бывают случаи, когда вы можете быть ограничены доступными инструментами, ограниченными ресурсами и прочим. Однако важно помнить, что то, что кажется таким очевидным вам, автору кода, может не быть таковым для новичка. Ищите небольшие оптимизации, которые вы можете сделать, чтобы улучшить это.

У вас есть опыт работы со скрытыми зависимостями или методами их обработки? Делитесь в комментариях.