Парадигмы программирования - императивная и декларативная

год назад·5 мин. на чтение

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

Это серия статей о функциональном программировании:
  1. Парадигмы программирования (рассматривается в этой статье)
  2. Композиция
  3. Функторы
  4. Каррирование
  5. Чистые функции
  6. Функции первого класса

Парадигмы программирования

Парадигма программирования — это стиль или "способ" программирования. Поэтому некоторые языки заставляют нас писать в определенной парадигме. Другие языки оставляют варианты открытыми для программиста, где каждая парадигма следует набору понятий. За всю историю компьютерного программирования инженеры разработали разные языки. Каждый язык основывался на одной или нескольких парадигмах. Эти парадигмы принадлежат к одной из следующих двух основных категорий:

1. Императивная парадигма

В императивных языках программирования поток управления является явным, где программист инструктирует программу, как изменить ее состояние. В императивную парадигму также включается:
  • Структурная парадигма
  • Объектно-ориентированная парадигма

2. Декларативная парадигма

В декларативной парадигме поток управления является неявным, когда программист указывает программе, что следует делать, не указывая, как это должно быть сделано. В декларативную парадигму также включается:
  • Функциональная парадигма
  • Логическая парадигма
  • Математическая парадигма
  • Реактивная парадигма
Большинство языков принадлежат либо к императивной, либо к декларативной парадигме, где каждая парадигма имеет набор понятий, которым необходимо следовать. Рассмотрим подробнее каждую парадигму.

Императивная парадигма

Императивная парадигма немного изменилась из-за структурной парадигмы, но у нее все еще есть проблемы:
  • Указание программе, как что-то делать (поток управления является явным)
  • Общее состояние
Чтобы понять эти проблемы рассмотрим примеры.

Проблема 1: Указание программе, как что-то делать (поток управления является явным)

Кейс: представьте себе 1000 сотрудников с руководителем, который ведет их по проекту. Руководитель начинает рассказывать 1000 сотрудников, как делать вещи одну за другой. Как вы думаете, насколько это будет плохо? Я почти уверен, что вы видите, что этот стиль управления на микроуровне имеет большие риски, ловушки и даже не сработает. Решение: Сгруппировать людей по зонам ответственности и делегировать в каждую группу руководителя группы. Руководитель каждой группы должен знать, как делать что-то для достижении цели. Это значительно уменьшит сложность, узкие места и станет намного проще в управлении. В этой аналогии
  • Руководитель = Программист
  • Руководители групп = Функции более высокого уровня
  • Сотрудники в каждой группе = Строки кода
Вывод: когда мы применяем организационную структуру более высокого порядка на программном уровне, наша жизнь становится проще.

Проблема 2: Общее состояние

Кейс: Представьте отца, у которого двое детей. У них есть общий банковский счет. Каждый месяц отец кладет на этот счет 1000 долларов. Оба ребенка не знают, что учетная запись используется совместно. Таким образом, они оба думают, что у каждого есть 1000 долларов, которые он может потратить на себя. В конце месяца оказывается, что на этом счету осталось -1000 долларов. Решение: У каждого ребенка должна быть отдельная учетная запись и указанная ежемесячная сумма. В этой аналогии:
  • Дети = Функции
  • Общий банковский счет = общее состояние
Вывод: Когда ваши функции имеют одно и то же состояние, они используют его неосознанно. Это испортит состояние вашей программы даже с двумя функциями. Так что всегда лучше, чтобы каждая функция имела собственное независимое состояние для использования.

Пример императивной парадигмы

Давайте посмотрим, как функция для суммирования может быть реализована в императивной парадигме.
const sum = (list) => {
  let result = 0
  for (let i = 0; i < list.length; i++) {
    result += list[i]
  }
  return result
}
Почему этот код считается императивным?
  1. Указание программе, как что-то делать (поток управления является явным): мы явно сообщаем циклу for, как работать. Также мы обращаемся к каждому элементу в массиве явно.
  2. Совместное состояние: результирующая переменная является общим состоянием, изменяющимся на каждой итерации (с общим состоянием в более крупных решениях будет гораздо сложнее справиться).

Декларативная парадигма

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

Пример декларативной парадигмы

Мы увидели, как функция sum может быть реализована в императивной парадигме. Давайте посмотрим, как ее можно реализовать декларативно.
const add = (a, b) => a + b
const sum = (list) => list.reduce(add)
Похоже на магию? Но почему это считается декларативным?
  • Описано, что программа должна выполнять, а не как (неявный поток управления): нет явного итератора, нет явного указания циклу, как работать или как получить доступ к элементам. Это было достигнуто с помощью метода reduce.
  • Не производит побочных эффектов: общее состояние — это форма побочных эффектов, которая была полностью устранена с помощью метода reduce и функции add.

Еще одно сравнение

Что, если мы хотим суммировать только четные числа? Разберем эту задачу на примерах в разных парадигмах.

Императивная реализация

const evenSum = (list) => {
  let result = 0
  for (let i = 0; i < list.length; i++){
    if(list[i] % 2 === 0) {
      result += list[i]
    }
  }
  return result
}

