0 added
0 removed
Original
2026-01-01
Modified
2026-02-26
1
<p><em>Это перевод заметки Эрика Норманда<a>Global Mutable State</a>.</em></p>
1
<p><em>Это перевод заметки Эрика Норманда<a>Global Mutable State</a>.</em></p>
2
<p>Одно из самых проблемных мест в программировании - mutable state - изменяемое состояние. Оно делает код сложным, и как только вы ввязались в него, всё со временем становится более запутанным. Сокращение глобального изменяемого состояния в программе - один из лучших способов повысить качество кода, независимо от того процедурный он или функциональный.</p>
2
<p>Одно из самых проблемных мест в программировании - mutable state - изменяемое состояние. Оно делает код сложным, и как только вы ввязались в него, всё со временем становится более запутанным. Сокращение глобального изменяемого состояния в программе - один из лучших способов повысить качество кода, независимо от того процедурный он или функциональный.</p>
3
<h4>Определение</h4>
3
<h4>Определение</h4>
4
<p>Global mutable state содержит в себе три слова, каждое из которых имеет важное значение:</p>
4
<p>Global mutable state содержит в себе три слова, каждое из которых имеет важное значение:</p>
5
<p><strong>Global</strong>- значит доступный из любого места кода. Таким способом весь код связан. Необходимо рассуждать о взаимодействии всех частей программы, а не её маленьком фрагменте, потому что любая другая часть может касаться этого фрагмента.</p>
5
<p><strong>Global</strong>- значит доступный из любого места кода. Таким способом весь код связан. Необходимо рассуждать о взаимодействии всех частей программы, а не её маленьком фрагменте, потому что любая другая часть может касаться этого фрагмента.</p>
6
<p><strong>Mutable</strong>- означает изменяемый (в русскоязычной среде часто говорят "мутабельный", - прим. ред.). Часто можно заметить: все, что может прочитать значение, может так же и изменить его. Два считывания данных, следующих одно за другим, могут возвращать разные значения. Или, что еще хуже, сами возвращаемые структуры данных изменяются после чтения.</p>
6
<p><strong>Mutable</strong>- означает изменяемый (в русскоязычной среде часто говорят "мутабельный", - прим. ред.). Часто можно заметить: все, что может прочитать значение, может так же и изменить его. Два считывания данных, следующих одно за другим, могут возвращать разные значения. Или, что еще хуже, сами возвращаемые структуры данных изменяются после чтения.</p>
7
<p>Дать определение состоянию (<strong>State</strong>) сложнее. Но, по существу, смысл в том, что значение зависит от истории программы. Насколько глубокой истории? В худшем случае (при наличии глобального изменяемого состояния) полной истории, от начала программы.<strong>Вам нужно знать всё об исполнении программы</strong>, включая то, как чередовались треды.</p>
7
<p>Дать определение состоянию (<strong>State</strong>) сложнее. Но, по существу, смысл в том, что значение зависит от истории программы. Насколько глубокой истории? В худшем случае (при наличии глобального изменяемого состояния) полной истории, от начала программы.<strong>Вам нужно знать всё об исполнении программы</strong>, включая то, как чередовались треды.</p>
8
<p>Если объединить понятия глобальный, мутабельный и состояние, получится грандиозное месиво. Когда кто-то говорит "сложно<a>рассуждать о работе программы</a>",<strong>он подразумевает "в ней есть баги и невозможно понять это, читая код"</strong>.</p>
8
<p>Если объединить понятия глобальный, мутабельный и состояние, получится грандиозное месиво. Когда кто-то говорит "сложно<a>рассуждать о работе программы</a>",<strong>он подразумевает "в ней есть баги и невозможно понять это, читая код"</strong>.</p>
9
<p>Плюс в том, что можно систематически избавляться от этих трёх аспектов. И вы, в принципе, можете удалять их по-отдельности. Я люблю говорить, что функционально программировать можно на любом языке, даже на самых процедурных. Один из способов - сокращать глобальное изменяемое состояние насколько возможно.</p>
9
<p>Плюс в том, что можно систематически избавляться от этих трёх аспектов. И вы, в принципе, можете удалять их по-отдельности. Я люблю говорить, что функционально программировать можно на любом языке, даже на самых процедурных. Один из способов - сокращать глобальное изменяемое состояние насколько возможно.</p>
10
<h4>Выявление глобального изменяемого состояния</h4>
10
<h4>Выявление глобального изменяемого состояния</h4>
11
<p>Моменты, которые его выдают: несколько переменных в глобальной области видимости (в Clojure: несколько atoms в верхнем уровне namespace), чтение и запись данных в глобальные переменные с нечёткими паттернами (или чтение из глобальных переменных несколько раз в маленьком куске кода). Переменная может измениться между считываниями данных.</p>
11
<p>Моменты, которые его выдают: несколько переменных в глобальной области видимости (в Clojure: несколько atoms в верхнем уровне namespace), чтение и запись данных в глобальные переменные с нечёткими паттернами (или чтение из глобальных переменных несколько раз в маленьком куске кода). Переменная может измениться между считываниями данных.</p>
12
<h4>Очистка кода</h4>
12
<h4>Очистка кода</h4>
13
<p>Сложно избавиться от глобального изменяемого состояния, когда оно уже существует. Его применение расползётся по коду, если его не закрепить. Глобальное изменяемое состояние настолько полезно, что его можно использовать в разных целях. Спустя некоторое время сложно понять, какие механизмы использования программы применялись, и как бы вы заменили их. Но мы подробно коснёмся каждого капризного аспекта по очереди.</p>
13
<p>Сложно избавиться от глобального изменяемого состояния, когда оно уже существует. Его применение расползётся по коду, если его не закрепить. Глобальное изменяемое состояние настолько полезно, что его можно использовать в разных целях. Спустя некоторое время сложно понять, какие механизмы использования программы применялись, и как бы вы заменили их. Но мы подробно коснёмся каждого капризного аспекта по очереди.</p>
14
<p><strong>1) Должна ли переменная быть глобальной?</strong></p>
14
<p><strong>1) Должна ли переменная быть глобальной?</strong></p>
15
<p>Предположим, вы можете так переработать код, чтобы объект передавался в функции, вместо того, чтобы быть глобальной переменной. Тогда вы могли бы создавать новый экземпляр каждый раз, когда запускаете код. Это, как минимум, гарантирует, что он каждый раз стартует с известного значения и что вы инкапсулируете мутацию при различных исполнениях.</p>
15
<p>Предположим, вы можете так переработать код, чтобы объект передавался в функции, вместо того, чтобы быть глобальной переменной. Тогда вы могли бы создавать новый экземпляр каждый раз, когда запускаете код. Это, как минимум, гарантирует, что он каждый раз стартует с известного значения и что вы инкапсулируете мутацию при различных исполнениях.</p>
16
<p>Другими словами,<strong>превратите глобальные переменные в локальные</strong>. Лучше всего - локальные для функции, выполняющей мутацию (или для меньшей области видимости, если возможно). В крайнем случае - это переменная из экземпляра локального объекта.</p>
16
<p>Другими словами,<strong>превратите глобальные переменные в локальные</strong>. Лучше всего - локальные для функции, выполняющей мутацию (или для меньшей области видимости, если возможно). В крайнем случае - это переменная из экземпляра локального объекта.</p>
17
<p>Очень заманчиво использовать глобальные переменные, потому что их наличие - простой способ разным фрагментам кода работать совместно. Вот пример:</p>
17
<p>Очень заманчиво использовать глобальные переменные, потому что их наличие - простой способ разным фрагментам кода работать совместно. Вот пример:</p>
18
<p>Давайте попробуем сделать переменные менее глобальными, используя методику описанную выше.</p>
18
<p>Давайте попробуем сделать переменные менее глобальными, используя методику описанную выше.</p>
19
<p>Самая крупная трансформация - это передача объекта state в каждый из методов. Теперь он больше не глобальный. Каждый раз, когда мы запускаем processFile, генерируется новый экземпляр. Мы начинаем с известного исходного состояния и знаем, что у нас не будет конкуренции для этого объекта.</p>
19
<p>Самая крупная трансформация - это передача объекта state в каждый из методов. Теперь он больше не глобальный. Каждый раз, когда мы запускаем processFile, генерируется новый экземпляр. Мы начинаем с известного исходного состояния и знаем, что у нас не будет конкуренции для этого объекта.</p>
20
<p>Другая трансформация была нацелена на то, чтобы больше полагаться на локальные переменные для аккумуляции промежуточных величин. Возможно, это выглядит очень примитивно, но в данном случае объект state ни при каких условиях не содержит неконсистентных данных.<strong>Он либо верный, либо не содержит данных</strong>.</p>
20
<p>Другая трансформация была нацелена на то, чтобы больше полагаться на локальные переменные для аккумуляции промежуточных величин. Возможно, это выглядит очень примитивно, но в данном случае объект state ни при каких условиях не содержит неконсистентных данных.<strong>Он либо верный, либо не содержит данных</strong>.</p>
21
<p><strong>2) Должна ли она быть изменяемой?</strong></p>
21
<p><strong>2) Должна ли она быть изменяемой?</strong></p>
22
<p>Существуют ли такие функции, которые считывают данные из переменной, но ничего не записывают в неё? Их можно изменить, чтобы они принимали текущее значение в качестве аргумента. Уменьшение объёма кода, который полагается на эти конкретные переменные - полезная вещь.</p>
22
<p>Существуют ли такие функции, которые считывают данные из переменной, но ничего не записывают в неё? Их можно изменить, чтобы они принимали текущее значение в качестве аргумента. Уменьшение объёма кода, который полагается на эти конкретные переменные - полезная вещь.</p>
23
<p>Другими словами, пишите код с использованием только аргументов и возвратом значений функций настолько часто, насколько возможно. Изолируйте мутацию переменной маленьким отрезком кода.</p>
23
<p>Другими словами, пишите код с использованием только аргументов и возвратом значений функций настолько часто, насколько возможно. Изолируйте мутацию переменной маленьким отрезком кода.</p>
24
<p>Давайте применим эту методику к нашему коду:</p>
24
<p>Давайте применим эту методику к нашему коду:</p>
25
<p>Код, который записывал данные в изменяемый аргумент, мы перевели в код, который просто возвращает вычисляемое значение. Затем использовали локальные переменные для хранения возвращаемых значений.</p>
25
<p>Код, который записывал данные в изменяемый аргумент, мы перевели в код, который просто возвращает вычисляемое значение. Затем использовали локальные переменные для хранения возвращаемых значений.</p>
26
<p>Заметьте, насколько меньше работы теперь выполняет функция readFile (там просто один вызов). Возможно, мы захотим удалить эту функцию вообще и просто вызывать openFile напрямую. Решать вам, но одна из вещей, которые я часто замечал, удаляя мутацию: функции становятся очевиднее для чтения и записи, а иногда настолько проще, что вам захочется заменить их на inline-вызов.</p>
26
<p>Заметьте, насколько меньше работы теперь выполняет функция readFile (там просто один вызов). Возможно, мы захотим удалить эту функцию вообще и просто вызывать openFile напрямую. Решать вам, но одна из вещей, которые я часто замечал, удаляя мутацию: функции становятся очевиднее для чтения и записи, а иногда настолько проще, что вам захочется заменить их на inline-вызов.</p>
27
<p><strong>3) Должна ли она иметь состояние?</strong></p>
27
<p><strong>3) Должна ли она иметь состояние?</strong></p>
28
<p>Можно ли переработать алгоритмы так, чтобы использовать их натуральные вводы и выводы (аргументы и возвращаемые значения), а не записи во внешний мир? Например, вы используете переменную, чтобы что-то посчитать. Может, вместо добавления в переменную функция будет просто возвращать полную сумму?</p>
28
<p>Можно ли переработать алгоритмы так, чтобы использовать их натуральные вводы и выводы (аргументы и возвращаемые значения), а не записи во внешний мир? Например, вы используете переменную, чтобы что-то посчитать. Может, вместо добавления в переменную функция будет просто возвращать полную сумму?</p>
29
<p>Программам нужно состояние. Но нужно ли нам полагаться на него, чтобы получить правильный ответ? И нужно ли, чтобы состояние зависело от всей истории программы?</p>
29
<p>Программам нужно состояние. Но нужно ли нам полагаться на него, чтобы получить правильный ответ? И нужно ли, чтобы состояние зависело от всей истории программы?</p>
30
<p>Давайте пройдём пошагово наш код, удаляя состояние.</p>
30
<p>Давайте пройдём пошагово наш код, удаляя состояние.</p>
31
<p>Переменная x - это состояние. Её значение зависит от того, сколько раз исполнялось тело цикла. Обычно такой вид цикла со счётчиком не нужен, потому что стандартная библиотека может сама считать коллекцию.</p>
31
<p>Переменная x - это состояние. Её значение зависит от того, сколько раз исполнялось тело цикла. Обычно такой вид цикла со счётчиком не нужен, потому что стандартная библиотека может сама считать коллекцию.</p>
32
<p>Вау! Теперь больше нет состояния. И вообще, тут всё так коротко, что мы можем делать вызов на месте. Функция вызывается всего один раз в processFile. Давайте встроим её сюда.</p>
32
<p>Вау! Теперь больше нет состояния. И вообще, тут всё так коротко, что мы можем делать вызов на месте. Функция вызывается всего один раз в processFile. Давайте встроим её сюда.</p>
33
<p>Вот так лучше. Но у нас всё ещё есть состояние. Его не так много, но давайте продолжим. Заметьте, насколько мы полагаемся на состояние recordCount, передаваемое в generateOutput. Есть ли гарантия, что задаваемый нами счётчик не отличается от счётчика в file? Единственный способ - переместить вычисление recordCount в generateOutput. Почему generateOutput должен доверять кому-то ещё, когда он может вычислять сам?</p>
33
<p>Вот так лучше. Но у нас всё ещё есть состояние. Его не так много, но давайте продолжим. Заметьте, насколько мы полагаемся на состояние recordCount, передаваемое в generateOutput. Есть ли гарантия, что задаваемый нами счётчик не отличается от счётчика в file? Единственный способ - переместить вычисление recordCount в generateOutput. Почему generateOutput должен доверять кому-то ещё, когда он может вычислять сам?</p>
34
<p>А теперь нам не нужна эта маленькая локальная переменная с названием file.</p>
34
<p>А теперь нам не нужна эта маленькая локальная переменная с названием file.</p>
35
<h4>Заключение</h4>
35
<h4>Заключение</h4>
36
<p>Этот простой пример был крайностью. И да, он был очевидным. Но мой опыт подсказывает, что вы заметите те же улучшения, когда будете удалять глобальное изменяемое состояние в реальных системах. О работе всех частей кода становится рассуждать проще (потому что вы делаете это локально). Становится проще рефакторить. Становится проще удалять код.</p>
36
<p>Этот простой пример был крайностью. И да, он был очевидным. Но мой опыт подсказывает, что вы заметите те же улучшения, когда будете удалять глобальное изменяемое состояние в реальных системах. О работе всех частей кода становится рассуждать проще (потому что вы делаете это локально). Становится проще рефакторить. Становится проще удалять код.</p>
37
<p>Сокращать глобальное изменяемое состояние - одна из отличительных черт функционального программирования. Но делать это, значит просто писать<em>хороший</em>код. Вы можете (и должны) проводить подобный рефакторинг в любом языке программирования и при любом подходе.</p>
37
<p>Сокращать глобальное изменяемое состояние - одна из отличительных черт функционального программирования. Но делать это, значит просто писать<em>хороший</em>код. Вы можете (и должны) проводить подобный рефакторинг в любом языке программирования и при любом подходе.</p>