Вроде бы всем известно что инкапсуляция это полезная штука, но мало кто знает что в практических задачах она никогда не является целью. Да, она является признаком удачного решения, когда ее можно обнаружить, идентифицировать в связанных фрагментах кода, или же ее отсутствие будет кричать о дырявости реализованной концепции. Но нельзя ставить себе целью инкапсуляцию — это абстрактное понятие обычно (практически всегда) трансформируется в фантомную цель которая уведет вас в сторону от решения вашей практической задачи.
На идею этой статьи меня натолкнула следующее цитата брошенная в запале дискуссии:
Вы часто видели, чтобы в тредах об ООП на «инкапсуляция помогает скрывать данные и реализацию» кто-то всерьёз отвечал «нет! компилятор можно пропатчить, чтобы он игнорировал
private
!
Вы тоже думаете что инкапсуляция это всегда про использование модификаторов private, public, protected
? Или каких-то других модификаторов? А чистый Си поддерживает инкапсуляцию? Но это все более менее известные вопросы, я предлагаю вам познакомиться (или вспомнить?) концепцию абсолютной инкапсуляции, которая не обходится только модификаторами, а обеспечивается чуть ли не инфраструктурой операционной системы. Естественно начнем с формулировки практической задачи в которой нам пригодится эта абсолютная инкапсуляция.
Эта статья продолжает идею о способах разделения больших проектов на части.
Кодирование и коды очень распространены в программировании. Мы кодируем и видео, и аудио, и просто файлы чтобы меньше занимали места, шифрование вообще специальный, самодостаточный вид кодирования, даже сериализация это в некотором смысле кодирование-перекодирование данных.
Для примеров, нам будет достаточно интерфейса всего с двумя функциями и мы совершенно не будем погружаться в какие-то бы ни было подробности реализации. Мы снова разберем идею разделения программы на подпрограммы и/или разделения большого проекта на подпроекты.
Представьте что у нас есть некоторый алгоритм кодирования (криптования,… не важно, Не будем говорить чего, нам не нужны подозрения в рекламе), алгоритм надо инициализировать каким-то начальным значением, а может целой структурой данных, допустим для этого надо вызвать функцию
int initCript(string seed);
и после этого мы можем многократно вызывать функцию
int encode(string datain, string dataout);
То есть функция инициализации задает внутреннее состояние алгоритма кодирования, а функция кодирования выполняет некоторый сложный алгоритм который может правильно работать только если корректно (в некотором смысле) заданы управляющие этим алгоритмом структуры данных.
Эти функции в таком случае очевидно связаны и логично объединить их в одном интерфейсе:
interface IEncoder
{
int initCript(string seed);
int encode(string datain, string dataout);
}
А теперь вообразите такую ситуацию: у нас есть обычные десктопные компьютеры с обычным процессором на котором алгоритм кодирования реализуется чисто программным способом, а есть другие компьютеры которые имеют встроенную аппаратную функцию, которая выполняет тот же самый алгоритм в 10 раз быстрее. Соответственно, мы должны в зависимости от типа компьютера (железа) вызывать, либо программную реализацию функции, либо мы должны вызывать реализацию, которая просто передает данные в железо и выполняет функцию аппаратными средствами.
Таким образом у нас должно быть две реализации одной и той же функции в зависимости от железа! Допустим они, эти две реализации у нас есть, но как нам построить нашу программу которая будет использовать — создавать и вызывать объект нужного класса в рантайме? Для справки, это фактически одна из основных проблем, которую решает технология DirectX, например.
Очевидно нам придется написать два класса
class SoftEncoder : interface Iencoder
{
… //внутренние-инкапсулированные поля/методы
int initCript(string seed){...}
int encode(string datain, string dataout){..программная реализация..}
}
class HWEncoder : interface IEncoder
{
… //внутренние-инкапсулированные поля/методы
int initCript(string seed){...}
int encode(string datain, string dataout){..аппаратная реализация..}
}
В технической англоязычной литературе принято называть код который вызывает функции класса и создает объекты этого класса кодом клиента (хотя вызывающий функции код не обязательно должен сам создавать объекты класса, пока обращаю ваше внимание на это, дальше мы разберем что не так с непосредственным созданием объектов подробнее), но нам такое название совершенно не годится, оно очень неодназначное и будет путать читателя, мне кажется.
Допустим фрагменты кода клиента который работает с нашим Енкодором, который надо выбрать в зависимости от возможностей аппаратной платформы выглядит так:
…
IEncoder* enc;
if(hwEncode == true)
enc = HWEncoder ();
else
enc = SoftEncoder ();
...
enc-> initCript(robustSeed);
…
//в каком то напряженном цикле:
enc-> encode(datain, dataout);
...
Тем кто занимается перекладыванием данных между базами данных и представлениями этих данных внутри какого-то визуального интерфейса, например в браузере наверно трудно будет представить, что реализация всего одной функции кодирования или декодирования (например одного видео фрейма MPEG формата) может составлять несколько файлов кода с множеством этапов, вспомогательных функций, таблиц данных из стандарта, и еще фиг знает чего. Людей которые не верят что сложность кода может превышать сложение с умножением в цикле очень трудно убедить в необходимости разделять большие алгоритмически+математически сложные программы на проекты, которые компилируются отдельно, но мы все таки попробуем, так как у такого разделения есть несомненные преимущества с какой бы стороны вы эту технику не рассматривали.
Нам надо, все таки, как то именовать эту основную, вызывающую функции нашего интерфейса, программу, давайте присвоим ей наименование Хост, что называется коротко и в данном контексте не с чем перепутать. То есть Хост это класс всего приложения и таким образом у нас получается схема приложения относительно нашего интерфейса:

Как вы видите я не большой специалист по генерации привлекательных картинок, но картинка нужна чтобы пояснить идею и такой мне кажется все таки достаточно, при наличии пояснений.
Смысл стрелок привязан к их цвету:
Синие стрелки обозначают реализацию интерфейса в соответствующих классах (этап написания программы);
Зеленые стрелки обозначают отношения создания объектов классов (этап запуска программы инициализации, подготовки к работе);
Оранжевые стрелки обозначают использование объекта класса через интерфейс.
При желании, на этой картинке, можно обратить внимание что зеленые стрелки создания перечеркивают уровень интерфейса, можно сказать проникают туда куда не надо (бы!).
Есть и классический аргумент о том что при создании классов напрямую из кода, которому нужен только интерфейс для работы, привязывает вызывающий код (код нашего Хоста) к коду реализаций наших Енкодеров. То есть с любыми изменениями в классе любого из наших Енкодоров нам придется пере компилировать и все приложение Хоста и (может быть самое неприятное) нам придется заменить это приложение на всех компьютерах на которых оно развернуто! Эту проблему как раз и решает абсолютная инкапсуляция, звучное название придуманное для заголовка, но вполне адекватное тем целям, которые оно декларирует.
Можно сделать так что Хосту не надо будет знать с типом какого объекта он работает через интерфейс! То есть мы не просто скрываем какие-то поля или приватные методы нашего класса, мы сам класс делаем неизвестным для того кто пользуется интерфейсом и кому и нужен только интерфейс от нашего класса.
Для этого нам нужно разделить наш монолитный проект, а у него уже была различимая внутренняя структура разделенная по классам и интерфейсам, но он все еще был монолитным(!). Все в одном неделимом проекте как в одном куске мрамора, но теперь мы можем разделить этот кусок на под проекты которые будут компилироваться независимо, и не только компилироваться но и распространяться! При изменениях в одном Енкодере нам не нужно будет менять ВСЕ приложения Хостов на всех машинах на которых они развернуты! К сожалению, мало кто из русскоязычной аудитории сможет разделить мой восторг по поводу такой возможности, потому что это уже практически забытая технология, которая в русскоязычном сегменте данной компетенции никогда и не была освоена и никто не мог почувствовать насколько эта технология облегчает жизнь на практике.
кажется, что-то временно пошло не так
Насколько я знаю, в сегменте англоязычной компетенции по той же теме, все гораздо сложнее. Да! Это они увлекли весь мир новой религией и алхимией в одном флаконе, идеей об искусственном интеллекте божественной силы, как я это называю, и сами начали в это активно играть. Но в этом сегменте компетенции уже давно и надежно обосновались профессионально построенные концепции-архитектуры и на базе фундаментальных основ и фундаментального понимания проблем программирования и соответствующих этому пониманию способов и методов их решения. Они уже не могут этого потерять!
Как известно, человечеству понадобилось примерно полторы тысячи лет чтобы пройти через схоластику и вернуться к основам научного знания заложенного в Древней Греции. Интересно сколько понадобится времени теперь, чтобы перестать пытаться заново изобретать основы программирования, а просто начать их систематизировать, структурировать и просто внимательно изучать?
Делили на 2 части получилось 3.
Теперь начнем с картинки:

У нас получилась очень необычная и, наверно, даже странная картинка и она несомненно требует пояснений!
Первый вывод который можно (и наверно нужно) сделать глядя на эту картинку разделение монолитного приложения-проекта на подпроекты очень не тривиальная задача. Для примера мы попытались выделить из нашего приложения Хоста класс Енкодера, как если бы этот Енкодер существовал у нас в единственном экземпляре реализации (например только SoftEncoder). Фактически мы нарисовали схему деления целого проекта на два подпроекта (основной проект + библиотека), но оказалось что при этом мы неизбежно получаем три подпроекта:
Хост — основной проект, вызывающий код, он же код использующий интерфейс;
Библиотека интерфейсов которая компилируется один раз и уже скомпилированная входит в состав и Хоста, и Енкодера (каждого из Енкодеров если их больше одного);
Библиотека с классом Енкодер который обеспечивает реализацию интерфейсов с которыми (с реализациями, абсолютно инкапсулированными!) будет работать вызывающий код Хоста, в нашем случае.
В общем-то это должно быть очевидно, что выделить фрагмент кода (Енкодер здесь) который взаимодействует с оставшейся частью приложения нельзя без того чтобы еще и не локализовать и не выделить способ этого взаимодействия в отдельном интерфейсном проекте, поэтому, как например в ядерной физике деление ядра из двух искомых частиц рождает генерацию фотона, который как бы не существовал в реальном мире, но также как интерфейс в программировании фотон являются связующей энергией в составе монолитного ядра.
А еще у нас появился еще один интерфейс который я назвал (обозначил)

Это с одной стороны такой незаметный аспект реализации абсолютной инкапсуляции, а с другой стороны это одна из важнейших составляющих технологии которая обычно излагается со множеством сложнейших подробностей что, по моему, очень мешает пониманию ее роли. Я же попробую изложить ее в самом примитивном виде который, тем не менее, позволяет осознать смысл этого важного аспекта.
Итак, мы теперь не можем создать класс который реализует нужный нам интерфейс просто потому что мы этот класс не знаем, он приходит к нам в скомпилированной библиотеке, и пока наше приложение работает уже, с некоторой первой реализацией Енкодера, нас может ожидать новое железо, которое выполняет функцию кодирования еще быстрее и для этого железа будет написан новый класс и библиотека которую мы получим должна будет просто заменить существующую библиотеку на новых машинах.
Как же создать класс который мы не знаем и который возможно еще даже не написан? Оказывается все очень просто, в каждой новой библиотеке в которой реализован новый класс Енкодера достаточно иметь предопределенную функцию с примерно такой (в самом примитивном случае) сигнатурой:
IEncoder* createEncoder();
или
void createEncoder(IEncoder** iencoder, string encoderType, string createErrors);
то есть функция может не только создать инкапсулированный в библиотеке объект и вернуть известный вызывающему коду интерфейс, но и сообщить какую-то информацию о созданном объекте, например что создан программный Енкодер или аппаратный Енкодер, и сообщить о проблемах если создать Енкодер не получилось.
Выводы
Мы сформулировали понятие об абсолютной инкапсуляции классов (типов) которая обеспечивается с помощью статических и/или динамических библиотек которые поддерживаются на уровне операционных систем. Абсолютная инкапсуляция заключается в том что библиотека скрывает не только какие-то определенные поля, методы, данные, состояние определенные в классе, при абсолютной инкапсуляции вызывающему коду остается неизвестным даже сам тип (класс) объекта, который реализует публичный интерфейс, через который приложение работает с этим объектом неизвестного типа (класса). Таким образом разработчики библиотек и разработчики приложений (сервисов, ...) с вызывающим кодом могут работать независимо и параллельно пока они не выходят за рамки предопределенных интерфейсов, которые составляют так называемый контракт между приложением и библиотеками. Наличие таких контрактов и заданных ими разделений на подпроекты также упрощает и сокращает процедуры развертывания приложений на машинах пользователей.
Комментарии (4)
Tsvetik
07.08.2025 03:50Что-то очень сложно написано.
Все это называтся полиморфизм.В Си интерфейсом является правильно написаный хидер (скрывающий конкретные типы за opaque указателями), а с помощью линкера в программу прицепляется конкретная реализация. Это т.н. link-time полиморфизм.
jdev
07.08.2025 03:50Абсолютная инкапсуляция чем-то принципиально отличается от Dependency Inversion Principle и Чистой архитектуры, о которых (в моём инфопузере) каждая собака знает? Не увидел этого в статье
IlyasA74
Уточните, пожалуйста, на каком ЯП написаны ваши примеры?