Декларативная реализация

const evenSum = (list) => {    
  const isEven = (n) => n % 2
  const add = (a, b) => a + b
  return list.filter(isEven).reduce(add)
}
Как видим, если мы хотим сравнить обе парадигмы (императивную и декларативную), то декларативная парадигма (в нашем случае Функциональная парадигма) больше похожа на шестеренки. Вы разрабатываете свои шестеренки как отдельные единицы, затем добавляете их туда, где они вам нужны. Но в императивной парадигме это больше похоже на тесто. Почти все смешано и слито в один и тот же кусок код. В целом декларативная парадигма — это:
  • Предсказуемость
  • Тестируемость
  • Многоразовость
  • Настраиваемость
  • Кэшируемость
  • Поддерживаемость
  • Компонуемость
Некоторые из этих моментов не обязательно имеют смысл в контексте примера с функцией sum, но будут иметь смысл в следующих статьях о функциональном программировании.

Эволюция парадигм

Итак, у нас есть 2 основные парадигмы: императивная и декларативная, каждая из которых имеет подпарадигмы. Теперь поговорим подробнее о структурной, объектно-ориентированной и функциональной парадигмах. с эволюционной точки зрения. Каждая парадигма ограничивала способ программирования, вводя что-то новое.
  • Структурная парадигма: ограниченное использование goto и «потока передачи управления» за счет введения в наш код такой структуры, как if/else/then/loop и других. Другими словами, он ограничивает поток передачи управления.
  • Объектно-ориентированная парадигма: ограничение полиморфизма с использованием указателей на функции за счет введения полиморфизма с использованием наследования.
  • Функциональная парадигма: ограничения общего состояния и побочные эффекты за счет введения иммутабельности.
Имейте в виду, что каждая парадигма может использовать одну или несколько концепций других парадигм (например, как объектно-ориентированная, так и функциональная парадигмы используют концепции структурной парадигмы).

Итоги

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

Хорошие практики и шаблоны проектирования для Vue Composables

10 месяцев назад·1 мин. на чтение

Composables (composition API) можно использовать для хранения основной бизнес-логики (например, вычислений, действий, процессов), поэтому они являются важной частью приложения. К сожалению, в командах не всегда есть соглашения для написания composables.

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

Общие шаблоны проектирования

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

Базовый composable

В документации Vue показан следующий пример компонуемого useMouse:
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// по соглашению имена composable функций начинаются с "use"
export function useMouse() {
  // состояние инкапсулировано и управляется самим composable
  const x = ref(0)
  const y = ref(0)

  // composable может обновлять состоянием
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // composable также может иметь доступ к жизненному циклу родительского компонента
  // для установки и очистки эффектов
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // отдаем состояние наружу как возвращаемое значение
  return { x, y }
}
Позже это можно использовать в компоненте следующим образом:
<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

Асинхронные composables

Для получения данных Vue рекомендует следующую компонуемую структуру:
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  watchEffect(() => {
    // обнуляем состояние перед запросом
    data.value = null
    error.value = null

    // toValue() разворачивает потенциальные рефы или геттеры
    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  })

  return { data, error }
}
Затем это можно использовать в компоненте следующим образом:
<script setup>
import { useFetch } from './fetch.js'

const { data, error } = useFetch('...')
</script>

Соглашения composable

Основываясь на приведенных выше примерах, вот контракт, которому должны следовать все composables:
  1. Имена composable файлов должны начинаться с use, например useSomeAmazingFeature.ts
  2. Он может принимать входные аргументы, которые могут быть примитивными типами, такими как строки, или может принимать ссылки и геттеры, но для этого требуется использовать вспомогательное средство toValue
  3. Composable должен возвращать значение ref, к которому можно получить доступ после деструктурирования composable, например const { x, y } = useMouse()
  4. Composables могут содержать глобальное состояние, к которому можно получить доступ и изменить в приложении.
  5. Composable может вызвать побочные эффекты, такие как добавление прослушивателей событий окна, но их следует очищать при отключении компонента.
  6. Composables следует вызывать только в <script setup> или в хуке setup(). В этих контекстах их также следует называть синхронно. В некоторых случаях их также можно вызывать в обработчиках жизненного цикла, таких как onMounted()
  7. Composables могут вызывать другие composables внутри
  8. Composables должны заключать в себе определенную логику, а когда они слишком сложны, их следует извлекать в отдельные composables для облегчения тестирования.

Рекомендации

Composables с состоянием и/или чистые функции

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

Модульные тесты (unit тесты) для компонуемых компонентов

На фронтенде обычно работа происходит с визуальными компонентами. Из-за этого модульное тестирование целых компонентов может быть не лучшей идеей, потому что по факту происходит модульное тестирование самого фреймворка (если была нажата кнопка, проверьте, изменилось ли состояние или открылось модальное окно). Благодаря тому, что мы переместили всю бизнес-логику внутрь composables (которые в основном являются функциями TypeScript), их очень легко протестировать с помощью Vitest, что позволяет нам также иметь более стабильную систему.