Русский
Русский
English
Статистика
Реклама

Композиция

ExtendScript Работа с композициями

25.02.2021 16:13:17 | Автор: admin

<- Предыдущая статья ExtendScript Работа с файлами

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

Одной из самых распространенных задач, которые встречались мне в работе была динамичная расстановка титров в видеороликах. Видео сегодня, один из самых популярных форматов контента. Одно из ограничений накладываемых браузером, невозможность запустить в ролике звук без действия пользователя. Естественный выход из данной ситуации, заменить звук титрами. Если это 5 - 10 титров, то можно сделать их и руками. Ну а что, если вам надо выпускать по пять роликов в день и в них не 10, а 50 - 70 титров? Да еще каждый ролик в 2 - 3 форматах. Да плюс ко всему, еще и на нескольких языках? Если вам кажется такая ситуация необычной, то я сталкиваюсь с ней ежедневно.

Так давайте же упростим эту задачу на сколько это возможно. Мы сделаем более менее универсальный скрипт, который будет интерпретировать текст в титры на сцене After Effects. Хоть я и решал эту задачу уже много раз, буду писать свой скрипт прямо во время написания статьи, чтобы ничего не упустить и сделать свой рассказ максимально подробным. Приступим.

Текст титров мы будем вводить следующим образом

#simple Текст первого титраТип данного титра simple#simple Титры simple могут быть и в одну строку#double Текст третьего титраТип этого титра double

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

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

{(function init(){   // Тут весь код из скрипта первой статьи})();}

Далее мы исправим дефолтный текст в текстовом поле

var editText = win.add(   'edittext',   [0, 0, 300, 300],   'Введите текст титров',   {multiline: true});

Ну а в обработчике нажатия кнопки run мы удалим строку выполнения кода, заменив ее на вызов метода createTitres

btnRun.onClick = function () {   try {       createTitres(editText.text.getTitresData());   } catch (err) {       alert(err)   }};

Соответственно нам надо создать метод createTitres, принимающий аргументом некий массив данных.

function createTitres(data) {}

И расширить класс String, добавив ему метод getTitresData, который будет парсить текст, создавая из него массив объектов. Это в свою очередь влечет небольшое расширение класса Array, ему мы добавим метод map. Приведу оба расширения вместе.

String.prototype.getTitresData = function() {   return this       .replace(/(^\n|^ |^"|\n+$| +$|"+$)/g, "")       .split("\n\n")       .map(function(d) {           var str = d.replace(/(^\n|^ |\n+$| +$)/g, "")           var tag = str.split(" ")[0];           var text = str.replace(tag + ' ', '');           return {               type: tag.replace('#', ''),               text: text,           }       });}Array.prototype.map = function(callback) {   var arr = [];   for (var i = 0; i < this.length; i++) {       arr.push(callback(this[i]));   }   return arr;}

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

this.replace(/(^\n|^ |^"|\n+$| +$|"+$)/g, "")

Далее мы разбиваем текст по разделителю двойной перенос

.split("\n\n")

И формируем из получившегося массива новый, отделяя от каждого титра тег

.map(function(d) {  var str = d.replace(/(^\n|^ |^"|\n+$| +$|"+$)/g, "")  var tag = str.split(" ")[0];  var text = str.replace(tag + ' ', '');  return {    type: tag.replace('#', ''),    text: text,  }});

Я запустил скрипт. В метод createTitres приходит массив объектов. Пока все верно. Можно двигаться дальше.

Теперь нам следует поработать над макетом. Открываем After Effects. Создаем новый проект. В окне Project добавим папку, назовем ее 1x1. В этой папке создадим композицию с именем ModelScene_1x1

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

Настраиваем нашу сцену.

Указываем ширину, высоту, частоту кадров, продолжительность сцены. Значение Duration лучше сделать побольше, чтобы влезли все титры. Все эти параметры мы позже скопируем в нашу рабочую сцену.

Макет сцены готов. Вернемся к скрипту.В методе createTitres первым действием мы получим данные всех сцен имеющихся в нашем макете.

function createTitres(data) {   var scenesData = getScenesData();}function getScenesData() {   var doc = app.project;   var data = [];   for (var i = 1; i <= doc.numItems; i++) {       var item = doc.item(i);       if (item instanceof CompItem &&           /^ModelScene/.test(item.name)) {           data.push({               type: item.name.split('_')[1],               width: item.width,               height: item.height,               frameRate: Math.floor(1 / item.frameDuration),               duration: item.duration           });       }   }   return data;}

В новом методе getScenesData мы проходимся по всем элементам в нашем проекте. Обратите внимание, что индексация элементов в списке проекта начинается с единицы. Общее число элементов хранится в свойстве проекта numItems.

for (var i = 1; i <= doc.numItems; i++)

Мы смотрим, является ли элемент композицией и есть ли в его имени префикс ModelScene.

if (item instanceof CompItem &&    /^ModelScene/.test(item.name))

Если условие выполнено, добавляем данные о сцене в массив.

data.push({    type: item.name.split('_')[1],    width: item.width,    height: item.height,    frameRate: Math.floor(1 / item.frameDuration),    duration: item.duration});

Нас интересует тип сцены, отделяем суффикс от ее имени. Ширина, высота, частота кадров и продолжительность сцены. Частоту кадров мы выссчитываем из свойства frameDuration - длительность одного кадра в секундах.

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

function createTitres(data) {   var scenesData = getScenesData();   for(var i = 0; i < scenesData.length; i++) {       var scene = getScene(scenesData[i]);   }}function getScene(data) {   var sceneName = 'scene-' + data.type;   return getItem(sceneName, CompItem) ||       app.project.items.addComp(           sceneName,           data.width,           data.height,           1,           data.duration,           data.frameRate       );}function getItem(name, type) {   var doc = app.project;   for (var i = 1; i <= doc.numItems; i++) {       if (doc.item(i).name === name) {           if (type) {               if (doc.item(i) instanceof type) {                   return doc.item(i);               }           } else {               return doc.item(i);           }       }   }   return null;}

Метод getScene сначала ищет композицию в проекте по имени

getItem(sceneName, CompItem)

И если не находит такой, то создает ее, используя параметры макета

app.project.items.addComp(    sceneName,    data.width,    data.height,    1,    data.duration,    data.frameRate);

Можете проверить работу скрипта. Если вы все сделали правильно, в проекте должна появиться композиция scene-1x1. Сюда мы будем добавлять наши титры. Но об этом в следующий раз.

Полный скрипт к этой статье с комментариями вы можете найти тут

В следующей статье я расскажу, как добавлять титры на сцену.

<- Предыдущая статья ExtendScript Работа с файлами

Подробнее..

Техники повторного использования кода

08.03.2021 12:14:02 | Автор: admin

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

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

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

Для некоторых подходов я добавил схемы, чтобы показать, как организованы составляющие сложных объектов. Будет часто упоминаться агрегация (агрегирование/делегирование/включение) и композиция.

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

Чтобы разделить логику одного сложного объекта на составные части, существуют несколько механизмов:

  • Разделение функционала на классы/объекты и смешивание их полей, методов в одном объекте.

  • Вынесение части функционала в обертки и помещение в них основного объекта, либо вкладывание объектов один в другой с организацией списка вложенных объектов.

  • Вынесение части функционала в отдельные объекты/функции и помещение их в основной объект.

  • Разделение функционала объекта на независимые части и использование какого-то внешнего механизма для организации нужного поведения с использованием этих частей.

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

1) Объединение (смешивание) функционала нескольких объектов в одном
Смешивание и примеси (миксины)
Классическое наследование
Множественное наследование и интерфейсы

2) Композиция/агрегация с использованием списка
Прототипное наследование
Паттерн декоратор и аналоги

3) Композиция/агрегация с использованием одноуровневых структур данных (ссылка, массив ссылок, словарь)
Паттерн стратегия
Entity Component (EC)

4) Композиция/агрегация с вынесением логики вне объекта и его составляющих
Entity Component System (ECS)

5) Композиция/агрегация с использованием графов
Паттерн State machine

6)Композиция/агрегация с использованием деревьев
Паттерн composite и другие древовидные структуры
Behaviour tree

7) Смешанные подходы
React hooks

Объединение (смешивание) функционала нескольких объектов в одном.

Смешивание и примеси (миксины)

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

Существует паттерн примесь (mixin/миксина), в основе которого лежит смешивание.
Примесь это объект, поля и методы которого смешиваются с полями и методами других объектов, расширяя функциональность объекта, но который не используется сам по себе.
Можно добавить несколько миксин к одному объекту/классу. Тогда это схоже с множественным наследованием.

Классическое наследование

Здесь описывается классическое наследование, а не то, как наследование классов устроено в JS.

Подразумеваю, что читатель уже знаком с понятиями "наследование" и "множественное наследование". В отличие от простого смешивания, в классическом наследовании есть ряд строгих правил и механизмов, которые позволяет избежать множество проблем. В основе классического наследования лежит все то же смешивание - члены нескольких объектов объединяются в один объект.

При наследовании происходит копирование членов родительского класса в класс-наследник. При создании экземпляра класса тоже происходит копирования членов класса. Я не исследовал детали этих механизмов, к тому же они явно отличаются в различных языках. Подробнее с этой темой можно ознакомиться в 4-ой главе книги "Вы не знаете JS:thisи Прототипы Объектов".

Когда можно использовать наследование, а когда не стоит?
Наследования не стоит использовать в качестве основной техники для повторного использования кода для сложных объектов. Его можно использовать совместно с композицией для наследования отдельных частей сложного объекта, но не для самого сложного объекта. Например, для React компонентов наследование плохо, а для частей (вроде объектных аналогов custom hooks) из которых мог быть состоять компонент-класс, наследования вполне можно использовать. Но даже так, в первую очередь стоит рассматривать разбиение на большее число составляющих или применения других техник, вместо наследования.

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

Множественное наследование и интерфейсы

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

Интерфейсы есть, например, в Typescript. Реализация нескольких интерфейсов в одном классе отчасти похоже на наследование, но с их использованием наследуется только описание свойств и сигнатура методов интерфейса. Наследование реализации не происходит.

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

Композиция/агрегация с использованием списка

Прототипное наследование

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

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

Стоит отметить, что в JavaScript операции записи/удаления работают непосредственно с объектом. Они не используют прототип (если это обычное свойство, а не сеттер). Если в объекте нет свойства для записи, то создается новое. Подробнее об этом: https://learn.javascript.ru/prototype-inheritance#operatsiya-zapisi-ne-ispolzuet-prototip

Цепочка прототипов организована как стек (Last-In-First-Out или LIFO). Какой объект добавлен в цепочку последним (если считать от итогового объекта-контейнера), к тому обращение будет первым.

Также существует вариант, когда при создании нового объекта с прототипом, создается копия прототипа. В таком случае используется больше памяти (хотя это проблема разрешаема), но зато это позволяет избежать ошибок в других объектах из-за изменения прототипа конкретного объекта.

Паттерн Декоратор и аналоги

Декоратор (wrapper/обертка) позволяет динамически добавлять объекту новую функциональность, помещая его в объект-обертку. Обычно объект оборачивается одним декоратором, но иногда используется несколько декораторов и получается своего рода цепочка декораторов.

Цепочка декораторов устроена как стек(LIFO). Какой объект добавлен в цепочку последним (если считать от итогового объекта-контейнера), к тому обращение будет первым.

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

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

HOF (higher order function) и HOC (Higher-Order Component) - паттерны с похожей идей. Они оборачивают функцию/компонент другой функцией/компонентом для расширения функционала.

HOF - функция, принимающая в качестве аргументов другие функции или возвращающая другую функцию в качестве результата. Примером HOF в JS является функция bind, которая, не меняя переданную функцию, возвращает новую функцию с привязанным к ней с помощью замыкания значением. Другим примером HOF является карринг.

HOC - чистая функция, которая возвращает другой компонент (а он уже содержит в себе произвольную логику), который внутри себя "рендерит" переданный компонент. При этом сам переданный компонент не меняется, но в него могут быть переданы props.

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

const funcA = сompose(funcB, funcC, funcD);

или же менее читабельный вариант:

const funcA = ()=> {  funcB( funcC( funcD() ) ) ;};

То же самое можно получить такой записью:

function funcA() {  function funcB() {      function funcC() {         function funcD()      }    }}  

Недостатком последнего варианта является жесткая структура функций. Нельзя поменять их очередность или заменить одну из функций без создания новой аналогичной цепочки или ее части. funcC нельзя использовать без funcD, а funcB без funcC и без funcD. В первых же двух примерах можно. Там функции независимы друг от друга.

Итого

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

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

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

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

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

Композиция/агрегация с использованием одноуровневых структур данных (ссылка, массив ссылок, словарь)

Под одноуровневой структурой данных я подразумеваю структуру, элементы которой не ссылаются на другие элементы.

Паттерн стратегия

Паттерны декоратор и стратегия служат для одной цели с помощью делегирования расширить функциональность объекта. Но делают они это по разному. Хорошо описана эта разница по ссылке - https://refactoring.guru/ru/design-patterns/strategy:
Стратегияменяет поведение объекта изнутри, аДекораторизменяет его снаружи.

Паттерн Cтратегия описывает разные способы произвести одно и то же действие, позволяя динамически заменять эти способы в основном объекте (контексте).

На схеме ниже пара примеров связи стратегий с основным объектом.

К похожим способам (использование ссылки) расширения функционала объекта и повторного использования кода можно отнести события в HTML элементах и директивы в Angular и Vue.

<button onclick="customAction()" /> // html<input v-focus v-my-directive="someValue" /> // vue

Entity Component (EC)

Я не знаю, как называется данный паттерн. В книге Game Programming Patterns он называется просто "Компонент", а по ссылке http://entity-systems.wikidot.com/ его называют системой компонентов/сущностей. В статье же я буду называть его Entity Component (EС), чтобы не путать с подходом, который будет описан в следующей главе.

Сначала пройдемся по определением:

  • Entity (сущность) объект-контейнер, состоящий из компонентов c данными и логикой. В React и Vue аналогом Entity является компонент. В Entity не пишут пользовательскую логику. Для пользовательской логики используются компоненты. Компоненты могут храниться в динамическом массиве или словаре.

  • Component объект со своими данными и логикой, который можно добавлять в любую Entity. В React компонентах похожим аналогом являются custom hooks. И описываемые здесь компоненты и пользовательские хуки в React служат для одной цели расширять функционал объекта, частью которого они являются.

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

Данный паттерн похож на паттерн стратегия. Если в объекте использовать динамический массив со стратегиями, организовать их добавление, удаление и получение определенной стратегии, то это будет похоже на Entity Component. Есть еще одно серьезное отличие - контейнер не реализует интерфейс компонентов или методы для обращения к методам компонентов. Контейнер только предоставляет доступ к компонентам и хранит их. Получается составной объект, который довольно своеобразно делегирует весь свой функционал вложенным объектом, на которые он ссылается. Тем самым EC избавляет от необходимости использования сложных иерархий объектов.

Плюсы EC

  • Низкий порог вхождения, т.к. в основе используется простая одноуровневая структура данных.

  • легко добавлять новую функциональность и использовать код повторно.

  • можно изменять составной объект (Entity) в процессе выполнения, добавляя или удаляя его составляющие (компоненты)

Минусы

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

В одной из своих следующих статей я опишу применение этого подхода для React компонентов. Тем самым я покажу, как избавиться от первых двух недостатков компонентов на классах, описанных в документации React-а:
https://ru.reactjs.org/docs/hooks-intro.html#its-hard-to-reuse-stateful-logic-between-components
https://ru.reactjs.org/docs/hooks-intro.html#complex-components-become-hard-to-understand

Этот подход используется с самого начала выхода движка Unity3D для расширения функционала элементов (объектов) дерева сцены, включая UI элементы, где вы можете получше ознакомится с данным подходом: https://docs.unity3d.com/ru/2019.4/Manual/UsingComponents.html. Но в таком случае придёться потратить не мало времени на изучение движка.

Итого

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

В случае использования EC может появиться новая проблема при большом количестве компонентов, связанных между собой в одном объекте, становиться сложно разобраться в его работе. Выходом может стать некий компонент, который контролирует взаимодействия между компонентами в одной Entity или в группе вложенных Entities. Такой подход известен как паттерн Посредник (Mediator).

Но даже посредника будет недостаточно для более сложных случаев. К тому же он не является универсальным. Для каждой Entity с множеством связанных компонентов придёться реализовывать новый тип посредника. Есть и другой выход. EC можно комбинировать с другими подходами на основе графов и деревьев, которые будут описаны позже.

Композиция/агрегация с вынесением логики вне объекта и его составляющих

Entity Component System (ECS)

Я не работал с этим подходом, но опишу то, как я его понял.

В ECS объект разбивается на 3 типа составляющих: сущность, компонент (один или несколько), система (общая для произвольного числа объектов). Этот подход похож на EC, но объект разбивается уже на 3 типа составляющих, а компонент содержит только данные.

Определения:

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

  • Component - объект с определенными данными для Entity. Не содержит логики.

  • System - в каждой системе описывается логика. Каждая система перебирает список компонентов определенных типов или компоненты определенных entities и выполняет логику с использованием данных в компонентах. Может извлекать компоненты из entities. Результатом выполнения системы будет обновление данных в компонентах. В некоторых случаях системы могут быть обычными функциями, получающими на вход нужные данные.

Также может быть некий объект-менеджер (или несколько менеджеров), который хранит все системы и объекты, а также периодически запускает все системы. Здесь уже реализация произвольная.

Пример простой ECS: Допустим есть несколько объектов, у которых есть идентификаторы. Несколько из этих объектов ссылаются на компоненты Position, в которых хранятся текущие координаты x, y, и на компонент Speed, который содержит текущую скорость. Есть система Movement, которая перебирает объекты, извлекает из них компоненты Position и Speed, вычисляет новую позицию и сохраняет новые значения x, y в компонент Position.

Как я уже говорил, реализации ECS могут отличаться. Например:

a) entity является контейнером для своих компонентов
http://entity-systems.wikidot.com/artemis-entity-system-framework

b) компоненты содержится в массивах/словарях. Entity является просто идентификатором, по которому определяется компонент, связанный с сущностью.
http://jmonkeyengine.ru/wiki/jme3/contributions/entitysystem/introduction-2
http://entity-systems.wikidot.com/fast-entity-component-system#java
https://www.chris-granger.com/2012/12/11/anatomy-of-a-knockout/

На схеме изображен первый вариант, когда entity ссылается на свои компоненты.

Плюсы ECS

  • Слабое сцепление составляющих объекта, поэтому легко добавлять новую функциональность комбинирую по-разному составляющие.

  • Проще тестировать, т.к. нужно тестировать только системы. Компоненты и сущности тестировать не нужно.

  • Легко выполнять многопоточно.

  • Более эффективное использование памяти, кэша и, следовательно, большая производительность.

  • Легко реализовать сохранение всего приложения, т.к. данные отделены от функционала.

Минусы ECS

  • Высокая сложность, не стандартный подход.

  • для простых проектов является ненужным усложнением.

Так как я занимаюсь фронтенд разработкой, а она по большей части относится к разработки UI, то упомяну, что ECS используется в игре WorldofTanksBlitz для разработки UI:
https://www.youtube.com/watch?v=nu8JJEJtsVE

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

Как и в аналогичном случае с EС, системы в данном подходе можно комбинировать с подходами на основе графов и деревьев. В таком случае логика по-прежнему будет реализована в системах, а хранение данных в компонентах. Но, я не знаю, эффективно ли совмещение этих подходов на практике. В первую очередь нужно стремиться реализовывать системы попроще, а уже потом рассматривать комбинацию с другими подходами.

Композиция/агрегация с использованием графов

К данному способу повторного использования кода я отнес паттерн машина состояний (State machine/Finite state machine/конечный автомат).

Аналогом машины состояний простой является switch:

switсh (condition) { case stateA: actionA(); case stateB: actionB(); case stateC: actionC();}

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

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

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

Я уже описывал паттерн Машина состояний и его составляющие, и вкратце писал о иерархической машине состояний в статье "Приемы при проектировании архитектуры игр"в главе "машина состояний".

Преимущества использования машины состояний:
Хорошо описано по ссылке: https://refactoring.guru/ru/design-patterns/state
Добавлю, что становится легче предусмотреть, обработать и протестировать все возможные случаи работы контекста (подсистемы), т.к. видны все его состояния и переходы. Особенно, если состояния являются просто объектами с данными и отделены от остальной логики и отображения.

Где при разработке UI можно использовать машину состояний?
Например, для логики сложной формы, у которой поведение и набор полей немного отличается в зависимости от роли пользователя и других параметров. Каждый объект-состояние может описывать состояние всех компонентов формы (активный, видимый, фиксированный текст элемента в таком-то состоянии и т.д.), отображение которых может отличаться в зависимости от роли пользователя и других параметров. Компоненты формы получают часть объекта-состояния, которая им нужна для своего отображения.

Другие примеры использования в UI:
https://24ways.org/2018/state-machines-in-user-interfaces/
https://xstate.js.org/docs/ (библиотека для JS, которую можно использовать c React, Vue, Svelte)
https://github.com/MicheleBertoli/react-automata (библиотека для React)
http://personeltest.ru/aways/habr.com/ru/company/ruvds/blog/346908/

Подходит ли State machine в качестве основного механизма повторного использования кода и разбиения сложных объектов на составные части?
Иногда он так и используется. Но, он мне кажется сложноватым и не всегда подходящим для использования в качестве основного. Зато он точно хорош в качестве дополнительного, когда нужно организовать взаимодействия между несколькими объектами или частями составного объекта.

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

Композиция/агрегация с использованием деревьев.

Паттерн composite и другие древовидные структуры

Деревья часто встречается в разработке. Например, объекты в JavaScript могут содержать вложенные объекты, а те также могут содержать другие вложенные объекты, тем самым образую дерево. XML, JSON, HTML, DOM-дерево, паттерн Комповщик (Composite) все это примеры древовидной композиции.

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

Behaviour tree

Интересным вариантом композиции является Behaviour tree (дерево поведения). Это организация логики программы (обычно AI) или ее частей в виде дерева.

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

Я уже описывал деревья поведений в прошлом в этой статье.

Более наглядный пример схемы готового дерева из плагина banana-tree

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

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

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

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

Смешанные подходы

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

Довольно многое можно отнести к смешанных подходам. Entity Component в Unity3D реализован так, что позволяет хранить не только компоненты, но и вложенные сущности. А для пользовательских компонентов можно использовать наследование в простых случаях, либо объединить компоненты с более продвинутыми техниками (паттерн mediator, машина состояний, дерево поведения и другие).

Примером смешивания подходов является анимационная система Mecanim в Unity3D, которая использует иерархическую машину состояний с деревьями смешивания (blend tree) для анимаций. Это относится не совсем к коду, но является хорошим примером комбинации подходов.

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

React hooks

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

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

Как я понял, хуки при вызове добавляют к текущему обрабатываемому компоненту (точнее к fiber-ноде) свое состояние объект, в котором могут быть указаны переданные сallback-и (в случае useEffect, useCallback), массив зависимостей, значения (в случае useState) и прочие данные (в случае useMemo, useRef, ).

А вызываются хуки при обходе дерева компонентов, т.е. когда вызывается функция-компонент. React-у известно, какой компонент он обходит в данный момент, поэтому при вызове функции-хука в компоненте, состояние хука добавляется (или обновляется при повторных вызовах) в очередь состояний хуков fiber-ноды. Fiber-нода это внутреннее представление компонента.

Стоит отметить, что дерево fiber элементов не совсем соответствует структуре дерева компонентов. У Fiber-ноды только одна дочерняя нода, на которую указывает ссылка child. Вместо ссылки на вторую ноду, первая нода ссылается на вторую (соседнюю) с помощью ссылки sibling. К тому же, все дочерние ноды ссылаются на родительскую ноду с помощью ссылки return.

Также для оптимизации вызова эффектов (обновление DOM, другие сайд-эффекты) в fiber-нодах используются 2 ссылки (firstEffect, nextEffect), указывающие на первую fiber-ноду с эффектом и следующую ноду, у которой есть эффект. Таким образом, получается список нод с эффектами. Ноды без эффектов в нем отсутствуют. Подробнее об этом можно почитать по ссылкам в конце главы.

Вернемся к хукам. Структура сложного функционального компонента с несколькими вложенными custom hooks для разработчика выглядит как дерево функций. Но React хранит в памяти хуки компонента не как дерево, а как очередь. На схеме ниже изображен компонент с вложенными хукам, а под ним fiber-нода с очередью состояний этих же хуков.

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

Чтобы просмотреть содержимое fiber-ноды, достаточно воспользоваться console.log и вставить туда JSX код, который возвращает компонент:

function MyComponent() {  const jsxContent = (<div/>);  console.log(jsxContent);  return jsxContent;}

Корневую fiber-ноду можно просмотреть следующим образом:

const rootElement = document.getElementById('root');ReactDOM.render(<App />, rootElement);console.log(rootElement._reactRootContainer._internalRoot);

Также есть интересная наработка: react-fiber-traverse

Под спойлером приведен код компонента с хуками и отображение его fiber-ноды
import { useState, useContext, useEffect,useMemo, useCallback,         useRef, createContext } from 'react';import ReactDOM from 'react-dom';const ContextExample = createContext('');function ChildComponent() {  useState('childComponentValue');  return <div />;}function useMyHook() {  return useState('valueB');}function ParentComponent() {  const [valueA, setValueA] = useState('valueA');  useEffect(function myEffect() {}, [valueA]);  useMemo(() => 'memoized ' + valueA, [valueA]);  useCallback(function myCallback() {}, [valueA]);  useRef('refValue');  useContext(ContextExample);  useMyHook();  const jsxContent = (    <div>      <ChildComponent />      <button onClick={() => setValueA('valueA new')}>Update valueA</button>    </div>  );  console.log('component under the hood: ', jsxContent);  return jsxContent;}const rootElement = document.getElementById('root');ReactDOM.render(  <ContextExample.Provider value={'contextValue'}>    <ParentComponent />  </ContextExample.Provider>,  rootElement,);

С более подробным описанием работы внутренних механизмов React на русском языке можно ознакомиться по ссылкам:
Как Fiber в React использует связанный список для обхода дерева компонентов
Fiber изнутри: подробный обзор нового алгоритма согласования в React
Как происходит обновление свойств и состояния в React подробное объяснение
За кулисами системы React hooks
Видео: Под капотом React hooks

У подхода с хуками на данный момент есть недостаток - фиксированное дерево функций в компонентах. При стандартном использовании хуков, нельзя изменить логику уже написанного компонента или хуков, состоящих из других хуков. К тому же это мешает тестированию хуков по отдельности. В какой-то степени можно улучшить ситуацию композицией (compose) хуков. Например, существует такое решение: https://github.com/helloitsjoe/react-hooks-compose

Линейность кода и составляющих сложного объекта

Известно, что множество вложенные условий, callback-ов затрудняют читаемость кода: https://refactoring.guru/ru/replace-nested-conditional-with-guard-clauses
http://personeltest.ru/aways/habr.com/ru/company/oleg-bunin/blog/433326/ (в статье упоминается линейный код)
https://www.azoft.ru/blog/clean-code/ (в статье упоминается линейность кода)

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

Подробнее..

Шпаргалка по функциональному программированию

19.03.2021 10:13:30 | Автор: admin

Привет, меня зовут Григорий Бизюкин, я преподаватель Школы разработки интерфейсов и фронтенд-разработчик в Яндексе. Давайте поговорим о функциональном программировании в мире JavaScript. Мы все про ФП что-то слышали, нам всем оно интересно, но у меня, когда я искал полезные материалы для подготовки к лекциям, сложилось такое впечатление: есть куча статей, каждая из которых либо говорит об ФП общими словами, либо раскрывает отдельный маленький кусочек темы, чего, конечно, недостаточно.



Добавим функционального света


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


Оглавление

Функциональное программирование


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


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


Функциональное программирование штука интересная, но вряд ли вы захотите переписать весь проект на функциональном языке. На практике именно отдельные подходы оказываются самыми полезными и применимыми. На них и сконцентрируемся. В контексте ФП часто можно встретить термины вроде линз и монад. Здесь они останутся за скобками, потому что уж слишком специфичны.


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


За и против


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


В целом считается, что ФП делает код понятнее, потому что является более декларативным. Остальные рассуждения оставим за скобками, так как на Хабре уже достаточно статей, где рассмотрены разные аргументы как за ФП, так и против. При желании можно обратиться к ним, чтобы решить для себя, когда вы хотите использовать ФП, а когда нет. Здесь мы сосредоточимся на объяснении терминов и подходов.


Императивный vs декларативный


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


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


В разработке та же история. Когда мы пишем декларативно, код выглядит гораздо проще:


const array = [4, 8, 15, null, 23, undefined]// императивный подходconst imperative = []for (let i = 0, len = array.length; i < len; ++i) {    if (array[i]) {        imperative.push(array[i])    }}// декларативный подходconst declarative = array.filter(Boolean)

Для фильтрации массива чисел больше не нужно думать о деталях: о том, как инкрементировать переменную i и как не выйти за границы массива. Нам достаточно передать предикат Boolean в функцию filter, и дело сделано.


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


/* css */.button {    color: azure;}

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


Такая же история и с SQL:


-- SQLSELECT titleFROM filmsWHERE rating > 9GROUP BY director

Запрос говорит о результате, а не о том, как именно его выполнить.


Функции и процедуры


Функция понятие, близкое к математическому. Она что-то получает на вход и всегда что-то возвращает.


const f = (x) => x * Math.sin(1 / x)

Процедура, в свою очередь, вызывается ради побочных эффектов:


const print = (...args) => {    const style = 'color: orange;'    console.log('%c' + args.join('\n'), style)}

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


В JS не существует процедур, потому что то, что мы считаем процедурой, на самом деле является функцией без return. Если опустить return, функция всё равно неявно возвращает undefined и остаётся функцией.


Но в функциональном программировании мы стремимся как можно больше использовать функции, которые явно что-то возвращают.


Параметры и аргументы


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


// x  параметр (почти любое число)const f = (x) => x * Math.sin(1 / x)// 0.17  аргумент (конкретное число)f(0.17)

Сигнатура


Количество, тип и порядок параметров. Объявление функции в JS не содержит информации о типе параметров из-за динамической типизации. Если не используется TypeScript, эту информацию можно указать через JSDoc.


/** * @param {*} value * @param {Function|Array<string>|null} [replacer] * @param {number|string|null} [space] * @returns {string} */function toJSON (value, replacer, space) {    return JSON.stringify(value, replacer, space)}

Арность


Арность количество параметров, которые принимает функция. В JavaScript арность функции можно определить при помощи свойства length.


const awesome = (good, better, theBest) => {}awesome.length // 3

У свойства length есть особенности, которые следует учитывать:


// аргументы по умолчаниюconst defaultParams = (answer = 42) => {}defaultParams.length // 0// остаточные параметрыconst restParams = (...args) => {}restParams.length // 0// деструктуризацияconst destructuring = ({target}) => {}destructuring.length // 1

Рекурсия


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


function factorial (n) {    if (n <= 1) {        return 1    }    return n * factorial(n - 1)}

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


function factorial (n, total = 1) {    if (n <= 1) {        return total    }    return factorial(n - 1, n * total)}

Несмотря на заманчивые возможности, поддержка хвостовой рекурсии до сих пор отсутствует и вряд ли появится в будущем, поэтому сведения о ней носят чисто теоретический характер. Если добавить в самое начало функции console.trace, можно убедиться, что каждый новый вызов создаёт новый кадр в стеке, несмотря на то, что условия рекурсии выполняются. Более подробно об оптимизации хвостовых вызовов можно почитать здесь.


Функция первого класса


Функции, которые мы можем использовать как обычные объекты, называются функциями первого класса. Их можно присваивать, передавать в другие функции и возвращать.


// присваиватьconst assign = () => {}// передаватьconst passFn = (fn) => fn()// возвращатьconst returnFn = () => () => {}

Функция высшего порядка


Функции, которые принимают или возвращают другие функции. С ними мы работаем каждый день.


// map, filter, reduce и т.д.[0, NaN, Infinity].filter(Boolean)// обещанияnew Promise((res) => setTimeout(res, 300))// обработчики событийdocument.addEventListener('keydown', ({code, key}) => {    console.log(code, key)})

При этом высшим порядком могут быть не только функции, но и, например, компоненты в React, принимающие или возвращающие другие компоненты. Они, соответственно, называются компонентами высшего порядка.


Предикат


Это функция, которая возвращает логическое значение. Самый распространённый пример использование предиката внутри функций filter, some, every.


const array = [4, 8, 15, 16, 23, 42]// isEven  это предикатconst isEven = (x) => x % 2 === 0const even = array.filter(isEven)

Замыкание


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


const createCounter = tag => count => ({    inc () { ++count },    dec () { --count },    val () {        console.log(`${tag}: ${count}`)    }})const pomoCounter = createCounter('pomo')const work = pomoCounter(0)work.inc()work.val() // pomo: 1const rest = pomoCounter(4)rest.dec()rest.val() // pomo: 3

В примере внутри замыкания хранятся две переменные: tag и count. Каждый раз, когда мы создаём новую переменную внутри другой функции и возвращаем её наружу, функция находит переменную, объявленную во внешней функции, через замыкание. Если тема замыканий кажется чем-то загадочным, почитайте о них подробнее в блоге HTML Academy.


Мемоизация


Полезный приём функция кеширует результаты своего вызова:


const memo = (fn, cache = new Map) => param => {    if (!cache.has(param)) {        cache.set(param, fn(param))    }    return cache.get(param)}const f = memo((x) => x * Math.sin(1 / x))f(0.314) // вычислитьf(0.314) // взять из кеша

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


Конвейер и композиция


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


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


Конвейер


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


# вывести идентификаторы процессов с подстрокой kernelps aux | grep 'kernel' | awk '{ print $2 }'

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


const double = (n) => n * 2const increment = (n) => n + 1// без конвейерного оператораdouble(increment(double(double(5)))) // 42// с конвейерным оператором5 |> double |> double |> increment |> double // 42

Если бы у нас была функция pipe, которая аналогичным образом организовывает поток данных, через неё можно было бы записать так:


pipe(double, double, increment, double)(5) // 42

Аргумент, переданный в конвейер, последовательно проходит слева направо:


// 5 -> 10 -> 20 -> 21 -> 42

Хм, а что если запустить конвейер в другую сторону?


Композиция


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


// композиция функций в чистом видеdouble(increment(double(double(5)))) // 42

Если записать то же самое через вспомогательную функцию compose, получится:


compose(double, increment, double, double)(5)

Внешне всё осталось почти так же, но место вызова функции increment изменилось, потому что теперь цепочка вычислений стала работать справа налево:


// 42 <- 21 <- 20 <- 10 <- 5

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


// оригинальная цепочка вызововone(two(three(x)))// более естественно с точки зрения чтенияpipe(three, two, one)(x)// более естественно с точки зрения записиcompose(one, two, three)(x)

Таким образом, конвейер и композиция это два направления одного потока данных.


Преимущества


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


Создание новых абстракций


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


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


Например, у нас есть два готовых кубика: получить слова из текста, оставить только уникальные слова:


// готовые кубикиconst words = str => str    .toLowerCase().match(/[а-яё]+/g)const unique = iter => [...new Set(iter)]const text = `Съешь ещё этих мягкихфранцузских булок, да выпей же чаю`const foundWords = words(text)const uniqueWords = unique(wordsFound)

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


function getUniqueWords (text) {    return unique(words(text))}const uniqueWords = getUniqueWords(text)

Сила композиции в том, что с её помощью можно создавать новые абстракции гораздо проще и удобнее:


// создаём новую деталь через композициюconst getUniqueWords = compose(unique, words)const uniqueWords = getUniqueWords(text)

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


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


const sort = iter => [...iter].sort()// новая деталь, которая пригодится для новых построекconst getSortedUniqueWords = compose(sort, getUniqueWords)const sortedUniqueWords = getSortedUniqueWords(text)

Если речь идёт о конструировании сложных деталей, вложенную композицию можно заменить на линейную:


// вложенная композицияcompose(sort, compose(unique, words))// линейная композицияcompose(sort, unique, words)

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


Бесточечный стиль


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


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


// стиль с параметрамиfunction getUniqueWords (text) {    return unique(words(text))}// стиль без параметров (бесточечный стиль)const getUniqueWords = compose(unique, words)

При работе со стилем без параметров функция не упоминает данные, которые мы обрабатываем.


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


Ограничения


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


const translate => (lang, text) => magicSyncApi(lang, text)const getTranslatedWords = compose(translate, unique, words)getTranslatedWords(text) // упс... что-то сломалось

Здесь на помощь приходит частичное примирение и каррирование, о которых мы поговорим позже.


Пишем сами


Реализовать конвейер можно было бы так:


const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x)

Чтобы реализовать композицию, достаточно заменить reduce на reduceRight:


const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x)

Как на практике?


На практике не так много случаев, где можно применить композицию. Кроме того, применимость ограничена отсутствием в JS встроенных механизмов нужно использовать библиотеки или самостоятельно реализовывать у себя необходимые функции. Но при этом, как ни странно, если вы используете на проекте Redux, ничего подключать не придётся, потому что композиция входит в состав библиотеки.


На проекте с Redux композиция наверняка будет использоваться для middleware, потому что createStore принимает только один усилитель (enhancer), а их, как правило, требуется хотя бы несколько.


// композиция в reduxconst store = createStore(    reducer,    compose(        applyMiddleware(...middleware),        DevTools.instrument(),    ))

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


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


const notifications = [    { text: 'Warning!', lang: 'en', closed: true },    { text: 'Внимание!', lang: 'ru', closed: false },    { text: 'Attention!', lang: 'en', closed: false }]// goodnotifications.filter((notification) => {    // ...проверить все условия})// betternotifications    .filter(isOpen)    .filter(isLang)// the bestcompose(    isLang,    isOpen)(notifications)

Частичное применение и каррирование


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


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


const sum = (x, y, z) =>    console.log(x + y + z)

Частичное применение


Преобразует функцию в одну функцию с меньшим числом параметров.


const partialSum = partial(sum, 8)partialSum(13, 21) // 42

Каррирование


Преобразует функцию в набор функций с единственным параметром.


const curriedSum = curry(sum)curriedSum(8)(13)(21) // 42

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


curriedSum(8, 13)(21) // 42curriedSum(8, 13, 21) // 42

В чём разница?


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


const partialSum = partial(sum, 42)partialSum() // NaN, потому что 42 + undefined + undefined

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


const curriedSum = curry(sum)curriedSum(8) // новая функция  sum(8)curriedSum(8)(13) // ещё одна новая функция  sum(8, 13)curriedSum(8)(13)(21) // 42, потому что набралось нужное число аргументов

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


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


Решение задачи с композицией


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


const translate => (lang, text) => magicSyncApi(lang, text)// через частичное применениеconst english = partial(translate, 'en')// через каррированиеconst english = curry(translate)('en')// создать новую деталь с возможностью переводаconst getTranslatedWords = compose(english, unique, words)getTranslatedWords(text) // теперь всё работает

Порядок данных


Частичное применение и каррирование чувствительны к порядку данных. Существует два подхода к порядку объявления параметров.


// сперва итерация, затем данные (iterate-first data-last)const translate => (lang, text) => /* */// сперва данные, затем итерация (data-first, iterate-last)const translate => (text, lang) => /* */

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


function flip (fn) {    return (...args) => fn(...args.reverse())}const curryRight = compose(curry, flip)const partialRight = compose(partial, flip)

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


Специализация


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


const fetchApi = (baseUrl, path) =>    fetch(`${baseUrl}${path}`)        .then(res => res.json())

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


// каррированиеconst fetchCurry = curry(fetchApi)const fetchUnsplash = fetchCurry('https://api.unsplash.com')const fetchRandomPhoto = fetchUnsplash(fetchApi, '/photos/random')// частичное применениеconst fetchUnsplash = partial(fetchApi, 'https://api.unsplash.com')const fetchRandomPhoto = partial(fetchUnsplash, '/photos/random')

Пишем сами


Свою версию частичного применения можно написать примерно так:


function partial (fn, ...apply) {    return (...args) => fn(...apply, ...args)}

Каррирование выглядит немного сложнее:


function curry (fn) {    return (...args) => args.length >= fn.length ?        fn(...args) : curry(fn.bind(null, ...args))}

Как на практике?


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


А ещё в JavaScript у функций есть метод .bind, который реализует частичное применение из коробки, поэтому, если порядок параметров позволяет, то вуаля:


const fetchApi = (baseUrl, endpoint) =>    fetch(`${baseUrl}${endpoint}`)        .then(res => res.json())const fetchUnsplash = fetchApi.bind(null, 'https://api.unsplash.com')const fetchRandomPhoto = fetchUnsplash.bind(null, '/photos/random')

Неизменяемые данные


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


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


// mutable glassconst takeGlass = (volume) => ({    look () { console.log(volume) },    drink (amount) {        volume = Math.max(volume - amount, 0)        return this    }})const mutable = takeGlass(100)mutable.drink(20).drink(30).look() // 50mutable.look() // 50

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


// immutable glassconst takeGlass = (volume) => ({    look () { console.log(volume) },    drink (amount) {        return takeGlass(Math.max(volume - amount, 0))    }})const immutable = takeGlass(100)immutable.drink(20).drink(30).look() // 50immutable.look() // 100

Преимущества неизменяемых структур данных:
предсказуемое изменение состояния,
быстрое сравнение по ссылке,
кешируемость,
легко распараллеливать,
легко тестировать.


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


Нечаянное мутирование данных


В JavaScript запросто можно нечаянно мутировать массив или любой другой объект:


function sortArray (array) {    return array.sort()}const fruits = ['orange', 'pineapple', 'apple']const sorted = sortArray(fruits)// упс... исходный массив тоже изменилсяconsole.log(fruits) // ['apple', 'orange', 'pineapple']console.log(sorted) // ['apple', 'orange', 'pineapple']

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


const object = {}// const означает константную ссылкуobject = {} // TypeError: Assignment to constant variable// но сам объект можно беспрепятственно изменятьobject.value = 42 // мутация объекта

Все ссылочные типы: объекты, массивы и другие всегда передаются по ссылке. Во время присваивания или передачи параметра происходит копирование ссылки, но не самих данных.


const array = []// копия ссылкиconst ref = arrayref.push('apple')// ещё одна копия ссылкиconst append = (ref) => {   ref.push('orange')}append(array)// массив дважды мутирован через ссылкуconsole.log(array) // [ 'apple', 'orange' ]

А что если применить средства метапрограммирования и, например, заморозить объект? В этом случае мы всё равно сможем изменить вложенные объекты по ссылке.


const object = { val: 42, ref: {} }const frozen = Object.freeze(object)// игнорирование ошибки без 'use strict'// или же TypeError: Cannot assign to read only property...frozen.val = 23// мутирование вложенных данных по ссылкеfrozen.ref.boom = 'woops'console.log(frozen) // { val: 42, ref: { boom: 'woops' }

Вместо заморозки можно воспользоваться Proxy API, но в этом случае тоже придётся дополнительно обрабатывать вложенные структуры, потому что они всё ещё открыты для изменений.


const object = { val: 42, ref: {} }const proxy = new Proxy(object, {    set () { return true },    deleteProperty () { return true }})// изменение или удаление свойства не сработаетproxy.val = 19delete proxy.val// точно так же, как и добавление новогоproxy.newProp = 23// но вложенные объекты всё ещё мутабельныproxy.object.boom = 'woops'console.log(proxy) // { value: 42, ref: { boom: 'woops' } }

В общем, в JavaScript нельзя просто так взять и защитить данные от непреднамеренного изменения.


Затраты на копирование


С копированием данных тоже не всё так просто. В большинстве случаев работает копирование массивов и объектов встроенными средствами JavaScript:


const array = [4, 8, 15, 16, 23]const object = { val: 42 }// создать новый объект или массив[].concat(array)Object.assign({}, oject)// но через деструктуризацию удобнее[...array]{...object}

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


const object = { val: 42, ref: {} }const copy = { ...object }copy.val = 23copy.ref.boom = 'woops'console.log(object) // { val: 42, ref: { boom: 'woops' }

Такая же история с функциональными методами массивов map и filter создают поверхностную копию исходного массива.


const array = [null, 42, {}]const copy = array.filter(Boolean)copy[0] = 23copy[1].boom = 'woops'console.log(array) // [ null, 42, { boom: 'woops' } ]console.log(copy) // [ 23, { boom: 'woops' }

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


The problems of shared mutable state and how to avoid them
What is the most efficient way to deep clone an object in JavaScript?


Неизменяемые структуры данных (persistent data structures)


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


Пожалуй, две самые популярные библиотеки в мире фронтенд разработки это Immutable и Immer. При помощи Immer мы можем сделать вот что:


import produce from 'immer';const object = { ref: { data: {} } };const immutable = produce(object, (draft) => {  draft.ref.boom = 'woops';});console.log(object) // { ref: { data: {} }console.log(immutable) // { ref: { data: {}, boom: 'woops' }console.log(object.ref.data === immutable.ref.data) // true

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


Как на практике?


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


const addTodo = (state = initState, action) => {    switch (action.type) {        case ADD_TODO: {            return {                ...state,                todos: [...state.todos, action.todo]            }        }        default: {            return state;        }    }}

Но, как мне кажется, во многих других ситуациях неизменяемые структуры данных подойдут куда лучше:


import produce from 'immer'const addTodo = (state = initState, action) =>    produce(state, draft => {        switch (action.type) {            case ADD_TODO: {                draft.todos.push(action.todo)                break            }        }    })

Чистые функции (pure functions)


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


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


Побочные эффекты (side effects)


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


function impure () {    // логирование    console.log('side effects')    // запись в файл    fs.writeFileSync('log.txt', `${new Date}\n`, 'utf8')    // запрос на сервер и т. д.    fetch('/analytics/pixel')}

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


Работа с глобальными переменными тоже побочный эффект.


function impure () {    // глобальная переменная    app.state.hasError = true}

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


function impure () {    // модификация DOM    document.getElementById('menu').hidden = true    // установка обработчика    window.addEventListener('scroll', () => {})    // запись в локальное хранилище    localStorage.setItem('status', 'ok')}

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


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


function impure (o) {    return Object.defineProperty(o, 'mark', {        value: true,        enumerable: true,    })}const object = {}const marked = impure(object)// defineProperty мутировала исходный объектconsole.log(object) // { mark: true }

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


Зависимость от параметров


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


function impure () {    // глобальная переменная    if (NODE_ENV === 'development') { /* */ }    // чтение данных из DOM    const { value } = document.querySelector('.email')    // обращение к локальному хранилищу    const id = localStorage.getItem('sessionId')    // чтение из файла и т. д.    const text = fs.readFileSync('file.txt', 'utf8')}

Внешние зависимости можно заменить на зависимость от параметров.


Непредсказуемый результат


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


function impure (min, max) {    return Math.floor(Math.random() * (max - min + 1) + min)}impure(1, 10) // 4impure(1, 10) // 2

Чтобы сделать функцию чистой, достаточно вынести неопределённость за пределы функции и передать её в качестве параметра. Например, вот так:


function pure (min, max, random = Math.random()) {    return Math.floor(random * (max - min + 1) + min)}pure(1, 10, 0.42) // 5pure(1, 10, 0.42) // 5

Теперь функция всегда возвращает один и тот же результат для одних и тех же параметров.


Преимущества чистых функций


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


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


const refTransparency = () =>    Math.pow(2, 5) + Math.sqrt(100)// вызов функцииrefTransparency()// можно раскрытьMath.pow(2, 5) + Math.sqrt(100)// и без особых трудностей понять результат32 + 10 // 42

Так почему бы всё не написать на чистых функциях?


Абсолютная и относительная чистота


Если взять и написать программу только из чистых функций, то получится:


(() => {})() // абсолютная чистота

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


// побочные эффекты выносятся за пределыconst text = fs.readFileSync('file.txt', 'utf8')// функция получает нужные данные только через параметрыfunction pure (text) {    // ... чистота}

Кроме того, чистота относительна. Функция ниже чистая или нет?


// pure или impure?function circleArea (radius) {    return Math.PI * (radius ** 2)}

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


Заключение


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


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


Жаргон функционального программирования
Functional-Light JavaScript
Mostly adequate guide to Functional Programming


Кроме того, загляните в репозиторий Awesome FP JS, вдруг найдёте что-то интересное для себя. Если же захочется целиком погрузиться в функциональную парадигму, но при этом продолжать разрабатывать фронтенд, можно посмотреть в сторону Ramda или Elm.


Спасибо за внимание!

Подробнее..

Перевод Финальные классы в PHP, Java и других языках

23.11.2020 12:04:01 | Автор: admin
Использовать финальные классы или не использовать финальные классы? Вот в чём вопрос. А еще в том, когда и как это делать правильно.



Почему стоит использовать финальные классы


Максимальное уменьшение области видимости


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

Поощрение подхода композиция вместо наследования


Принцип открытости/закрытости гласит: класс должен быть открыт для расширения, но закрыт для изменений.

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

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

Почему этот класс не финальный?


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

Заблуждения


Когда мы в первый раз изучали ООП, мы приводили классический пример наследования. Тем не менее, когда Алан Кей создал Smalltalk, наследование не было его главной концепцией. Основной концепцией был обмен сообщениями, то есть вы можете отправлять сообщения объектам, а они инкапсулируют в себе данные и логику. Вы можете изменять их поведение, используя различные объекты, что на самом деле является композицией. Но в итоге концепция наследования настолько популярна, что в конечном счёте затмевает композицию.

Выгоды от использования финальных классов


  • Понятные контракты. Использование интерфейсов заставит вас мыслить в терминах связей между объектами.
  • Изолированные блоки кода без побочных эффектов. Внедрение интерфейсов в качестве зависимостей устранит все неприятные побочные эффекты кода, над которым вы работаете.
  • Тестируемость. Подмена на фиктивные зависимости чрезвычайно проста, если они являются интерфейсами.
  • Низкая и управляемая сложность. Поскольку всё изолировано, вам не нужно беспокоиться о волнообразных изменениях. Это значительно снижает сложность вашего кода.
  • Низкая когнитивная нагрузка. С уменьшением сложности ваш мозг сможет сосредоточиться на самом важном.
  • Гибкость кода. Удаляя любую ненужную связь, вы сможете рефакторить намного легче, чем раньше.
  • Уверенность в себе. Возможность тестировать свой код изолированно не оставляет сомнений в правильности его изменения.

Композиция вместо наследования


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

Что вам следует начать делать [вместо того что вы делаете сейчас]


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

Итого: Интерфейсы Финальные классы Композиция
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru