Я бы предпочел сказать вам пункт назначения

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

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

Функциональные языки программирования также носят декларативный характер. Программа больше фокусируется на логических утверждениях и ищет доказательства, возвращающие результат для этих утверждений. Когда мы используем функции массива JavaScript, такие как map и filter (или библиотеки, такие как lodash), мы часто используем декларативное программирование.

Что, если бы Танос щелкнул декларативно?

Давайте рассмотрим быстрый императивный пример.

export function snap(thor, thanos, features) {
  if (
    thanos.hasSpaceStone &&
    thanos.hasMindStone &&
    thanos.hasRealityStone &&
    thanos.hasPowerStone &&
    thanos.timeStone &&
    thanos.hasSoulStone &&
    !thor.aimsForTheHead
  ) {
    return features
      .sort(() => Math.random() - Math.random())
      .slice(0, features.length / 2);
  } else if (thor.aimsForTheHead) {
    return features;
  } else {
    // ...
  }
}

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

Преимущества

  • Более оптимален по производительности
  • Легче отлаживать
  • Нет необходимости изучать DSL
  • Лучше интегрируется с IDE и библиотеками анализа кода

Проблемы

  • Труднее читать
  • Потенциально сложнее добавить больше потока управления

Давайте вернемся к Snap с более декларативным подходом.

const proofs = [
  {
    logicGate: "AND",
    statements: [
      { variable: "hasSpaceStone", operator: "equals", value: true },
      { variable: "hasMindStone", operator: "equals", value: true },
      { variable: "hasRealityStone", operator: "equals", value: true },
      { variable: "hasPowerStone", operator: "equals", value: true },
      { variable: "timeStone", operator: "equals", value: true },
      { variable: "hasSoulStone", operator: "equals", value: true },
      {
        variable: "aimsForTheHead",
        operator: "equals",
        value: false,
      },
    ],
    outcome: (features) => {
      return features
        .sort(() => Math.random() - Math.random())
        .slice(0, features.length / 2);
    },
  },
  {
    statements: [
      {
        variable: "aimsForTheHead",
        operator: "equals",
        value: true,
      },
    ],
    outcome: (features) => features,
  },
];

Обратите внимание, что в этом примере, хотя строк кода больше, мы можем легче читать и понимать несколько результатов. Здесь также имеется язык относительно общего назначения, а не предметно-ориентированный язык. Нам как разработчикам это нравится, потому что нам не нужно тратить много времени на изучение чего-то нового для простой задачи. Нам не нужно писать длинные сложные операторы if или операторы case для возврата наших результатов. И достаточно просто написать еще один набор предложений и определить присваивание.

Преимущества

  • Легче читать
  • Ориентирован на результат
  • Легче добавить больше утверждений для будущих улучшений

Проблемы

  • Сложнее отлаживать
  • Затраты на производительность с промежуточным императивным потоком управления
  • Больше строк кода
  • Может плохо интегрироваться с IDE или библиотеками анализа качества.

Практический пример использования

Переключение функций — это программный подход, обеспечивающий длительные циклы разработки функций. Иногда для завершения функции требуется несколько спринтов. Иногда команда разработчиков может захотеть протестировать функцию на подмножестве потребителей (когорте) и собрать данные об использовании этой функции. Долгоживущие ветки разработки - это боль и, как правило, не лучший вариант. Чтобы обеспечить подход функционального программного обеспечения с неполными функциями, многие команды разработчиков используют переключатели функций.
Одной из проблем, с которой сталкиваются некоторые инженеры при переключении функций, является мертвый код. Иногда необходимо определить процессы, чтобы убедиться, что логика функции удалена из кода (например, задача удаления функции переключается, когда функция завершена). Если мы сможем удалить как можно больше потока управления и вместо этого реализовать несколько методов, это может быть предпочтительнее, и в долгосрочной перспективе его будет легче поддерживать. У команды может быть много функций, которые сложно переключать в зависимости от среды и других факторов, требующих сложных потоков управления. Рассмотрим этот пример функции.

// types.js
export type FeatureToggleType = {
  project: string;
  key: string;
  description: string;
  environments: ("Local" | "Development" | "Staging" | "Preprod")[];
};

// featureConfig.json
[
  {
    "project": "CMS",
    "key": "Cache",
    "description": "Enable Caching for production",
    "environments": ["Production"]
  },
  {
    "project": "CRM",
    "key": "newWidget",
    "description": "New Widget component.  IT should only be enabled in Local and Dev environments.",
    "environments": ["Development"]
  },
  {
    "project": "Navigation",
    "key": "productFeature",
    "description": "Products link for launching the new product application.",
    "environments": ["Local", "Development", "Staging", "Preprod"]
  }
]

// featureProofs.js
export const toggleProofs: Proofs<FeatureToggleType, string> = [
  {
    statements: [
      {
        variable: "project",
        operator: "equals",
        value: "CMS",
      },
      {
        variable: "key",
        operator: "equals",
        value: "Cache",
      },
      {
        variable: "environment",
        operator: "contains",
        value: "Prod",
      },
    ],
    logicGate: "AND",
    outcome: "cacheIsEnabled",
  },
  {
    statements: [
      {
        variable: "key",
        operator: "contains",
        value: "widget",
      },
      {
        variable: "key",
        operator: "startsWith",
        value: "new",
      },
    ],
    logicGate: "OR",
    outcome: "newWidgetIsEnabled",
  },
  {
    statements: [
      {
        variable: "description",
        operator: "equals",
        value:
          "Recommendations link for launching the new product application.",
      },
      {
        variable: "description",
        operator: "equals",
        value: "Products link for launching the new product application.",
      },
    ],
    logicGate: "XOR",
    outcome: "productLinkIsEnabled",
  },
];

// featureMethods.js
export function getFeatures(
  features: FeatureToggleType[],
  toggleProofs: Proofs<FeatureToggleType, string>
) {
  const gambit = new Gambit(toggleProofs);
  return features.map((feature) => gambit.evaluate(feature)?.outcome);
}
  1. Функции определяются в файле конфигурации в файле featureProofs.json. Если бы нам нужны были более динамичные результаты, это легко мог бы быть какой-либо API флагов функций.
  2. Вместо реализации чрезмерной логики потока управления для каждого переключателя функций мы создаем файл, используя декларативный стиль программирования, определяющий потенциальные результаты в featureProofs.js. В этом файле используется язык более общего назначения.
  3. Наконец, достаточно просто использовать функциональное программирование и декларативно написать метод, который возвращает все доступные функции в featureMethods.js.

Где найти библиотеку

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