Сочиняя ПО: Введение +9


Эта статья - часть серии статей "Сочиняя ПО"  про функциональное программирование и различные техники создания программ на JavaScript ES6+, начиная с азов. Оставайтесь на связи, много нового впереди!

Композиция: "Действие, заключающееся в составлении единого целого из частей или элементов."

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

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

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

  • Что такое функциональная композиция (композиция функций)?

  • Что такое объектная композиция (композиция объектов)?

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

Весь мир сегодня зависит от программ. Каждый новый автомобиль - это мини суперкомпьютер на колесах, и ошибки в программном обеспечении могут привести к реальным авариям и стоить жизни реальным людям. В 2013 юристы признали команду разработки компании Toyota виновной в "безрассудном пренебрежении" после того, как расследование аварии выявило т.н. "спагетти-код" с примерно 10000 глобальных переменных.

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

Мы обязаны делать свою работу лучше.

Вы используете композицию каждый день

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

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

Композиция функций

Функциональная композиция это процесс применения функции к результату вызова другой функции. В алгебре это выглядит так: пусть даны две функции f и g, тогда их композиция это (f ? g)(x) = f(g(x)). Символ ? - это оператор композиции. Вы всегда можете сказать, что "композиция функций f и g равна f от g от x". Можно также сказать, что f это внешняя функция, а g внутренняя, потому что f применяется к результату функции g.

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

const g = n => n + 1;
const f = n => n * 2;
const doStuff = x => {
  const afterG = g(x);
  const afterF = f(afterG);
  return afterF;
};
doStuff(20); // 42

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

const g = n => n + 1;
const f = n => n * 2;
Promise.resolve(20)
  .then(g)
  .then(f)
  .then(value => console.log(value)); // 42

Более того, каждый раз, когда вы комбинируете в цепочку вызовы методов обработки массивов (mapfilter, etc), методов lodash, или observables из RxJS, вы используете композицию функций. Создаете цепочки вызовов - значит используете композицию. Если результат вызова функции передается в другую функцию - это композиция. Если вы создаете цепочку вызовов из двух методов - вы комбинируете их используя this в качестве входящих данных.

Когда вы используете композицию намеренно - вы делаете это лучше. Используя композицию функций намеренно мы можем улучшить наш doStuff() и превратить его в простую функцию из одной строки:

const g = n => n + 1;
const f = n => n * 2;
const doStuffBetter = x => f(g(x));
doStuffBetter(20); // 42

Для начала, давайте создадим абстракцию для логгирования "after g" и сделаем небольшую функцию trace():

const trace = label => value => {
  console.log(`${ label }: ${ value }`);
  return value;
};

Теперь мы можем переписать наш код:

const doStuff = x => {
  const afterG = g(x);
  trace('after g')(afterG);
  const afterF = f(afterG);
  trace('after f')(afterF);
  return afterF;
};
doStuff(20); // =>
/*
"after g: 21"
"after f: 42"
*/

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

import pipe from 'lodash/fp/flow';
const doStuffBetter = pipe(
  g,
  trace('after g'),
  f,
  trace('after f')
);
doStuffBetter(20); // =>
/*
"after g: 21"
"after f: 42"
*/

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

// pipe(...fns: [...Function]) => x => y
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

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

pipe() создает цепочку вызовов, пайплайн если хотите, передавая вывод одной функции на вход другой. Когда вы используете pipe() (и ее близнеца compose()) вам не нужны промежуточные переменные для хранения результата вызова. Создание функций без описания (идентификации) их аргументов называется молчаливым программированием. Чтобы провернуть такой фокус, вы должны вызвать функцию, которая возвращает новую функцию, вместо того, чтобы описывать функцию явно. Это означает, что вам не нужно использовать function или => для создания функции (как в примере выше сделано для функции doStuffBetter).

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

Уменьшение сложности дает некоторые преимущества:

  • Использование памяти. Мозг человека может держать в памяти одновременно ограниченное количество элементов, и каждая переменная потенциально занимает один такой элемент. Чем больше переменных в коде, тем сложнее вспомнить, для чего именно используется та или иная переменная. Обычно мы можем держать в уме от 4 до 7 различных элементов одновременно. Если количество элементов превышает число 7, то количество ошибок значительно возрастает. Используя пайплайн, мы избавились от 3-х переменных, а значит освободили как минимум половину места в нашей голове для других вещей. Это значительно уменьшает умственную нагрузку. Конечно, программисты могут держать в уме значительно больше, чем обычные люди, но, все же, не настолько больше, чтобы игнорировать важность этого момента.

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

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

Композиция объектов

"Используйте композицию объектов вместо наследования классов" - Банда Четырех, "Приёмы объектно-ориентированного проектирования. Паттерны проектирования"

"В информатике составной тип данных - это любой тип данных, который может быть сконструирован с использованием примитивных типов данных языка программирования и других составных типов. […] Процесс создания составного типа данных называется композицией." - Wikipedia

Это представители примитивных типов:

const firstName = 'Claude';
const lastName = 'Debussy';

А вот это составной тип:

const fullName = {
  firstName,
  lastName
};

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

Следует отметить, что "Банда Четырех" описала шаблон проектирования, называемый "Компоновщик" (Composite), который является специфическим типом рекурсивной композиции объектов, позволяющей использовать отдельные компоненты и их объединения одинаковым образом. Некоторые разработчики попадают в ловушку и считают, что шаблон "Компоновщик" это единственный путь использования композиции объектов. Это не так, существует множество различных типов и способов композиции объектов.

"Банда Четырех" продолжает: "Вы увидите что композиция объектов применяется снова и снова в шаблонах проектирования", а затем сводит все типы композиций к трем видам отношений, включающих делегирование (используется, например, в шаблонах "Состояние", "Стратегия", "Посетитель"), ассоциацию (когда объект знает про другой объект по ссылке, обычно передаваемой как параметр, это вид отношения "использует", как, например, объект-обработчик использует логгер для записи в лог запроса) и агрегацию (когда дочерние объекты формируют родительский объект, каждый свою часть, это вид отношения "имеет", как, например, элемент DOM-дерева имеет потомков).

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

Мы будем использовать более общее определение композиции объектов, данное, например, в работе "Методы теории категорий в программировании: аспекты топологии" (1989):

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

Другой хороший источник это работа Гленфорда Майерса "К надежному ПО через композицию" (1975). Обе книги давно уже разошлись, но их все еще можно найти на Amazon или Ebay, если вы желаете погрузиться глубже в тему составных объектов.

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

  • Сильная связанность: так как потомки зависят от реализации родителя, то иерархия классов приводит к самой сильной связанности из вообще возможных в ООП

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

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

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

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

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

Вот так выглядит создание составных объектов с использованием классов:

class Foo {
  constructor () {
    this.a = 'a'
  }
}
class Bar extends Foo {
  constructor (options) {
    super(options);
    this.b = 'b'
  }
}
const myBar = new Bar(); // {a: 'a', b: 'b'}

А вот так выглядит создание составных объектов с использованием смешения объектов:

const a = {
  a: 'a'
};
const b = {
  b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}

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

  1. Существует более одного способа создавать составные объекты

  2. Некоторые пути лучше других

  3. Вы выбираете самый простой, наиболее гибкий способ решения поставленной задачи.

Заключение

Эта статья не о том, что лучше, функциональное программирование (ФП) или объектно-ориентированное программирование (ООП) и не о том, какой язык программирования лучше. Компоненты могут представлять собой функции, структуры данных, класс, и т.д. Различные языки программирования располагают к использованию различных базовых элементов для компонентов. Java предполагает использование классов, Haskell - функций, и т.д. Неважно, какой язык и какую парадигму вы предпочитаете, вам не уйти от функциональной композиции и структур данных. В конце концов именно к этому все и сводится.

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

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

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

Не важно, как вы пишете код, вы должны делать это хорошо.

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

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

Настало время узнать как разрабатывать ПО.




К сожалению, не доступен сервер mySQL