Введение в Композицию в Javascript

Spread the love

Статья подробно и на примерах объясняется значение термина Композиция

Перевод статьи Eric ElliottComposing Software: An Introduction

Композиция: «Акт объединения частей или элементов в единое целое». ~ Dictionary.com

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

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

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

  • Что такое композиция функций?
  • Что такое композиция объекта?

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

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

Вы комбинируете код каждый день

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

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

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

Композиция функций – это процесс передачи результата выполнения одной функции ко входу другой функции. Говоря языком алгебры: задаются две функции f и g, при этом (f ∘ g) (x) = f (g (x)). Символ точки является оператором композиции. ОН обычно произносится как «составленный с» или «после». Вы можете прочитать это, как «f, составлена из g, равного f из g из x», или «f после g, равного f из g из x». Мы говорим f после g, потому что сначала вычисляется g, затем его результат передается в качестве аргумента для f.

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

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;

const wait = time => new Promise(
  (resolve, reject) => setTimeout(
    resolve,
    time
  )
);

wait(300)
  .then(() => 20)
  .then(g)
  .then(f)
  .then(value => console.log(value)) // 42
;

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

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

Намеренно составляя функции, мы можем улучшить нашу функцию doStuff() до простого однострочного:

const g = n => n + 1;
const f = n => n * 2;

const doStuffBetter = x => f(g(x));

doStuffBetter(20); // 42

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

const doStuff = x => {
  const afterG = g(x);
  console.log(`after g: ${ afterG }`);
  const afterF = f(afterG);
  console.log(`after f: ${ afterF }`);
  return afterF;
};

doStuff(20); // =>
/*
"after g: 21"
"after f: 42"
*/

Во-первых, давайте отвлечемся от «after f», «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()), вам не нужны промежуточные переменные. Написание функций без упоминания аргументов называется бессмысленным стилем (point-free style). Для этого вы будете вызывать функцию, которая возвращает новую функцию, а не объявлять ее явно. Это означает, что вам не понадобится ключевое слово function или синтаксис стрелки (=>).

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

Есть несколько плюсов в снижение сложности кода:

Ограничение памяти

Средний человеческий мозг может запоминать только небольшое количество сущностей в рабочей памяти. Когда вы добавляете больше переменных, наша способность точно помнить значение каждой переменной уменьшается. Обычно человек может помнить в один момент времени 4–7 сущностей. При увеличение этих цифр частота ошибок резко возрастает.

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

Соотношение сигнал шум

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

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

Площадь поверхности для ошибок

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

Меньше кода = меньше поверхности для ошибок = меньше ошибок.

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

«Пользуйся композицией объектов, а не используй наследование классов», «Банда четырех», «Design Patterns: Elements of Reusable Object Oriented Software»

«В компьютерной науке составной тип данных или составной тип данных – это любой тип данных, который может быть создан в программе с использованием примитивных типов данных языка программирования и других составных типов. […] Акт конструирования составного типа известен как композиция ». ~ Википедия

Это пример примитивов:

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

А это пример композиции:

const fullName = {
  firstName,
  lastName
};

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

Обратите внимание, что «Банда четырех» определяет шаблон, называемый composite pattern, который представляет собой определенный тип рекурсивной композиции объектов, который позволяет идентично обрабатывать отдельные компоненты и агрегированные композиты. Некоторые разработчики запутываются, думая, что composite pattern является единственной формой композиции объектов. Не путайтесь. Существует много разных видов композиции объектов.

Еще одна цитата от «Банды четырех»: «вы увидите, что композиция объектов применяется снова и снова в шаблонах проектирования». А затем они каталогизируют три вида композиционных отношений объекта: делегирование, знакомство и агрегация.

Делегирование (delegation) используется в шаблонах state, strategy и visitor pattern.

Знакомство (acquaintance) когда объект знает о другом объекте по ссылке, обычно передаваемой как параметр: отношение используется, например, в обработчике сетевого запроса в который передает ссылку на logger для логгирования запроса.

Агрегация (aggregation) когда дочерние объекты образуют часть родительского объекта: отношение has-a, например, дочерние объекты DOM являются компонентными элементами в узле DOM – то есть у дочернего узла DOM есть (has) дочерние элементы.

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

Мы будем использовать более общее определение композиционных объектов из «Categorical Methods in Computer Science: With Aspects from Topology» (1989):

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

Еще один хороший справочник – “Reliable Software Through Composite Design”, Glenford J Myers, 1975. Обе книги давно вышли, но вы все равно можете найти продавцов на Amazon или eBay, если вы хотите более подробно изучить тему композиции объектов.

Наследование классов – это всего лишь один из видов составных объектов. Все классы создают составные объекты, но не все составные объекты создаются классами или через наследование классов. «Сочетание композиции объекта с наследованием класса» означает, что вы должны формировать составные объекты из небольших составных частей, а не наследовать все свойства от предка в иерархии классов. Последнее вызывает большое количество известных проблем в объектно-ориентированном проектировании:

  • Проблема жесткой связи: Поскольку дочерние классы зависят от реализации родительского класса, наследование классов является наиболее тесной связью, доступной в объектно-ориентированном проектировании. 
  • Проблема хрупкого базового класса: Из-за тесной связи изменения в базовом классе потенциально могут нарушить большое количество классов-потомков, управляемыми третьими сторонами. Автор базового класса может сломать код, о котором он и не подозревает.
  • Проблема негибкой иерархии: Вся совокупность данных и методов одного предка, при условии длительного времени развития, вряд ли будет полезна в новых вариантах использования.
  • Проблема дублирования по необходимости: Из-за негибкой иерархии новые варианты использования часто реализуются путем дублирования, а не расширения, что приводит к сходным классам, которые неожиданно расходятся. Когда начинается дублирование, неясно, из каких классов должны происходить новые классы или почему.
  • Горилла / банановая проблема: «… Проблема с объектно-ориентированными языками заключается в том, что у них есть вся эта неявная среда, которую они носят с собой. Вы хотели банан, но получили гориллу с бананом и джунглями ». ~ Джо Армстронг,« Coders at Work » 

Наиболее распространенная форма компоновки объектов в 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. Выбирайте простейшее, наиболее гибкое решение для поставленной задачи.

Заключение

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

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

Чего мы не делали, мы не утверждали, что функциональное программирование лучше объектно-ориентированного программирования или что вы должны выбирать одно или другое. ООП против FP – ложная дихотомия. Каждое реальное приложение Javascript, которое я видел в последние годы, сильно смешивает FP и ООП.

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

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

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

Разработчик программного обеспечения, который не понимает композицию, подобен строителю дома, который не знает о болтах или гвоздях. Создание программного обеспечения без осознания композиции похоже на то, как строитель дома склеивает стены “клейкой лентой” и “crazy клеем”.

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

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

Была ли вам полезна эта статья?
[5 / 5]

Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован.