Как устранить (или справиться с) скрытыми зависимостями

1656666371 kak ustranit ili spravitsya s skrytymi zavisimostyami

автор Шалвы

odAr3dMCBRLyHI-rlaIudmFTMUp96ncKZz2S
Фото Блейка Конналли на Unsplash

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

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

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

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 <script> теги. Он сообщает интерпретатору получить содержимое указанного файла, выполнить его и, если он имеет оператор возврата, передать возвращаемое значение вызывающему. Это способ разделения кода на несколько файлов. Лike a &lt;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 <script>; теги

Пожалуй, это наиболее распространенный пример. Сравните следующие два фрагмента кода (допустим 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 делает невидимым для программы.

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

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

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

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

// 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 можно с первого взгляда сказать, что:

  1. Файл config.php возвращается что-то (Мы можем предположить, что это какая-то конфигурация, но это сейчас не важно).
  2. И файл загрузчика, и файл 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 объектов, мы фактически переместили его в пространства имен. Мы сделали бы то же самое для других переменных, которые эти библиотеки хотят сделать доступными, уменьшив количество потенциально скрытых зависимостей к одной на модуль.

Иногда вы просто не можете помочь

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *