Глобальные состояния: зачем и как их избегать

Original author: The Valuable Dev
  • Translation

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

Всё это может быть раздражающими последствиями глобалов: скрытых переменных, меняющих своё состояние в неизвестных местах, по причинам, которые вы ещё не выяснили.

Вам нравится блуждать во тьме, пока вы пытаетесь изменить приложение? Конечно, не нравится. К счастью, у меня есть для вас свечи:

  1. Во-первых, я опишу то, что мы чаще всего называем глобальными состояниями. Этот термин не всегда применяется точно, поэтому требует пояснения.
  2. Далее мы узнаем, чем глобалы вредны для нашей кодовой базы.
  3. Затем я объясню, как урезать область видимости глобалов, чтобы превратить их в локальные переменные.
  4. И наконец, я расскажу об инкапсуляции и о том, почему крестовый поход против глобальных переменных является лишь частью большой проблемы.

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

Ты готов, дорогой читатель, вскочить на коня и познать своего врага? Вперёд, найдём эти глобалы и заставим их отведать вкус стали наших мечей!

Что такое состояние?




Начнём с основ, чтобы вы, разработчики, понимали друг друга.

Состояние (state) — это определение системы или сущности. Состояния встречаются в реальной жизни:

  • Когда компьютер выключен, его состояние — выключен.
  • Когда чашка чая горячая, её состояние — горячая.

В разработке ПО некоторые конструкции (например переменные) могут иметь состояния. Скажем, строка «hello» или число 11 не считаются состояниями, они значения. Они становятся состоянием, когда прикрепляются к переменной и помещаются в память.

<?php

echo "hello"; // No state here!
$lala = "hello"; // The variable $lala has the state 'hello'.

Можно выделить два вида состояний:

Изменяемые состояния: после своей инициализации могут меняться в ходе исполнения вашего приложения в любой момент.

<?php

$lala = "hello"; // Initialisation of the variable.
$lala = "hallo"; // The state of the variable $lala can be changed at runtime.

Неизменяемые состояния: не могут меняться в ходе исполнения. Вы присваиваете своей переменной первое состояние, и его значение впоследствии уже не меняется. «Константами» в обиходе называют примеры неизменяемых состояний:

<?php

define("GREETING", "hello"); // Constant definition.
echo GREETING; 
GREETING = "hallo"; // This line will produce an error!

Теперь давайте послушаем гипотетическую беседу между Денисом и Василием, вашими коллегами-разработчиками:

— Дэн! Ты везде насоздавал глобальные переменные! Их нельзя поменять без того, чтобы всё не сломалось! Я тебя прибью!
— Нифига, Васёк! Мои глобальные состояния офигенные! Я вложил в них душу, это шедевры! Я обожаю свои глобалы!

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

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

<?php

namespace App\Ecommerce;

$global = "I'm a mutable global variable!"; // global variable

class Shipment
{
    public $warehouse; // local variable existing in the whole class

    public function __construct()
    {
        $info = "You're creating a shipment object!"; // local variable bound to the constructor scope
        echo $info;
    }
}

class Product
{
    public function __construct()
    {
        global $global;
        $global = "I change the state now 'cause I can!"; 
        echo "You're creating a product object!"; // no state here
    }
}

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

Если вы и правда так подумали, очень рекомендую продолжить чтение.

Глобальные состояния хуже чумы и холеры?


Самая большая диаграмма связей


Факт: вам будет проще создавать точную диаграмму связей приложения, если она содержит только локалы с маленькими и определёнными областями видимости, без каких-либо глобалов.

Почему?

Допустим, у вас большое приложение с глобальными переменными. Каждый раз, когда вам нужно что-то поменять, вам придётся:

  • Вспомнить, что существуют эти изменяемые глобальные состояния.
  • Прикинуть, повлияют ли они на область видимости, которую вы собираетесь менять.

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

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

Это ещё не всё. Если вам нужно поменять состояние глобалов, то вы не будете представлять, на какую область видимости это повлияет. Приведёт ли это к неожиданному поведению другого класса, метода или функции? Успехов в поиске.

Короче, вы объедините все классы, методы и функции, использующие одинаковые глобальные состояния. Не забывайте: зависимости сильно повышают сложность. Вас это пугает? Должно бы. Маленькие определённые области видимости очень полезны: вам не нужно держать в голове всё приложение, достаточно помнить лишь о тех областях, с которыми работаете.

Люди плохо отслеживают сразу большой объём информации. Когда мы пытаемся так делать, то быстро истощаем запас когнитивных возможностей, нам становится трудно сосредоточиться, и мы начинаем создавать баги и глупости. Вот почему так неприятно действовать в глобальной области видимости вашего приложения.

Коллизии имён глобалов


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

  • Во-первых, вам понадобится выяснить, что ваша библиотека использует глобальные переменные.
  • Во-вторых, вам понадобится вычислить, какая переменная использовалась в ходе исполнения — ваша или библиотеки? Это не так просто, имена-то одинаковые!
  • В-третьих, раз вы не можете самостоятельно изменить библиотеку, придётся переименовать свои глобальные изменяемые переменные. Если они использованы по всему приложению, вы будете рыдать.

На каждом этапе вы будете рвать волосы от ярости и отчаяния. Скоро вам уже не понадобится расчёска. Вряд ли вас соблазняет такой сценарий. Возможно, кто-то вспомнит, что JavaScript-библиотеки Mootools, Underscore и jQuery всегда конфликтовали друг с другом, если их не помещать в более мелкие области видимости. А, и знаменитый глобальный объект $ в jQuery!

Тестирование превратится в кошмар


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

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

Проблемы с параллелизмом


Изменяемые глобальные состояния могут доставить много проблем, если вам необходим параллелизм (concurrency). Когда вы меняете состояние глобалов в нескольких потоках исполнения, то по уши вляпаетесь в мощное состояние гонки.

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

Избегание глобальных изменяемых состояний




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

Возьмём REST API: конечные точки получают какие-то HTTP-запросы с параметрами и отправляют ответы. Эти HTTP-параметры, отправленные на сервер, могут быть востребованы на многих уровнях вашего приложения. Очень соблазнительно сделать эти параметры глобальными при получении HTTP-запроса, модифицировав их перед отправкой ответа. Добавляем сверху в каждый запрос параллелизм, и рецепт катастрофы готов.

Глобальные изменяемые состояния могут также напрямую поддерживаться в реализации языка. Например, в PHP есть суперглобалы.

Если у вас откуда-то взялись глобалы, то как с ними быть? Как рефакторить приложение Дениса, вашего коллеги-разработчика, который создал глобалы везде, где только можно, потому что за последние 20 лет он ничего не читал по разработке?

Аргументы функций


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

<?php

namespace App;

use Router\HttpRequest;
use App\Product\ProductData;
use App\Exceptions;

class ProductController
{
    public function createAction(HttpRequest $httpReq)
    {
        $productData = $httpReq->get("productData");

        if (!$this->productModel->validateProduct($productData)) {
            return ValidationException(sprintf("The product %d is not valid", $productData["id"]));
        }

        $product = $this->productModel->createProduct($productData);
    }
}

class Product
{
    public function createProduct(array $productData): Product
    {
        $productData["name"] = "SuperProduct".$productData["name"]; // This is not what you should do; I talk about it later in the article.

        try {
            $product = $this->productDao->find($productData["id"]);
            return product;
        } catch (NotFoundException $e) {
            $product = $this->productDao->save($productData);
            return $product;
        }
    }
}

class ProductDao
{
    private $db;

    public function find(int $id): array
    {
        return $this->db->find(['product' => $id]);
    }

    public function save(array $productData): array
    {
        return $this->db->saveProduct($productData);
    }
}

Как видите, массив $productData из контроллера, через HTTP-запрос, проходит через разные уровни:

  1. Контроллер получил HTTP-запрос.
  2. Параметры переданы в модель.
  3. Параметры переданы в DAO.
  4. Параметры сохранены в базе данных приложения.

Мы могли бы сделать этот массив параметров глобальным, когда извлекли его из HTTP-запроса. Кажется, что так проще: не нужно передавать данные в 4 разные функции. Однако передача параметров в качестве аргументов функций:

  • Очевидно покажет, что эти функции используют массив $productData.
  • Очевидно покажет, что какие функции используют какие параметры. Видно, что для ProductDao::find из массива $productData нужен только $id, а не всё подряд.

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

Вы уже слышите, как Денис протестует: «А если у функции уже три и более аргументов? Если нужно добавить ещё больше, то вырастет сложность функции! И что насчёт переменных, объектов и других конструкций, которые везде нужны? Будете передавать их каждой функции в приложении?».

Вопросы справедливые, дорогой читатель. Как хороший разработчик, вы должны объяснить Денису, использовав свои навыки общения, вот что:

«Денис, если в твоих функциях слишком много аргументов, то проблемой могут быть сами функции. Вероятно, они делают слишком много, отвечают за слишком много вещей. Ты не думал разделить их на более мелкие функции?».

Ощущая себя оратором в афинском Акрополе, вы продолжаете:

«Если тебе нужны переменные во многих областях видимости, то это проблема, и скоро мы об этом поговорим. Но если они и правда тебе нужны, то что плохого в передаче их через аргументы функций? Да, тебе придётся их набрать на клавиатуре, но мы же разработчики, это наша работа — писать код».

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

Контекстные объекты


Контекстными называют те объекты, которые содержат данные, определённые каким-то контекстом. Обычно эти данные хранятся в виде конструкции «ключ-пара», как например ассоциативный массив в PHP. У такого объекта нет поведения, только данные, аналогично объекту-значению.

Контекстный объект может заменить любое глобальное изменяемое состояние. Вернитесь к предыдущему примеру кода. Вместо передачи данных из запроса через уровни мы можем использовать объект, инкапсулирующий эти данные.

Контекстом будет сам запрос: другой запрос — другой контекст — другой набор данных. Затем контекстный объект будет передан любому методу, которому понадобятся эти данные.

Вы скажете: «Это офигенно и всё такое, но что это даёт?»

  • Данные инкапсулированы в объекте. Чаще всего вашей задачей будет сделать данные неизменяемыми, то есть чтобы вы не могли изменить состояние — значение данных в объекте после инициализации.
  • Очевидно, что контексту нужны данные контекстного объекта, поскольку они передаются всем функциям (или методам), которым эти данные нужны.
  • Это решает проблему параллелизма: если у каждого запроса будет собственный контекстный объект, вы можете безопасно записывать или считывать их в их собственных потоках исполнения.

Но у всего в разработке есть цена. Контекстные объекты могут вредить:

  • Глядя на аргументы функции, вы не будете знать, какие данные лежат в контекстном объекте.
  • В контекстный объект можно положить что угодно. Осторожнее, не положите слишком много, например, всю пользовательскую сессию, или даже большую часть данных вашего приложения. А то может получиться такое: $context->getSession()->getUser()->getProfil()->getUsername(). Нарушите закона Деметры, и вашим проклятием станет безумная сложность.
  • Чем больше контекстный объект, тем сложнее узнать, какие данные и в какой области видимости он использует.

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

Если перед исполнением программы вы понятия не имеете, сколько состояний будет передано вашим функциям (например, параметры из HTTP-запроса), то контекстные объекты могут быть полезны. Поэтому их некоторые фреймворки их используют, вспомните, к примеру, объект Request в Symfony.

Внедрение зависимостей


Другой хорошей альтернативой глобальным изменяемым состояниям будет прямое внедрение нужных вам данных в объект прямо при его создании. Это определение внедрения зависимости: набора методик для внедрения объектов в ваши компоненты (классы).

Почему именно внедрение зависимостей?


Цель — ограничить использование ваших переменных, объектов или иных конструктов, поместить их в ограниченную область видимости. Если у вас есть зависимости, которые внедрены, а следовательно могут действовать только внутри области видимости объекта, то вам будет проще узнать, в каком контексте они используются и почему. Никакой тоски и мучений!

Внедрение зависимостей делит жизненный цикл приложения на две важные фазы:

  1. Создание объектов приложения и внедрение их зависимостей.
  2. Использование объектов для достижения ваших целей.

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

Многие фреймворки используют внедрение зависимостей, иногда в довольно сложных схемах, с конфигурационными файлами и Dependency Injection Container (DIC). Но вовсе не обязательно всё усложнять. Вы можете просто создавать зависимости на одном уровне и внедрять их уровнем ниже. Например, в мире Go я не знаю никого, кто использовал бы DIC. Ты просто создаёшь зависимости в основном файле с кодом (main.go), а затем передаёшь их на следующий уровень. Можно также инстанцировать всё подряд в разные пакеты, чтобы чётко обозначить, что «фаза внедрения зависимостей» должна выполняться только на этом конкретном уровне. В Go области видимости пакетов могут сделать какие-то вещи проще, чем в PHP, в котором DIC’и широко применяются в каждом известном мне фреймворке, в том числе в Symfony и Laravel.

Внедрение через конструктор или сеттеры


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

  • Если вам нужно знать, какие есть зависимости у класса, вам достаточно найти конструктор. Не нужно искать разрозненные по всему классу методы.
  • Настройка зависимостей при установке даст вам уверенность в безопасности использования объекта.

Немножко поговорим о последнем пункте: это называется «применение инварианта» (enforcing invariant). Создавая экземпляр объекта и внедряя его зависимости, вы знаете: что бы ни понадобилось вашему объекту, он настроен правильно. А если вы используете сеттеры, как вы узнаете, что ваши зависимости уже заданы в момент использования объекта? Можете пойти в стек и попробовать выяснить, вызывались ли сеттеры, но я уверен, что вам не хочется этим заниматься.

Нарушение инкапсуляции


В конце концов, единственное различие между локальными и глобальными состояниями — это их области видимости. Они ограничены у локальных состояний, а для глобальных доступно всё приложение. Однако вы можете столкнуться с проблемами, характерными для глобальных состояний, если используете состояния локальные. Почему?

Ты сказал «инкапсуляция»?


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

Начнём с начала. Что нам говорит Википедия про определение инкапсуляции? Языковой механизм ограничения прямого доступа к каким-то компонентам объекта. Ограничение доступа? Зачем?

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

Растущая область видимости и утечки состояний




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

Возьмём пример: Anemic Domain Model может увеличивать область видимости ваших изменяемых моделей. По сути, Anemic Domain Model делит данные и поведение ваших доменных объектов на две группы: модели (объекты только с данными) и сервисы (объекты только с поведением). Чаще всего эти модели будут использоваться во всех сервисах. Следовательно, есть вероятность, что какой-то модели будет всё время расти область видимости. Вы не будете понимать, какая модель в каком контексте используется, их состояние изменится, и на вас обрушатся все те же проблемы.

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

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

Как определить такие области видимости? Следуйте бизнес-модели вашей компании. Приложение должно быть зеркалом бизнеса, а значит области видимости должны содержать состояния и поведения, отражающие бизнес-модель.

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

Возможности копирования состояний


Во многих случаях хорошим решением будет копирование состояний без их прямого изменения. Вернёмся к нашему примеру с Product, точнее, к этому методу:

class Product
{
    public function createProduct(array $productData): Product
    {
        $productData["name"] = "SuperProduct".$productData["name"]; // This is not what you should do; I talk about it later in the article.

        try {
            $product = $this->productDao->find($productData["id"]);
            return product;
        } catch (NotFoundException $e) {
            $product = $this->productDao->save($productData);
            return $product;
        }
    }
}

Массиву $productData лучше оставаться неизменяемым. Если вы напрямую поменяете его состояние, а затем передадите другим функциям, то вскоре просто не сможете узнать, какое состояние принял этот массив.

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

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

Лучше сделать так:

class Product
{
    public function createProduct(array $productData): Product
    {
        // Since $productData is passed to other variable, it has to be immutable.
        $name = "SuperProduct".$productData["name"];

        try {
            $product = $this->productDao->find($productData["id"]);
            return product;
        } catch (NotFoundException $e) {
            $product = $this->productDao->save($name, $productData);
            return $product;
        }
    }
}

Вы ясно показали, что имя продукта не такое же, как исходное имя продукта из массива $productData. Вы продемонстрировали, что состояние изменено. Если вам нужно будет передать $productData в любой другой метод, то вы будете знать, что он всегда содержит исходные данные из HTTP-запроса.

Вы даже можете изолировать это изменение состояние в отдельном методе, чтобы было ещё очевиднее: «Внимание, я сейчас меняю это состояние».

Что насчёт глобальных неизменяемых состояний?




В любых приложениях можно встретить глобальные неизменяемые состояния. Например, такое состояние у констант.

Безопасно ли их использовать?

  • Вам не нужно думать о том, какое же состояние у вашего глобала, оно будет одинаковым по всему приложению.
  • Как следствие, чтобы узнать состояния ваших неизменяемых переменных, вам достаточно посмотреть только на первое присваивание (желательно, чтобы оно шло после определения) переменных.

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

К примеру, константа ShipmentDelay будет использована, надеюсь, только там, где реализована логика отгрузки товара. А если Денис, ваш коллега-разработчик, начнёт использовать ShipmentDelay для другой задержки, не относящейся к отгрузкам, то ваш глобал будет использоваться там, где он не имеет смысла. Глупо? Я видел много разработчиков, которые делают подобные странные вещи во имя священного принципа DRY.

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

Нужно ли находить и уничтожать все глобальные состояния?


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

Однако приложениям свойственно увеличиваться. Поэтому помните:

  • Нужно по мере возможности избегать глобальных изменяемых состояний.
  • Уменьшить область видимости любого глобального изменяемого состояния можно с помощью аргументов функций, внедрения зависимостей и контекстных объектов.
  • Глобальные переменные — лишь верхушка айсберга: на самом деле нам важно соблюдать общий принцип инкапсуляции, который нарушается разными путями, лишь одним из которых являются глобальные изменяемые состояния.
  • Глобальные неизменяемые состояния менее вредны, но их тоже лучше не применять где попало.

В разработке ПО не бывает однозначно хороших и однозначно плохих решений. Всё бывает полезным в зависимости от ситуации, и одна из самых трудных задач — принять верное решение в ваших условиях. Поэтому экспериментируйте, создавайте прототипы и думайте о последствиях.

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

Причиной всех описанных выше проблем являются сами состояния, локальные или глобальные, и их изменяемость. Если всё будет неизменяемым, то в большинстве приложений проблемы будут возникать реже. Но очевидно, что нам нужно менять состояния, чтобы приложения работали как нужно. Поэтому помните, что всегда лучше всеми силами избегать изменяемости!
Mail.ru Group
1,134.79
Building the Internet
Share post

Comments 19

    +1

    Когда чашка чая горячая, её состояние — остывающая.

      0

      Когда она остыла до комнатой температуры, ее состояние "Ожидает нагревания".

      +2
      Ну программирование же не только веб…
      Так-то бывают даже автоматы состояний.
        +2
        И в вебе тоже бывают, даже чаще, чем можно подумать.
        0

        Шел 2019 год, программисты PHP продолжали неравный бой с глобальным состоянием...

          +5
          PHP часто ругают, и даже заслуженно.
          Но. PHP практически полностью избавляет программиста от забот параллельного исполнения кода. Поэтому PHP-программисты особо не парясь используют глобальные переменные. Никакой другой язык этого бы не простил.

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

          В общем, это уже лопата, но ещё не трактор. Лопату можно доверить и зелёному подростку, а вот трактор — нет.
          И сколько там солдатов из стройбатов заменяют экскаватор?
            +1
            PHP практически полностью избавляет программиста от забот параллельного исполнения кода. Поэтому PHP-программисты особо не парясь используют глобальные переменные.

            Извините, не вижу связи. Любой MVC фреймворк из 2010 года избавляет программиста от забот параллельного исполнения кода. В том числе и PHP фреймворки. И ни один из них не заставляет (и даже не предлагает) использовать глобальные переменные.


            А за 10 лет можно было уже впитать в экосистему хорошие практики, "выкинуть" статьи и книжки, описывающие проблемные места — вернее написать новые, которые заместят старые.


            Поэтому для меня подобная статья — как статья про вред goto. Немножечко из прошлого...

              +3
              В том числе и PHP фреймворки

              Разница в том, что PHP защита от переживаний о глобальном состоянии — это на уровне языка. Принципиально невозможно, чтобы один запрос (web request) повлиял на контекст другого запроса.
              В PHP принципиально нет возможности написать программу, в которой переменную изменяет код в разных потоках.
              Да, с глобальным переменными можно накосячить и в одном потоке.


              Поэтому ваша отсылка — "в том числе и в PHP фрейморках" — некорректна: от PHP фрейморков в этом плане ничего не зависит, это свойство самого PHP.

                0
                Ну это пришло из модели работы cgi — на каждый входящий запрос отдельный процесс, который завершается после формирования ответа.
                  –1
                  В PHP принципиально нет возможности написать программу, в которой переменную изменяет код в разных потоках.
                  А это я сейчас что сделал?
                  <?php
                  
                  class Example extends Thread {
                      public $value = 0;
                  
                      public function run()
                      {
                          for ($i = 0; $i < 50; ++$i) {
                              $this->value += 1;
                          }
                      }
                  };
                  
                  $example = new Example();
                  $example->start();
                  
                  for ($i = 0; $i < 50; ++$i) {
                      $example->value += 1;
                  }
                  
                  $example->join();
                  
                  var_dump($example->value); // Выводит числа от 50 до 100, когда как
                  
                    0
                    pnctl_fork и можно очень сильно обрадоваться
                  –1
                  PHP практически полностью избавляет программиста от забот параллельного исполнения кода. | Никакой другой язык этого бы не простил. | Я имею в виду параллельные web-requests, которые приходят на сервер. | PHP защита от переживаний о глобальном состоянии — это на уровне языка.
                  Неправда. Описанная технология не имеет ни малейшего отношения к PHP, и в действительности является заслугой протоколов CGI и FastCGI, которые реализуют веб-серверы, такие как Apache и nginx. Более того, через эти самые протоколы спокойно подключаются десятки других языков, от питона, руби и луа до плюсов, сей и джавы, на тех же самых условиях — параллельная однопоточная обработка веб-запросов.

                  Причем фактического ограничения на однопоточность при обработке запроса нет даже в самом PHP, как можно увидеть в примере что приведен глубже в ветке. Это просто не очень целесообразно, но если очень захочется никто не запретит, достаточно включить стандартное расширение pthreads в php.ini.
                    0

                    "малейшее" таки имеет — PHP изначально делался с упором на интеграцию с Apache, так что это по сути одна экосистема со своими стандартами, язык с веб-сервером взаимно влияли друг на друга.
                    Можете считать PHP универсальным языком без привязки к каким-либо практикам, но давайте по-честному — многое на уровне языка заточено именно под среду Apache и принцип "без забот параллельного исполнения".
                    А то, что есть Ratchet и т.п. — это уже надстройки над тем, что дано из коробки.


                    pthreads, к слову, адски неудобен в эксплуатации, в частности если в несколько потоков работать с подключениями к БД и прочими ресурсами. А хороших аналогов я не нашёл, что приводит к выводу, что ограничение однопоточностью фактически есть, хоть и теоретически можно выйти за его рамки. Только вот почти никому уже это не надо, разве что демоны писать (но это обычно делают уже на других языках).

                      0
                      так что это по сути одна экосистема со своими стандартами, язык с веб-сервером взаимно влияли друг на друга
                      Где я могу об этом почитать? Гугл сходу ничего похожего не выдает, а в статьях посвященных истории разработки PHP Apache упоминается вскользь полтора раза, и то не всегда, и наоборот.
                      Можете считать PHP универсальным языком без привязки к каким-либо практикам
                      Такого утверждения я не делал.
                      многое на уровне языка заточено именно под среду Apach
                      Что конкретно на уровне именно языка заточено под Apache? Приведите пожалуйста хотя бы один пример.

                      Основное отличие PHP в режиме командной строки и в режиме веб-программы это переданный набор суперглобалов с инфой о сервере, запросе и т.д. Все его библиотечные функции и биндинги либо есть в стандартных библиотеках других языков, либо несложно подключаются. Все его statements, операторы, типы, и прочие концепции присутствуют в других языках, кроме разве что фич шаблонизатора, но к Apache это не имеет никакого отношения. Связь с веб-сервером при юзе CGI работает через stdin+stdout, т.е. не требует никаких изменений в языке.

                      принцип «без забот параллельного исполнения»
                      Опять же, что конкретно в PHP способствует этому принципу, кроме того что на уровне языка нет ключевых слов для мультипоточности, как и в подавляющем большинстве всех других языков?

                      А хороших аналогов я не нашёл, что приводит к выводу, что ограничение однопоточностью фактически есть
                      Ну уж нет. Фактически поддержка есть, и даже не pthread единым, а ее удобство это уже отдельный вопрос. Изначальное утверждение что PHP в принципе этого не позволяет — неверно.

                      Да и вообще, весьма странно брать «отсутствие в PHP забот параллельного исполнения» как что-то положительное, для чего PHP якобы специально дорабатывался, а позже аргументировать это тем что «в PHP адски хреновая библиотека потоков». Складывается впечатление что эту библиотеку специально хреновой сделали, и за счет этого PHP избавляет от забот о мультипоточности.
                        0
                        Связь с веб-сервером при юзе CGI работает через stdin+stdout, т.е. не требует никаких изменений в языке.

                        Вот именно, что при юзе CGI. Но самое классическое применение, которое поощряли даже разработчики самого PHP — это mod_php в Apache, об этом ниже.


                        Что конкретно на уровне именно языка заточено под Apache? Приведите пожалуйста хотя бы один пример.

                        Как минимум стандартные функции, которые начинаются с apache_..., и прочие отсылки к вебу на уровне ядра. Они не выделены в отдельные расширения/модули и валяются в стандартной сборке PHP. Более универсальный язык, не заточенный изначально под конкретную среду, выносил бы подобный функционал в дополнения, а не стандартные функции языка.


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


                        Фактически поддержка есть, и даже не pthread единым, а ее удобство это уже отдельный вопрос. Изначальное утверждение что PHP в принципе этого не позволяет — неверно.

                        Если что-то реализовано так, чтоб с ним невозможно было работать без дозы кислоты — это не "не позволяет", но и не "позволяет".
                        Вроде технически можно, а на практике за это почти никто браться не будет. У меня один хороший знакомый пытался уже потоки подключить (для неблокирующей работы с лонг-поллингом VK), после недели страданий ушёл в запой и поставил Gearman.


                        Да и вообще, весьма странно брать «отсутствие в PHP забот параллельного исполнения» как что-то положительное, для чего PHP якобы специально дорабатывался, а позже аргументировать это тем что «в PHP адски хреновая библиотека потоков». Складывается впечатление что эту библиотеку специально хреновой сделали, и за счет этого PHP избавляет от забот о мультипоточности.

                        Это скорее историческая данность, чем "специально". PHP своё место в рынке уже давно чётко обрисовал, многопоточности от него мало кто хотел. Кому прям очень надо или пришёл highload — перешли на систему очередей задач или внешние скрипты на других языках.


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

                          0
                          Но самое классическое применение, которое поощряли даже разработчики самого PHP — это mod_php в Apache
                          Да, но и этот способ не требует изменений внутри языка или его реализации. Как и у прочих интерпретируемых языков, интерпретатор php может быть легко встроен в другое приложение, что с ним и сделали.
                          Как минимум стандартные функции, которые начинаются с apache_..., и прочие отсылки к вебу на уровне ядра. Они не выделены в отдельные расширения/модули и валяются в стандартной сборке PHP.
                          Все эти функции находятся в server-specific расширении apache, которое добавляет тот самый mod_php. Они не являются ни частью языка, ни даже его стандартной библиотеки. Более того, если использовать модули Apache для других языков, они предоставят доступ к аналогичным функциям.
                          что замысел был в языке для веба
                          Я с этим и не спорю. Разработчики PHP несомненно учитывали его нишу при выборе направления развития. Моя мысль в том, что никакого конкретного функционала PHP, который бы «избавлял от проблем многопоточности» или «способствовал параллельным web-requests», как утверждал автор корневого комментария, на уровне языка у PHP нет. И уж тем более в нем нет ничего что позволяет утверждать что «никакой другой язык этого бы не простил». Вместо php можно подключить любой другой скриптовый язык без встроенных потоков (или запретить их вручную), например Lua, и в отношении потоков, web-requests и вреда глобальных переменных, все будет точно так-же. Прям один в один.
                            0

                            Ваша мысль — она про то, как фактически реализован язык. Автор, скорее всего, имел в виду не заточку самого интерпретатора, а то, как его обычно внедряют и используют: в самых популярных связках (Apache + mod_php & nginx + php-fpm) язык действительно "избавляет от проблем многопоточности".


                            В общем, спор тут, кажется, сугубо о формулировках.

                              0
                              Автор, скорее всего, имел в виду не заточку самого интерпретатора, а то, как его обычно внедряют и используют: в самых популярных связках (Apache + mod_php & nginx + php-fpm)
                              Автор несколько раз повторил «на уровне языка», «свойство самого PHP» и сделал вполне конкретное утверждение про переменные. По моему смысл вполне однозначен. Жаль только, что в ответ на пример кода и контр-аргументы вместо диалога с пояснением автора, почему я не прав, мне почти сразу же прилетели только минусы и плевок в карму.
                +1

                Глобальное изменяемое состояние уровня приложения — безусловное зло. Но тут есть еще один момент, которого нет в статье. Есть несколько кейсов, когда глобальное состояние есть и с этим ничего не сделать:


                1. СУБД. Ладно, когда ваше приложение снаружи, вы хотя бы можете сделать вид, что это не ваше дело (на самом деле — нет). Но в финансах много legacy приложений, где основной код написан на PL/SQL или T-SQL. И вот это уже ад в аду.
                2. Когда вы пишете ОС или работаете с железом.
                3. Когда вы пишете своё (нагруженное) сложное приложение (ту же СУБД или что-то подобное)
                4. Когда вы пишете что-то общее уровня приложения. Например кэш (к упомянутой уже СУБД)
                  Все эти случаи имеют много глобальных хранилищ состояний той или иной степени модульности и управляемости. Это сильно усложняет и разработку, и тестирование.

                Only users with full accounts can post comments. Log in, please.