Найти в Дзене

Абстракции. Во сне и наяву.

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

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

Разработчик Кеша
Разработчик Кеша

Это Кеша. Кеша начинающий разработчик. Кеша хочет стать хорошим разработчиком. Давайте поможем Кеше разобраться с абстракциями.

Определение абстракции

Введем определение абстракции. Хотя для определения это пока не очень четко сформулировано. Назовем это смыслом.

Абстракция — это то, что должно сделать нашу программистскую жизнь проще и понятней, добавив семантически емкое утверждение для использования в клиентском коде.

Проще и Понятней! Это очень важно! Запомните на всю жизнь.

Если то, что вы сделали, использовать в последствие неудобно, значит избавьтесь от этого скорее. Эта абстракция — плохая абстракция. Если это абстракция вообще. Скорее всего (да точно!) можно сделать проще.

А еще, понятие абстракции может трактоваться по-разному разными специалистами. Вот например, паттерн Фасад — это абстракция или не абстракция? Вроде как упрощает жизнь, вроде как вносит понятную семантику, но называется паттерн, а не абстракция 🤔 Или паттерн в данном случае есть реализация абстракции?

А еще наследование — это же тоже вроде как можно назвать реализацией абстракции. Или функция, возвращающая функцию.

Может и так.

В общем, если кто может поделиться другим определением абстракции — буду признателен.

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

Введение закончили, теперь давайте посмотрим примеры.

Перейдем к примерам

В качестве примера рассмотрим кейс моей разработки Презентатора. Я использую в нем Vue, поэтому запускать клиентский код буду в хуке onMounted.

Итак, имеем такое ТЗ — нужно отрисовать на экране квадрат черного цвета 100 на 100 пкс.

Как мы сделаем это в самом начале?

Видимо как-то так:

Рисуем на странице черный квадрат 100 на 100 пкс.
Рисуем на странице черный квадрат 100 на 100 пкс.

Что с этим решением не так? Да ничего. При текущих вводных, решение подойдет, и его следует оставить.

Единственно, можно все же вынести создание квадрата в отдельную функцию:

Экстрактируем создание квадрата в функцию getBox
Экстрактируем создание квадрата в функцию getBox

Или даже вот так:

Запись в optional-chaining
Запись в optional-chaining

Хотя я не очень люблю optional-chaining. На мой взгляд, читабельность кода падает, да и вносит это какой-то налет неопределенности что ли. Я практически всегда предпочитаю условную конструкцию. Да и в данном случае, хотелось бы обработать вариант отсутствия корневого элемента на странице и уведомить пользователя. Оставляем if.

Итак, задачу мы выполнили. Отлично. Обратите внимание, кстати, что количество строк в варианте с функцией getBox() выросло. Мы добавили абстракцию, можно сказать. И в данном случае это увеличило объем кода. Но вынос функции в какой-либо модуль все же уменьшит клиентский код и повысит читаемость.

Так так, секунду. «Это и все? — спросите вы. — Мы рассмотрели вопрос?». Конечно нет. Идем дальше.

Признаки абстракции

Давайте порассуждаем. Можно ли назвать функцию getBox() абстракцией? На мой взгляд — нет. Это просто вспомогательная функция, или какой-то будущий метод в каком-то классе.

До абстракции не хватает:

  • системности;
  • семантики;
  • декларативности.

Что все это значит?

Системность

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

Системность
Системность

Семантика

Используемый элемент должен занимать свою семантическую роль. Т.е. быть фигурой в какой-либо системе. Мы хотим показать квадрат — мы показываем квадрат.

Семантика
Семантика

Декларативность

Клиентский код должен максимально кратко описывать требование бизнес-логики, не вдаваясь в детали реализации. Кейс из примера выше — императивный все же.

Декларативность
Декларативность

Рассмотрим абстракции

Давайте вернемся к ТЗ и реализации. Повторюсь, если ТЗ остается таким — не выдумывайте ничего больше с этим квадратом. Оставляйте.

В моем же ТЗ Презентатора необходимо манипулировать объектами и объекты будут разные — от примитивов до виджетов. Здесь уже стоит подумать об абстракциях.

Движок рендеринга сцены Презентатора отчасти напоминает движок HTML-DOM. Но со своими специалитетами. И без всего того, что мне пока не требуется.

Теперь рассмотрим абстракции.

Сейчас схема реализации компонентов выглядит так:

Узел -> Примитив -> Фигура -> Фасад -> Компонент -> Виджет

В данном случае можно увидеть, что каждый уровень (слой) реализует свои задачи, а так же имеет свою семантику.

Рассмотрим семантику каждого слоя.

Узел

Реализует логику движка рендеринга. Здесь инициализация и обновление атрибутов, мета-данные, методы отрисовки и обновления, манипуляция с другими узлами. На данном уровне Узел ничего не знает о том, каким в итоге окажется компонент — текстом, картинкой или flex-блоком.

Примитив

Реализует базовую логику работы любого объекта. Содержит общие атрибуты для будущих производных классов. А так же логику работы с декораторами.

Фигура

Вот здесь уже задается специфика конкретного объекта. Текста или же flex-контейнера. Сцену презентации уже можно строить из этих объектов.

Фасад

Вообще говоря, для создания Фигур создан специальный сервис-фабрика. Через нее можно заказать создание определенной Фигуры. Примерно по аналогии с document в Web-API.

Однако, данный сервис кроме того умеет строить дерево компонентов и выполнять некоторые другие полезные функции.

Слой фасада я добавил для удобства работы с примитивами, преследуя максимальную декларативность.

Компонент

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

Виджет

В данном случае, Виджет — это семантическая группа Компонентов. Ничего особенного.

Создаем фигуры

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

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

Давайте теперь заглянем в код и посмотрим примеры использования. И, наконец, рассмотрим моменты, которыми хотел поделиться.

Чтобы создать объект «ящика» нужно воспользоваться сервисом Tree. По некоторым причинам, примитивы не создаются напрямую через вызов соответствующего конструктора, хотя технически это, конечно же, можно сделать.

Создание фигуры Box
Создание фигуры Box

Ну и чтобы удовлетворить ТЗ передадим атрибуты:

Создание черного квадрата 100 на 100 пкс. при помощи сервиса
Создание черного квадрата 100 на 100 пкс. при помощи сервиса

Отлично, теперь у нас есть экземпляр объекта черного квадрата 100 на 100 пкс. Но это пока вирутальный объект. Чтобы увидеть его на странице, его нужно замонтировать в DOM.

Для этого у базового класса есть метод mountToElement(), воспользуемся им.

Монтируем фигуру на страницу
Монтируем фигуру на страницу

Теперь черный квадрат виден на страничке. ТЗ выполнено вновь. Но мы же не для этого пошли дальше и стали рассматривать иерархию абстракций? Конечно нет, идем дальше.

Посмотрим вновь на клиентский код создания примитива.

Создание черного квадрата 100 на 100 пкс. при помощи сервиса
Создание черного квадрата 100 на 100 пкс. при помощи сервиса

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

После добавления нового слоя я могу получить тот же ящик вот так:

Создание черного квадрата 100 на 100 пкс. при помощи фасада
Создание черного квадрата 100 на 100 пкс. при помощи фасада

Что ж, стало короче и декларативней.

Вариант const box = tree.create('BoxPrimitive'); я бы все равно назвал императивным. Хотя напрямую я не создаю HTML-элемент, в этом коде я все равно «показываю», как это делается.

Вариант const box = Box(); же, полностью скрывает детали реализации и дает нам легкость манипуляции с целевыми объектами сцены. Хочу ящик — получаю ящик. На мой взгляд, проще уже некуда. Чистая абстракция.

Используем фасады

Теперь давайте заглянем еще дальше и вспомним, как мы получаем черный квадрат:

Создание черного квадрата 100 на 100 пкс. при помощи фасада
Создание черного квадрата 100 на 100 пкс. при помощи фасада

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

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

Как теперь мы можем получить наш черный квадрат?

Вот так:

Задействуем уровень компонентов приложения
Задействуем уровень компонентов приложения

Что ж, стало еще меньше кода. Но добавился еще один слой абстракции — уже на уровне приложения.

☝️ Следует помнить! Чем больше уровней абстракции, тем сложнее будет приложение. И тем выше будет стоимость ошибки в проектировании. Т.к. если потребуется спуститься в самый низ по иерархии и внести изменение в базовый компонент, это может привести к самым непредсказуемым последствиям.
Создавайте абстракции только там, где это действительно необходимо.

Но про это мы еще поговорим много раз.

Используем виджеты

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

Мы уже имеем черный ящик. Давайте добавим текст.

Создание текстового объекта при помощи фасада
Создание текстового объекта при помощи фасада

Как видите, здесь используется фасад.

А теперь соберем виджет.

Сейчас я работаю над созданием Презентатора, поэтому сделаем типовой слайд для презентации при помощи слоя Виджет.

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

Создание заголовочного слайда презентации при помощи виджета
Создание заголовочного слайда презентации при помощи виджета

Поработаем на уровне приложения.

Создадим папку для компонентов дизайн-системы приложения, в ней расположим 2 подпапки — для компонентов и виджетов:

Структура компонентов дизайн-системы на уровне приложения
Структура компонентов дизайн-системы на уровне приложения

Создадим компонент полноэкранного контейнера с выравниванием по центру по обоим осям BlackCanvas:

Создание черного полноэкранного канваса при помощи фасада
Создание черного полноэкранного канваса при помощи фасада

Здесь я взял примитив Flex — этот компонент реализует модель flex-box.

Сделаем компонент главного заголовка презентации MainTitle:

Создание заголовка презентации при помощи фасада
Создание заголовка презентации при помощи фасада

Теперь в папке ds/widgets создадим наш целевой компонент Cover:

Создание заголовочного слайда при помощи виджета
Создание заголовочного слайда при помощи виджета

Отлично, все готово! Теперь покажем наш слайд на экране:

Рендерим заголовочный слайд презентации
Рендерим заголовочный слайд презентации

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

В качестве фона используем наш черный ящик с подключенной моделью flex-box и растянутым на полный экран.

Заголовок центрируем по обеим осям.

А вот и результат:

Hello, World!
Hello, World!

Заключение

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

Мы рассмотрели на примерах какие могут быть абстракции и как их готовить.

Так же обращу ваше внимание на то, что каждый новый уровень абстракции нелинейно усложняет ваше приложение. Не добавляйте их, если на то нет действительной причины.

А еще, уровень Виджет, это совсем не конечный уровень абстракции 😉 Если посмотреть внимательно, то содержимое слайдов можно вынести в config-файлы. А затем на основе них «проигрывать» презентации.

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

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

Для меня это — настоящее искусство.

Хайлайты

Абстракция — это то, что должно сделать нашу программистскую жизнь проще и понятней, добавив семантически емкое утверждение для использования в клиентском коде.

Абстракция — это:

  • системность;
  • семантика;
  • декларативность.

Абстракция — это смысл более высокого порядка, чем код и программистские термины.

Чем больше уровней абстракции, тем сложнее будет приложение и тем выше будет стоимость ошибки в проектировании.

Каждый новый уровень абстракции нелинейно усложняет приложение.

Всем чистого кода!

Подписывайся на мой канал в TG: https://t.me/cantfailcode