Скидки до 20% + 2-ая профессия бесплатно и подарки на 50 000₽

Главная | Все статьи | Код

Что такое expression problem, или О дуализме функционального и объектно-ориентированного программирования

Время чтения статьи ~6 минут 116
Что такое expression problem, или О дуализме функционального и объектно-ориен... главное изображение

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

Термином «expression problem» обозначают проблему выбора подхода, который позволит добавлять в программу новые сущности и операции над ними без изменения существующей реализации.

Примечание — В компилируемых языках изменение существующего кода приводит к его перекомпиляции.

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

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

ООП и expression problem

В ООП каждую сущность (фигуру) можно описать классом, а действия над ней — методами этого класса:

class Rectangle {
  // конструктор для создания объекта Прямоугольник
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  // метод вычисления площади прямоугольника
  getArea() {
    return this.width * this.height;
  }

  // метод вычисления периметра прямоугольника
  getPerimeter() {
    return 2 * (this.width + this.height);
  }
}

class Circle {
  // конструктор для создания объекта Круг
  constructor(radius) {
    this.radius = radius;
  }

  // метод вычисления площади круга
  getArea() {
    return Math.PI * this.radius ** 2;
  }

  // метод вычисления периметра круга
  getPerimeter() {
    return 2 * Math.PI * this.radius;
  }
}

Чтобы добавить новую сущность, достаточно создать ещё один класс с нужными методами.

class RightTriangle {
  constructor(baseLength, height) {
    this.baseLength = baseLength;
    this.height = height;
  }

  getArea() {
    return 0.5 * this.baseLength * this.height;
  }

  getPerimeter() {
    return Math.sqrt(4 * this.height ** 2 + this.baseLength ** 2)
      + this.baseLength;
  }
}

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

class Rectangle {
  // ...прочие операции
  getHeight() {
    return this.height;
  }
}

class Circle {
  // ...прочие операции
  getHeight() {
    return 2 * this.radius;
  }
}

class RightTriangle {
  // ...прочие операции
  getHeight() {
    return this.height;
  }
}

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

Функциональное программирование и expression problem

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

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

const shapes = {
  rectangle: (width, height) => ({ width, height }),
  circle: (radius) => ({ radius }),
};

const areas = {
  rectangle: ({ width, height }) => width * height,
  circle: ({ radius }) => Math.PI * radius ** 2,
};

const perimeters = {
  rectangle: ({ width, height }) => 2 * (width + height),
  circle: ({ radius }) => 2 * Math.PI * radius,
};

// обозначен тип сущности 'circle'
const shapeType = 'circle';
// получение из объекта shapes значения свойства circle,
// функции для создания фигуры круг
const makeShape = shapes[shapeType];
// аналогично из объекта areas получена функция для
// вычисления площади сущности circle
const getArea = areas[shapeType];

// создание круга с радиусом 2.42
const circle = makeShape(2.42);
// и вычисление его площади
const circleArea = getArea(circle);

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

// объект, содержащий функции для получения высоты фигуры
const heights = {
  rectangle: ({ height }) => height,
  circle: ({ radius }) => 2 * radius,
};

const shapeType = 'rectangle';
const makeShape = shapes[shapeType];
const getHeight = heights[shapeType];

const rectangle = makeShape(42.42, 67.67);
const heightRectangle = getHeight(rectangle);

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

const shapes = {
  rectangle: (width, height) => ({ width, height }),
  circle: (radius) => ({ radius }),
  // добавлено новое свойство: функция для создания треугольника
  rightTriangle: (baseLength, height) => ({ baseLength, height }),
};

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

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

Разработчику придётся доработать реализацию операций и добавить в каждый объект свойство rightTriangle.

const areas = {
  // ...прочие фигуры
  rightTriangle: ({ baseLength, height }) => (
    0.5 * baseLength * height
  ),
};

const perimeters = {
  // ...прочие фигуры
  rightTriangle: ({ baseLength, height }) => (
    Math.sqrt(4 * height ** 2 + baseLength ** 2) + baseLength
  ),
};

const heights = {
  // ...прочие фигуры
  rightTriangle: ({ height }) => height,
};

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

Заключение

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

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

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

Примечание — В проекте «Вычислитель отличий» на Хекслете студенты сталкиваются с expression problem. В этом проекте лучше работает подход, принятый в функциональном программировании, так как в приложение нужно добавить несколько операций над сущностями.

Адаптированный перевод статьи The Expression Problem in .NET by Camden Reslink. Над примерами кода работал ментор Хекслета Станислав Дзисяк. Мнение администрации Хекслета может не совпадать с мнением автора оригинальной публикации.

Аватар пользователя Дмитрий Дементий
116
Похожие статьи
Рекомендуемые программы
профессия
Верстка на HTML5 и CSS3, Программирование на JavaScript в браузере, разработка клиентских приложений используя React
10 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на Python, Разработка веб-приложений и сервисов используя Django, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
Тестирование веб-приложений, чек-листы и тест-кейсы, этапы тестирования, DevTools, Postman, SQL, Git, HTTP/HTTPS, API
4 месяца
с нуля
Старт 26 декабря
профессия
Программирование на Java, Разработка веб-приложений и микросервисов используя Spring Boot, проектирование REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
новый
Google таблицы, SQL, Python, Superset, Tableau, Pandas, визуализация данных, Anaconda, Jupyter Notebook, A/B-тесты, ROI
9 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на PHP, Разработка веб-приложений и сервисов используя Laravel, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на Ruby, Разработка веб-приложений и сервисов используя Rails, проектирование и реализация REST API
5 месяцев
c опытом
Старт 26 декабря
профессия
Программирование на JavaScript в браузере и на сервере (Node.js), разработка бекендов на Fastify и фронтенда на React
16 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на JavaScript, разработка веб-приложений, bff и сервисов используя Fastify, проектирование REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
новый
Git, JavaScript, Playwright, бэкенд-тесты, юнит-тесты, API-тесты, UI-тесты, Github Actions, HTTP/HTTPS, API, Docker, SQL
8 месяцев
c опытом
Старт 26 декабря