Skip to content

Latest commit

 

History

History
287 lines (201 loc) · 30.8 KB

File metadata and controls

287 lines (201 loc) · 30.8 KB

Композиция программного обеспечения: Вступление

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

Композиция — это составление целого из частей.

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

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

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

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

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

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

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

Мы должны добиваться лучшего.

Вы составляете программы каждый день

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

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

Компонуем функции

Композиция функций — это процесс применения одной функции к результату другой. Например, в математике даны две функции f и g, в результате будет будет (f ∘ g)(x) = f(g(x)), где кругляшок является оператором композиции. Часто говорят "применение к" или "после". Вы можете произнести вслух "применение f к g равно результату f от g от x" или "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

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

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 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() ), то не используете промежуточные переменные. Описание функций без указания аргументов называется бесточечной нотацией. Для этого вы вызываете функцию, которая возвращает новую функцию, без явного её объявления. Это означает, что вам не нужно использовать ключевое слово function или стрелочный синтаксис (=>).

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

Несколько преимуществ уменьшения сложности:

Работа памяти

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

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

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

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

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

Место для ошибок

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

Меньше кода = меньше места для ошибок = меньше ошибок

Компонуем объекты

"Предпочитайте композицию наследованию класса" — «Банда Четырех», «Приёмы объектно-ориентированного проектирования. Паттерны проектирования» (прим. пер. the Gang of Four, “Design Patterns: Elements of Reusable Object Oriented Software”)

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

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

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

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

const fullName = {
  firstName,
  lastName
};

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

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

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

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

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

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

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

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

  • Проблема тесной связи: поскольку дочерние классы зависят от реализации родительского класса, то наследование является самой тесной связью, доступной в объектно-ориентированном дизайне.
  • Проблема хрупкого базового класса: из-за тесной связи изменения в базовом классе могут нарушить работу большого числа дочерних классов, и вероятно в коде, управляемом третьими сторонами. Автор может нарушить код, о котором он не знает.
  • Проблема негибкой иерархии: с таксономией одного предка и учетом достаточного времени и эволюции, все таксономии классов в конечном итоге неверны для новых случаев их применения.
  • Проблема дублирования по необходимости: из-за негибкой иерархии новые классы часто реализуются путем дублирования, а не c помощью расширения, что приводит к появлению подобных классов, которые неожиданно различаются. Как только происходит дублирование, становится неочевидно, из какого класса должны происходить новые классы или почему.
  • Проблема гориллы с бананом: "...проблема в объектно-ориентированных языках заключается в том, что у них существует неявная среда, которую они с собой несут. Вы хотели всего лишь банан, но в результате получаете гориллу, держащую этот банан, и все джунгли в придачу." ~ Joe Armstrong, “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. Вы хотите выбрать самое простое и гибкое решение для поставленной задачи.

Заключение

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

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

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

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

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

Суть разработки программного обеспечения — это композиция.

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

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

Теперь время изучить как составлять программы.

Продолжение следует в “The Rise and Fall and Rise of Functional Programming”

Узнайте больше на EricElliottJS.com

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

Eric Elliott автор книг “Programming JavaScript Applications” (O’Reilly), и “Learn JavaScript with Eric Elliott”. Он внес свой вклад в опыт разработки программного обеспечения для Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, и ведущих артистов, в том числе Usher, Frank Ocean, Metallica, и множество других.

О работаете удаленно из любой точки мира с самой красивой женщиной в мире.


Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.