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

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

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

Умение писать код с единой ответственностью

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

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

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

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

// sum(1, 2) will always return 3, hence pure
var sum = (a, b) => a + b;
// sumImpure(1) will return different results based on the value of // globalVar, hence impure
var globalVar = ... ;
var sumImpure = (a) => a + globalVar;

Способность легко понять решаемую проблему

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

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

Сочинение

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

  • Два числа будут использоваться для расчета приведенного в действие числа
  • Результат предыдущего шага будет отменен
  • Результат предыдущего шага будет увеличен на 1
// composition - evaluated right to left
var f = R.compose(R.inc, R.negate, Math.pow);
f(3, 4); // -(3^4) + 1
// another form of composition with left to right evaluation
var g = R.pipe(Math.pow, R.negate, R.inc);
g(3, 4); // -(3^4) + 1

Цепочка

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

В качестве примера см. приведенный ниже код, в котором используется Java 8. Код был написан декларативным образом с использованием методов цепочки для объекта IntStream.

int sum = IntStream.rangeClosed(1, 10)
    .filter(number -> number % 2 == 0) // 2 + 4 + 6 + 8 + 10
    .sum(); // 30

Возможность повторного использования кода

Повторное использование кода приводит к уменьшению количества строк кода, что снижает вероятность ошибки при внесении изменений в код. Это известно как принцип СУХОЙ. Функциональное программирование предоставляет такие методы, как композиция функций, функции более высокого порядка, каррирование и частичные функции, которые помогают повторно использовать код.

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

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

Частичные функции

Иногда у вас есть функции, которые принимают более одного аргумента. Применяя другой набор аргументов, вы можете получить несколько функций из одной и той же функции. Приведенный ниже фрагмент был взят из библиотеки JavaScript lodash. Обратите внимание, как функция greet повторно используется для создания функций sayHelloTo и greetFred.

var greet = (greeting, name) => greeting + ' ' + name;
var sayHelloTo = _.partial(greet, 'hello');
sayHelloTo('fred'); // 'hello fred'
// Partially applied with placeholders.
var greetFred = _.partial(greet, _, 'fred');
greetFred('hi'); // 'hi fred'

карри

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

var abc = (a, b, c) => [a, b, c];
var curried = _.curry(abc);
// calling with three arguments separately
curried(1)(2)(3); // [1, 2, 3]
// calling with 2 arguments and then with 1 argument
curried(1, 2)(3); // [1, 2, 3]
// calling with all three arguments together
curried(1, 2, 3); // [1, 2, 3]
// Curried with placeholders.
curried(1)(_, 3)(2); // [1, 2, 3]

Умение писать надежный код

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

Монады

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

// if you want to handle all exceptions in the same way
Try.of(() -> bunchOfWork()).getOrElse(other);
// if you want to provide different handling per exception type
A result = Try.of(this::bunchOfWork)
    .recover(x -> Match(x).of(
        Case(instanceOf(Exception_1.class), ...),
        Case(instanceOf(Exception_2.class), ...),
        Case(instanceOf(Exception_n.class), ...)
    ))
    .getOrElse(other);

Еще один пример монады, инкапсулирующей нулевые проверки, можно найти по этой ссылке.

Резюме

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

Этот пост изначально был опубликован здесь.