Если на Силикатной улице (это в Мытищах) остановить тысячу случайных прохожих и спросить их, на каких трёх слонах покоится ООП, каждый второй назовёт инкапсуляцию. В коридорах МИФИ, или на собеседовании в Яндексе — процент будет даже выше. Даже LLM способна на шести пальцах объяснить, почему.
И, тем не менее, ООП — один из худших способов обеспечить инкапсуляцию. Идолопоклонники на этом месте могут поставить тексту, мне и вселенной — минус, остальным я на примерах попытаюсь объяснить, что побудило меня к столь резкому заявлению.
Начнем с определения. К сожалению, Вика, как всегда, подкачала (приведу, просто чтобы разрядить обстановку и поржать):
Инкапсуляция (англ. encapsulation, от лат. in capsula) — в информатике, процесс разделения элементов абстракций, определяющих её структуру (данные) и поведение (методы); инкапсуляция предназначена для изоляции контрактных обязательств абстракции (протокол/интерфейс) от их реализации.
Английская версия, как всегда, немного лучше:
Essentially, encapsulation prevents external code from being concerned with the internal workings of an object.
В общем, инкапсуляция — это сокрытие деталей реализации от внешнего мира, которому доступны только те возможности работы с инкапсулируемой сущностью, которые посчитал правильными экспортировать автор этого черного ящика. Механический турок, который перемещает по полю фигуры и кивает, объявляя шах. Человек там внутри, Deep Blue, или ChatGPT — зрителя не колышет.
Проблема в том, что зрителя — обычно колышет.
Особенно, учитывая, что в нашем случае зритель — не совсем зритель, а потребитель кода. Тоже разработчик. Тожеразработчики (как яжематери) — наделены удивительной страстью к вмешательству в дела, которые не имеют к ним никакого отношения. Иначе невозможно придумать ни единого аргумента в пользу существовании рефлексии и аспектов в джаве, конструкции prepend
в руби (и это еще элегантный хак, в отличие от манкипатчинга!), внешнего переопределения функций в питоне и открытости прототипов к любой непотребщине в джаваскрипте.
Мне лично доводилось писать в сырую память по смещению в дельфи, когда в честно купленной мной библиотеке вдруг поломали обратную совместимость, и потребовали денег за апдейт.
Есть такой популярный тезис (интерпретация достаточно вольная, но общность я не терял): все нормальные языки одинаковы, потому что Тьюринг.
Это поверхностный и в целом неверный взгляд на разработку.
Полнота по Тьюрингу определяет только возможности языка. Способности и потребности она никак не регулирует.
А ведь если язык предоставляет возможность для хака, она рано или поздно будет использована. Привет, Мёрфи.
При этом, абсолютно чистые (pure) языки в вакууме, для которых может быть строго математически доказана корректность во всех случаях — практически бессмысленны. Чистые функции — очень красивая модель, неприменимая в повседневной жизни. Привет, Кант.
Компромиссом являются: unsafe
в расте, Any
в тайпскрипте, и всякая прочая потребень, которая перечеркивает жирным хером (в этом определении нет обсценной лексики, только лингвистический вокабуляр) — все потуги сделать, как надо. (Для тех, кто сегодня впервые узнал про причуды церковнославянского литероименования — название языка «C++» должно заиграть новыми красками.)
Короче, что там с инкапсуляцией?
Инкапсуляция должна бы запретить любое изменение внутреннего состояния сущности (объекта) извне любым способом, кроме специально разрешенных автором. Вот классический вариант:
public class Temperature {
Double celcius;
public Double getCelsius() {
return celsius;
}
public void setCelsius(Double value) {
celsius = value;
}
}
После такого рода примеров, людям обычно предлагается дальше дорисовать сову самостоятельно. Люди идут работать в индустрию. И сталкиваются с первым же заданием: добавить еще один атрибут к классу, который я тут приводить не буду, потому что на хабре размер текста публикации ограничен пятью мегабайтами.
Но даже в тривиальном примере выше есть проблема: нам надо бы обработать случай value < -273.15°C
. И вот тут-то инкапсуляция и отыквится. Приведенный пример вроде бы можно починить: либо выбросив Exception
из setCelcius
, либо вернув boolean
вместо void
. Проницательный читатель уже понял, почему инкапсуляция все равно останется поломана, для остальных поясню: кишки внутреннего устройства класса Temperature
теперь торчат наружу. Снаружи придется либо поймать исключение, либо обработать код возврата. И что-то предпринять, если случилась беда. И это нарушит инкапсуляцию. Формально — не нарушит, конечно, мы просто подрихтуем интерфейс, который теперь будет перекладывать часть работы по обеспечению корректной работы инстансов класса на вызывающий код, и скажем, что так и было задумано. Но установка температуры теперь пустит метастазы (а со временем — неминуемо расползется вширь) в вызывающий код. Там будет что-то такое:
Double temp = getTempFromSensor(); // from outside
// temperature.setValue(temp) // won’t work for broken sensor
if (!temperature.setValue(temp)) { /* DO WHAT??? */}
Что-то предпринять, определенно, надо: код не отработал, как было задумано. Но что именно — мы не знаем, потому что (та-дам!) инкапсуляция. Если вам кажется, что пример надуман, — попытайтесь прервать чтение, налить себе кофе, чая, воды, виски, — и придумать (или вспомнить) похожий пример самостоятельно. Уверен, что каждый, написавший хотя бы тысячу строк кода на джаве, когда-то да сталкивался с чем-то похожим.
Ближе к жизни
Для тех, кому надуманные примеры — не указ, у меня заготовлен и настоящий реальный мир. Конечный автомат практически невозможно имплементировать на объектной модели, не потеряв математическое доказательство консистентности.
FSM (без потери общности я буду дальше подразумевать под этим акронимом автоматы Мили, потому что именно они чаще всего используются в разработке) — это как раз тот случай, когда отсутствие инкапсуляции разрушает математическую модель. Промежуточные и конечное состояние автомата — зависит от ① начального состояния, ② внутреннего состояния на каждом шаге и ③ входного сигнала на каждом шаге. Давайте назовем их initial, state и event, ради сестры таланта.
Это означает, строго говоря, что реализация FSM должна экспортировать буквально две функции/метода: start(initial)
и transition(event)
. Предположим для простоты, что у нас всегда есть доступ на чтение к чему угодно, мы сейчас говорим только про те функции/методы, которые собственно обеспечивают работу FSM.
Итак, у нас есть «конструктор» (функция start
) и «пинок» (transition
), который переводит (или не переводит) FSM в другое состояние. Вроде бы, должно быть очевидно, что никакие setState
и тому подобные декларативные конструкции здесь неуместны: функция setState
— это деталь имплементации, которая обязана быть инкапсулирована.
Итак, у нас есть класс, экспортирующий два метода. Его внутреннее состояние. И мы пытаемся использовать этот класс (тут 2025 на дворе, ага) — в высококонкурентной среде. Для простоты и опять-таки без потери общности — пусть это будет турникет (про оригинальность моих примеров складывают легенды, я в курсе). Создали откуда-нибудь из фабрики и дергаем из контроллеров монетоприемника и рычага: event(монета)
, event(пассажир)
, event(бесконтактная карта)
… И тут один пассажир прикладывает карту, второй — бросает монету (конкурентность в метро очень высока, насколько мне известно). Что делать?
Есть только два варианта разрешения конфликта: ① блокировать монетоприёмник и считыватель карт пока турникет открыт, или ② накапливать «проходы» и не закрывать турникет, пока они не закончатся. ① нарушает инкапсуляцию (потроха нашего FSM экспортируются в прошивку железяк самого турникета). ② невозможен в ООП без плясок с бубном. Так появляются Event Channel, Adapter и Façade.
В стейте хранится остаток уже оплаченных проходов, и при попадании в состояние «закрыто» при положительном балансе — турникет открывается сам собой. Здравствуй, Observer. Я мог бы и дальше нагружать эту телегу пограничными условиями, которые необходимо отработать, но и так пока хватит. Мы уже обнаружили себя в ситуации, когда инкапсуляция столь тривиальной стейт-машины, что она приводится почти во всех пособиях в качестве первого, самого простого, примера, размазана по пяти сущностям (паттернам).
В этот самый момент все адекватные люди принимают соломоново решение: гори она, инкапсуляция, синим пламенем, и левой рукой добавляют в имплементацию FSM метод setState
(самые упоротые — реализуют хаки на аспектах и рефлексии). Потому что все равно никакой инкапсуляции, размазанной по пяти сущностям, не бывает. Пока, конечно, писатели не додумаются до введения терминов «кооперативная инкапсуляция» и «вытесняющая инкапсуляция», но я надеюсь на легитимизацию эвтаназии до наступления этого светлого будущего.
Удачной паттернофобии!