[Перевод] Функциональное программирование: дурацкая игрушка, которая убивает производительность труда. Часть 1

Возможно, вы уже слышали о так называемом «функциональном» программировании. Возможно, вы даже подумываете о том, что вам стоит его как-нибудь попробовать.

Ни в коем случае этого не делайте!

wrxh7rlj7pjiyzivyksens90mtu.png

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

Функциональное программирование не может удовлетворить многогранным корпоративным требованиям


71cc21efe85ed065a058cce2712ef687.jpg


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

Полагаю, вышеприведённый текст особенно понятным не выглядит. Но совсем скоро всё встанет на свои места.

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

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

import { filter, first, get } from 'lodash/fp';

const filterByType = type =>
  filter( x => x.type === type );

const fruits = [
  { type: 'apple', price: 1.99 },
  { type: 'orange', price: 2.99 },
  { type: 'grape', price: 44.95 }  
];

const getFruitPrice = type => fruits =>
  fruits
  |> filterByType(type)
  |> first
  |> get('price');

const getApplePrice = getFruitPrice('apple');

console.log('apple price', getApplePrice(fruits));


Если это всё ничего кроме злости у вас не вызывает, то знайте, что вы в этом не одиноки!

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

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

Функциональные программные решения не выдерживают проверку временем


1b6e7f82d24df8304a9a44dae7157698.jpg


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

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

class Fruit {
  constructor(type, price) {
    this.type = type;
    this.price = price;
  }
}

class AbstractFruitFactory {
  make(type, price) {
    return new Fruit(type, price);
  }
}

class AppleFactory extends AbstractFruitFactory {
  make(price) {
    return super.make("apple", price);
  }
}

class OrangeFactory extends AbstractFruitFactory {
  make(price) {
    return super.make("orange", price);
  }
}

class GrapeFactory extends AbstractFruitFactory {
  make(price) {
    return super.make("grape", price);
  }
}

class FruitRepository {
  constructor() {
    this.fruitList = [];
  }

  locate(strategy) {
    return strategy.locate(this.fruitList);
  }

  put(fruit) {
    this.fruitList.push(fruit);
  }
}

class FruitLocationStrategy {
  constructor(fruitType) {
    this.fruitType = fruitType;
  }

  locate(list) {
    return list.find(x => x.type === this.fruitType);
  }
}

class FruitPriceLocator {
  constructor(fruitRepository, locationStrategy) {
    this.fruitRepository = fruitRepository;
    this.locationStrategy = locationStrategy;
  }

  locatePrice() {
    return this.fruitRepository.locate(this.locationStrategy).price;
  }
}

const appleFactory = new AppleFactory();
const orangeFactory = new OrangeFactory();
const grapeFactory = new GrapeFactory();

const fruitRepository = new FruitRepository();
fruitRepository.put(appleFactory.make(1.99));
fruitRepository.put(orangeFactory.make(2.99));
fruitRepository.put(grapeFactory.make(44.95));

const appleLocationStrategy = new FruitLocationStrategy("apple");

const applePriceLocator = new FruitPriceLocator(
  fruitRepository,
  appleLocationStrategy
);

const applePrice = applePriceLocator.locatePrice();

console.log("apple", applePrice);


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

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

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

Серьёзный менеджмент нуждается в серьёзных возможностях


54c763a2ab5d46d412dbe54ad7299e5f.jpg


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

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

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

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

Следующий пример ясно показывает неполноценность функционального программирования. Использование этой методологии слишком сильно упрощает рефакторинг:

// до рефакторинга:

// calculator.js:
const isValidInput = text => true;

const btnAddClick = (aText, bText) => {
  if (!isValidInput(aText) || !isValidInput(bText)) {
    return;
  }
}


// после рефакторинга:

// inputValidator.js:
export const isValidInput = text => true;

// calculator.js:
import { isValidInput } from './inputValidator';

const btnAddClick = (aText, bText, _isValidInput = isValidInput) => {
  if (!_isValidInput(aText) || !_isValidInput(bText)) {
    return;
  }
}


Если вас воротит от простоты этого рефакторинга — знайте, что такие ощущения испытываете не только вы. До рефакторинга было шесть строк кода, после стало семь строк? Это что — шутка?

Сравним это с нормальным рефакторингом объектно-ориентированного кода:

// до рефакторинга:
public class CalculatorForm {
    private string aText, bText;
    
    private bool IsValidInput(string text) => true;
    
    private void btnAddClick(object sender, EventArgs e) {
        if ( !IsValidInput(bText) || !IsValidInput(aText) ) {
            return;
        }
    }
}


// после рефакторинга:
public class CalculatorForm {
    private string aText, bText;
    
    private readonly IInputValidator _inputValidator;
    
    public CalculatorForm(IInputValidator inputValidator) {
        _inputValidator = inputValidator;
    }
    
    private void btnAddClick(object sender, EventArgs e) {
        if ( !_inputValidator.IsValidInput(bText)
            || !_inputValidator.IsValidInput(aText) ) {
            return;
        }
    }
}

public interface IInputValidator {
    bool IsValidInput(string text);
}

public class InputValidator : IInputValidator {
    public bool IsValidInput(string text) => true;
}

public class InputValidatorFactory {
    public IInputValidator CreateInputValidator() => new InputValidator();
}


Именно так выглядит настоящее программирование! До рефакторинга было девять строк кода, а после стало 22. Для выполнения этой работы требуется приложить больше усилий. Это заставит программистов, работающих на компанию, дважды подумать перед тем, как ввязывать в подобную авантюру.

Ущербная концепция декларативного подхода к программированию


0b0147810a0d938d287d9468942d8557.jpg


Так называемые «функциональные» программисты неоправданно гордятся собой из-за того, что пишут декларативный код. Но тут нечем гордиться — подобный код лишь создаёт иллюзию продуктивности.

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

Взглянем на хорошо абстрагированный объектно-ориентированный код:

class CountryUserSelectionStrategy {
  constructor(country) {
    this.country = country;
  }
  
  isMatch(user) {
    return user.country === this.country;
  }
}

class UserSelector {
  constructor(repository, userSelectionStrategy) {
    this.repository = repository;
    this.userSelectionStrategy = userSelectionStrategy;
  }
  
  selectUser() {    
    let user = null;

    for (const u in users) {
      if ( this.userSelectionStrategy.isMatch(u) ) {
        user = u;
        break;
      }
    }
    
    return user;
  }
}

const userRepository = new UserRepository();
const userInitializer = new UserInitializer();
userInitializer.initialize(userRepository);

const americanSelectionStrategy = new CountryUserSelectionStrategy('USA');
const americanUserSelector = new UserSelector(userRepository, americanSelectionStrategy);

const american = americanUserSelector.selectUser();

console.log('American', american);


Присмотритесь к императивному циклу for (const u in users). Не обращайте внимания на второстепенный шаблонный объектно-ориентированный код, не связанный с выполняемой задачей. Его нужно было включить в программу для того, чтобы сделать этот пример соответствующим жёстким требованиям абстрагирования, предъявляемым любой серьёзной организацией к коду.

Декларативный код, с другой стороны, является слишком конкретным, он, что неправильно, заставляет разработчика ориентироваться на менее важные вещи вроде бизнес-логики. Сравните мощное решение корпоративного уровня, описанное выше, со следующим образцом неполноценного «декларативного» кода:

SELECT * FROM Users WHERE Country=’USA’;


Каждый раз, когда я вижу SQL-код, меня прямо-таки коробит от его декларативности. Почему SQL? Почему бы программистам не использовать адекватные абстракции корпоративного класса и не писать бы нормальный объектно-ориентированный код? Особенно учитывая то, что в их распоряжении уже есть всё необходимое. Это — просто взрыв мозга, иначе и не скажешь.

Моделирование реального мира


4bdcd437aa1ee4fc65f390a0deb26196.jpg


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

Любой уважающий себя программист должен ежедневно использовать наследование для достижения высокого уровня многократного использования кода. Как уже было сказано, наследование отлично подходит для моделирования реального мира. Кошки, например, всегда наследуют свои свойства и поведение от единственного абстрактного животного из реального мира. Жизнь зародилась в океане несколько миллиардов лет назад. В результате все млекопитающие (включая кошек) унаследовали свойства от некоей первозданной рыбы. Например, говоря объектно-ориентированным языком, это может быть нечто вроде garfield.fishHead — свойства, описывающего рыбью голову кота Гарфилда. То же самое можно сказать и о поведении, что, в терминологии ООП может выглядеть как garfield.swim() (плавание) и garfield.layCaviar() (икрометание). Никого не удивляет то, что кошки так сильно любят принимать ванны и плавать! Люди, в сущности, это то же самое. Человек, если захочет, легко может начать метать икру!

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

Функции всегда должны быть привязаны к объектам


5501e2207d8fb99adbf1afc13d7ea7b5.jpg


То, что функции должны быть всегда привязаны к объектам, подсказывает нам здравый смысл. Кроме того, это отлично воспроизводит то, что мы можем видеть в реальном мире. В блокнотах имеется встроенный метод «записать». Это метод вызывается каждый раз, когда вы планируете что-то записать в блокнот. Вы можете этого и не осознавать, но и у вас есть методы. Например — нечто вроде .eat(veggies), позволяющий вам есть овощи, и .doHomework(), благодаря которому вы делали домашние задания когда учились в школе. Это — просто здравый смысл. Как иначе ваша мама, когда вы были помладше, заставляла бы вас есть овощи и делать домашние задания? Конечно, она напрямую вызывала эти ваши методы!

Ни одну работу в реальном мире нельзя выполнить, не привлекая профессионального менеджера, координирующего задачи. Его можно представить в виде объекта Manager. Молодёжи, возможно, нужен менеджер, помогающий удовлетворять базовые человеческие потребности, ну, скажем, вроде «смотреть Netflix и отдыхать».

Кто, в конце концов, будет координировать все составные части этого сложного процесса? А люди достаточно умные могут нанять и несколько менеджеров, поступив в точности так, как рекомендует ООП.

В реально мире создание чего-то нового и интересного, кроме того, требует специальной фабрики, которую можно представить в виде объекта Factory. У Леонардо да Винчи, например, была фабрика по производству картин — MonaLisaFactory, а Дональд Трамп строит секретную фабрику WallFactory.

Несложно заметить, что это — очередной гвоздь в «функциональный» гроб, так как функциональное программирование не пытается моделировать реальный мир. Функциям позволено существовать отдельно от объектов, а это просто неправильно.

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

Функциональное программирование не даёт возможностей для профессионального роста


87d9a9244d1fd81c5d72a74ab39edbb9.jpg


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

Во-первых, программисту нужно изучить продвинутые ООП-техники вроде наследования, абстракции, инкапсуляции и полиморфизма. Затем нужно хорошо освоить тьму паттернов проектирования (вроде паттерна «Синглтон») и начать использовать их в своём коде. Существует около 30 базовых паттернов проектирования, которые нужно очень хорошо знать. В идеале где-то в процессе изучения паттернов программист должен начать использовать в своём коде различные абстракции корпоративного уровня.

Следующий шаг — ознакомление с технологиями наподобие Domain-Driven Design, и изучение того, как разделять на части монолитные программные проекты. Кроме того, рекомендовано изучение подходящих инструментов для рефакторинга кода вроде Resharper, так как рефакторинг объектно-ориентированного кода — задача нетривиальная.

Для того чтобы достичь достойного уровня в сфере ООП нужно 20–30 лет. Но надо отметить, что даже большинство тех, у кого наберётся 30 лет опыта, не могут считаться настоящими мастерами ООП. Путь ООП-ученика тяжёл и наполнен неопределённостью. Объектно-ориентированного разработчика, в результате, ждёт учёба длиною в жизнь. Это ли не прекрасно?

А как насчёт несчастных функциональных программистов? К сожалению, им нужно изучить не так уж и много. Я сам учил нескольких джуниоров функциональному программированию на JavaScript. У них начало неплохо получаться примерно через полгода. Им просто понадобилось понять несколько базовых концепций, а затем надо было научиться быстро их применять. Где тут упоение от пожизненной учёбы? Я бы им не позавидовал.

Продолжение следует…

Уважаемые читатели! Что вам больше всего не нравится в функциональном программировании?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru