
Содержание статьи
автор Шалвы

В программном обеспечении зависимость возникает, когда один модуль в программе, Азависит от другого модуля или среды, Б. Скрытая зависимость возникает, когда А зависит от Б способом, который не является очевидным.
Чтобы выявить скрытую зависимость, обычно приходится копаться в исходном коде модуля. Модуль может ссылаться на что угодно: от целой службы до класса или функции и нескольких строк кода.
Вот небольшой пример зависимости, сравнивая два способа ее выражения:
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();
Обратите внимание на тонкую разницу? Обе реализации конструктора Cart зависят от объекта клиента. Но первый требует, чтобы вы передали этот объект, а второй ожидает, что в среде уже есть доступный объект клиента.
Разработчик видит let cart = new Cart()
не могли бы сказать, что объект cart зависит от глобального переменного клиента, за исключением того, чтобы они посмотрели на конструктор Cart.
Скрытые зависимости в дикой природе
Я поделюсь несколькими примерами скрытых зависимостей, которые я встречал в реальных кодовых базах.
Возьмем типовую программу серверного PHP. В нашем `index.php`, точке входа нашей программы, мы могли бы иметь что-то вроде этого:
include 'config.php';include 'loader.php';$app = new Application($config);
Код выглядит подозрительно, не правда ли? Где взял $config
переменная происходит из? Давайте посмотрим.
The include
Директива похожа на HTML <scri
pt> теги. Он сообщает интерпретатору получить содержимое указанного файла, выполнить его и, если он имеет оператор возврата, передать возвращаемое значение вызывающему. Это способ разделения кода на несколько файлов. Лike a &l
t;script>
тег, include также может поместить переменные в глобальную область.
Давайте посмотрим файлы, которые мы включаем. The config.php
файл содержит типовые параметры конфигурации для серверной программы:
$config = [ 'database' => [ 'host' => '127.0.0.1', 'port' => '3306', 'name' => 'home', ], 'redis' => [ 'host' => '127.0.0.1', ]];
The loader.php
это в основном самодельный загрузчик классов. Вот упрощенная версия его содержимого:
$loader = new Loader(__DIR__);$loader->configure($config);
Видите проблему? Код в loader.php
(и оставшийся код в index.php
) зависит от некоторой переменной с названием $config
но непонятно где $config
определяется, пока вы не откроете config.php
. Этот шаблон кодировки на самом деле не редкость.
- Включая JavaScript
<scri
pt>; теги
Пожалуй, это наиболее распространенный пример. Сравните следующие два фрагмента кода (допустим cart-fx
и cart-utils
некоторые случайные библиотеки JS):
Приложение A:
<script src=" src="https://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
тоже! (Помните, что наш собственный код также является модулем.)
- Чтение из окружения
Я виновник этого. Некоторое время назад я создал пакет PHP для взаимодействия с API. Пакет позволял вам передавать ключи API конструктору. К сожалению, это также позволило вам указать ваши ключи как переменные среды, и пакет будет автоматически использовать их. Посмотрите, и не смейтесь надо мной.
Когда я сделал это, я верил, что облегчу жизнь разработчику. Однако на самом деле я предположил среду, в которой работал код, и привязывал функциональность пакета к определенным условиям, которые выполнялись в среде.
Итак, почему скрытые зависимости опасны?
Я могу упомянуть две основные причины:
- Легко ненамеренно удалить зависимый модуль, не удаляя зависимость. Для примера возьмем мой пакет повыше. Представьте, что разработчик настраивает программу, использующую пакет в новой среде. Решая, какие переменные среды перенести из старой среды, разработчик может не добавлять те, которые нужны пакету, поскольку они не могут найти их в кодовой базе.
- Небольшое изменение зависимого кода может нарушить работу целой программы или сделать ее ошибочной. Возьмем наш случай
index.php
файл выше – изменение местами первых двух строк может показаться безвредным изменением, но это приведет к поломке программы, поскольку строка 2 зависит от набора переменных в строке 1. Еще более серьезный случай этого будет примерно таким:
$config = […];include 'bootstrap.php';$app = new Application($config);
Предположим, наш bootstrap.php
файл вносит некоторые важные изменения $config
. Если по какой-либо причине вторая строка будет перемещена вниз, программа будет работать без ошибок, но ключевые изменения конфигурации bootstrap.php
делает невидимым для программы.
Избавление от скрытых зависимостей
Как и в большей части разработки программного обеспечения, нет жестких правил работы со скрытыми зависимостями, но я нашел несколько основных принципов, которые работают для меня:
- Напишите модульный код, а не просто разделить на несколько файлов. Идеальный модуль стремится быть самодостаточным и минимальной зависимости от общего глобального состояния. Модуль также должен явно указывать свои зависимости.
- Уменьшите количество предположений, которые должен сделать модуль о его среде или других модулях.
- Открыть понятный интерфейс. В идеале, кроме таких вещей как подписи функций/классов, пользователю вашего модуля не нужно просматривать исходный код, чтобы выяснить, какие зависимости модуля.
- Избегайте засор окружающей среды. Удерживайтесь от соблазна добавить переменные в родительскую область. Как можно чаще отдавайте предпочтение явному возврату или экспорту переменных к вызывающему.
Я покажу, как мы можем рефакторинговать первый пример выше, используя эти принципы. Первое, что нужно сделать, это создать конфигурационный файл возвращение массив конфигурации, поэтому абонент может явно сохранить это в переменной:
// config.phpreturn [ '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
файл выглядит так:
$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
можно с первого взгляда сказать, что:
- Файл
config.php
возвращается что-то (Мы можем предположить, что это какая-то конфигурация, но это сейчас не важно). - И файл загрузчика, и файл
Application
зависит от этого что-то чтобы выполнять свою работу.
Гораздо лучше, не правда ли?
Давайте разберем наш второй пример. Мы могли бы изменить это несколькими способами: переключиться на import
/require
для поддерживаемых браузеров или воспользуйтесь инструментами создания, которые предоставят полизаполнение для этого. Но есть небольшое изменение, которое может несколько улучшить ситуацию:
<script src=" src="https://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
объектов, мы фактически переместили его в пространства имен. Мы сделали бы то же самое для других переменных, которые эти библиотеки хотят сделать доступными, уменьшив количество потенциально скрытых зависимостей к одной на модуль.
Иногда вы просто не можете помочь
Есть случаи, когда вы можете быть ограничены доступными инструментами, ограниченными ресурсами и т.д. Однако важно иметь в виду, что то, что кажется столь очевидным для вас, автора кода, может не быть таковым для новичка. Ищите небольшие оптимизации, которые вы можете сделать, чтобы улучшить это.
У вас есть опыт работы со скрытыми зависимостями или методами их обработки? Поделитесь в комментариях.