Как стать автором
Обновить

Можно ли реализовать инкапсуляцию средствами ООП?

Время на прочтение6 мин
Количество просмотров5.3K

Если на Силикатной улице (это в Мытищах) остановить тысячу случайных прохожих и спросить их, на каких трёх слонах покоится ООП, каждый второй назовёт инкапсуляцию. В коридорах МИФИ, или на собеседовании в Яндексе — процент будет даже выше. Даже 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 (самые упоротые — реализуют хаки на аспектах и рефлексии). Потому что все равно никакой инкапсуляции, размазанной по пяти сущностям, не бывает. Пока, конечно, писатели не додумаются до введения терминов «кооперативная инкапсуляция» и «вытесняющая инкапсуляция», но я надеюсь на легитимизацию эвтаназии до наступления этого светлого будущего.

Удачной паттернофобии!

Теги:
Хабы:
+19
Комментарии158

Публикации

Ближайшие события