Статьи

Альтернативы глобальным переменным и передача одного и того же значения по длинной цепочке вызовов

Старые хорошие глобальные переменные являются неправильным решением реальной проблемы : вы хотите, чтобы некоторая информация была доступна в большом контексте. В объектно-ориентированных языках, таких как Java, у нас часто нет чего-то, что мы называем глобальной переменной. Вместо этого мы лжем себе, используя публичные статические переменные или синглтоны и отрицая, что они — одно и то же. Нет, мы слишком объектно-ориентированные и слишком хорошие разработчики, чтобы использовать глобальные переменные.

Передача значения для всего одного вызова метода: намного лучше?

Итак, вы покаетесь и решите отказаться от глобальных переменных. Чем ты занимаешься? Вы передаете значение явно. Здорово. И затем вы передаете его другому методу и другому, и одно и то же значение снова и снова распространяется, пока оно не станет доступно во многих местах вашего приложения. Намного лучше, а? Вы загрязнили подписи слишком многими методами, и если вы когда-нибудь решите изменить тип передаваемого значения, это потребует большой работы.

противопожарная Ведро бригада

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

Рассмотрим конкретный пример: я работал над инструментом для анализа кода Java, и для некоторых объектов требовался экземпляр TypeResolver : объект, которому дано имя (например, java.lang.String или my.shiny.Class ), предоставил бы им представления. типов. Хороший.

Давайте назовем классы напрямую используя TypeResolver typeResolverUsers . Теперь, кто бы ни использовал typeResolverUsers (назовем их usersOfTypeResolverUsers ), нужен TypeResolver   передать его typeResolverUsers, который он вызывал . Таким образом, каждый usersOfTypeResolverUsers запрашивал TypeResolver во время создания и сохранял его, чтобы позже передать его классам, которые действительно нуждались в нем. Так что теперь, кто бы ни создавал экземпляр одного из usersOfTypeResolverUsers, должен был иметь TypeResolver   ссылка. И так далее, пока слишком многие классы не знали о существовании класса TypeResolver .

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

Другой пример: Position и WorldSize

Давайте подумаем о другом примере: мы хотим представить Позицию на карте мира (подумайте о моем любимом генераторе мира: WorldEngine ). Теперь позиция, конечно, определяется двумя значениями: х и у. Однако, учитывая, что это карта мира, мы хотим иметь возможность оборачивать ее влево-вправо. Таким образом, для выполнения определенных операций нам необходим доступ к размеру мира: если мы знаем, что мир имеет ширину 500 ячеек, когда мы переместимся из Позиции с x = 499 вправо, мы получим x = 0.

В этом сценарии мы могли бы:

  • используйте глобальную переменную: это означает, что в нашей программе есть один возможный размер мира. Он хранится в каком-то месте, доступном из всего приложения. Это работает, если это предположение верно. Если мы вместо этого работаем над несколькими мирами одновременно, нам нужна альтернатива.
  • мы могли бы сохранить размер мира в экземпляре Position . Это означает, что в каждом случае, когда мы хотим создать Позицию, нам нужна ссылка на размер мира. Поскольку приложение постоянно работает с экземплярами Position, это означает, что многие методы (возможно, большинство методов) получат экземпляр WorldSize и передадут его по цепочке.

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

Я думаю, что позиция действительно определяется только x и y. Он должен знать WorldSize для выполнения определенных задач, но это контекстная информация, а не то, что контролируется или принадлежит компании Position .

Контекстная информация: какого черта это?

По этой причине я работаю над концепцией контекстной информации. Сейчас я реализовал это так:

  • мы можем объявить, что определенные значения могут зависеть от контекста: мы бы указали имя и тип для каждого из них
  • мы можем присваивать значения определенной контекстно-зависимой переменной в соответствии с динамической областью действия : это означает, что мы сделаем это значение доступным не только в определенном блоке операторов, но также и для всех функций и методов, вызываемых в этом блоке, рекурсивно
  • значение будет доступно только в данном потоке (да, ThreadLocal для спасения)

Примеры

Это текущий синтаксис, реализованный на моем языке программирования Турин :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
context WorldSize worldSize
 
void myComplexOperation() {
   ...
   // here worldSize is empty
   ...
   context (worldSize = WorldSize(600, 400) {
       // here worldSize has a value
       mySubOperation()
   }
   ...
   // here worldSize is empty
   ...
}
 
void mySubOperation() {
   // here I can access worldSize from the context
   // because I was called in a context which had a value
   // for worldSize
   if (x > context.worldSize.get().width) {
     ...
   }
}

Альтернативные решения

Есть много способов сделать значения доступными для вашего приложения без использования явно глобальных переменных. Один способ использовать Singleton, другой — объявить статические переменные.

Другое решение — RegistryPattern :

Реестр — это глобальная ассоциация ключей и объектов, позволяющая получить доступ к объектам из любого места. Он включает два метода: один, который берет ключ и объект и добавляет объекты в реестр, и другой, который берет ключ и возвращает объект для ключа.

За и против

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

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

Что касается шаблона ContextObject , мы предлагаем проверку типа: мы объявили тип значения контекста, чтобы мы могли убедиться, что ему назначены только совместимые значения.