ChatGPT на русском языке, бесплатноНовости и обновления в Telegram
На sponsr есть решения ваших задач
Полезные видео о фронтенде. Подпишись на Rutube
Функции первого класса. Функциональное программирование
2 года назад·4 мин. на чтение
В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Функции первого класса.
Это серия статей о функциональном программировании:
Итак, вы спросите себя: «Хорошо, я понимаю взаимосвязь между функциональным программированием и математикой, но как первоклассные функции сделают возможными все эти преимущества?»
Очень хороший вопрос. Так как функциональное программирование полностью зависит от наличия привилегий функций, функции первого класса — это краеугольный камень для всех концепций функционального программирования.
Наличие в языке программирования функций первого класса позволяет иметь удивительные шаблоны, которые рассмотрим далее.
Рассмотрим каждую строчку.
Строка 1:
- Парадигмы программирования
- Композиция
- Функторы
- Каррирование
- Чистые функции
- Функции первого класса (рассматривается в этой статье)
Что такое функция первого класса?
Считается, что язык программирования поддерживает функции первого класса, если он не имеет ограничений на то, как функции могут создаваться или использоваться. Говорят, что язык программирования имеет функции первого класса, когда функции в этом языке рассматриваются как любая другая переменная. В общем, языки программирования накладывают ограничения на способы манипулирования вычислительными элементами. Говорят, что элементы с наименьшими ограничениями имеют статус первого класса. Вот некоторые из "прав и привилегий" первоклассных элементов:- Может быть назначен обычным переменным
- Передаются в качестве аргументов функциям
- Возвращается как результат функций
- Входит в любые структуры данных
Особенности функций первого класса
1. Функция первого класса может быть назначена обычным переменным
const string = "Foo" const num = 2 const bool = false const greet = (name) => `Hello ${name}` // ...другие примитивные типы данных greet('John') // Hello John
2. Функция первого класса передается в качестве аргумента в другие функции
Есть функцияconst nums = [1, 2, 3, 4, 5] const addOne = (n) => n + 1 const addedOne = nums.map(addOne) // [2, 3, 4, 5, 6]
addOne
, которая обрабатывается как переменная и передается в функцию .map
. При этом функция addOne
действительно является функцией первого класса.
3. Функция первого класса возвращается как результат функции
Функцияconst makeCounter = () => { let count = 0 return () => ++count } const counter = makeCounter() counter() // 1 counter() // 2 counter() // 3 counter() // 4
makeCounter
вернула функцию, которую мы присвоили переменной счетчика. Где переменная counter
теперь содержит обычную функцию.
4. Функция первого класса входит в любые другие структуры данных
Мы можем хранить функции в массивах и, как вы уже догадались, мы также можем хранить их в объектах и так же перебирать их.const wakeUp = name => `${name}, wake up early!` const takeShower = name => `${name}, take shower!` const workout = name => `${name}, workout!` const shutUp = name => `${name}, shut up!` const morningRoutines = [ wakeUp, takeShower, workout, shutUp ] morningRoutines.forEach(routine => routine('John')) // John, wake up early! // John, take shower! // John, workout! // John, shut up!
Почему функции первого класса важны
Функциональное программирование находится под сильным влиянием математики. Функциональное программирование хотело бы, чтобы математика была включена в каждую строку кода. Хотя математика состоит только из функций и переменных, она все равно очень мощная и выразительная. Это то, что пытается сделать и функциональное программирование - решать каждую отдельную проблему с использованием функций и только функций. Когда вы в языке программирования можете обращаться с функцией так же просто, как с переменной, этот язык будет гораздо более гибким и откроет много возможностей для улучшений. Функциональное программирование сделает ваш код более предсказуемым, тестируемым, повторно используемым, настраиваемым, кэшируемым, поддерживаемым, компонуемым и читабельным.Паттерны на основе функций первого класса
1. Функции высшего порядка (Higher-order functions)
Функции считаются функциями высшего порядка, когда они принимают функции в качестве аргументов (например, большинство методов Array,.map
, .filter
, .reduce
, .every
) и/или возвращают функцию в качестве результата (точно так же, как makeCounter
).
2. Замыкания
Замыкание — это функция, возвращаемая «родительской» функцией, и имеющая доступ к внутреннему состоянию родительской функции. Как и в предыдущем примере сmakeCounter
.
Чтобы уточнить, приведем еще один пример.
/*1*/ const add = (x) => (y) => x + y /*2*/ /*3*/ const add5 = add(5) // add5 = (y) => 5 + y /*4*/ const add10 = add(10) // add10 = (y) => 10 + y /*5*/ /*6*/ add5(1) // 6 /*7*/ add10(1) // 11
add
— это функция, которая принимает первый параметр x
и возвращает анонимную функцию, которая принимает второй параметр y
и возвращает x + y
.
Строка 3: выполнение add(5)
вернет функцию со значением 5
внутри нее. Компилятор/оптимизатор поймет это именно так:
Строка 4: точно такая же, как и строка 3. Выполнениеconst add5 = (y) => 5 + y
add(10)
вернет функцию со значением 10
внутри нее. Компилятор/Оптимизатор поймет это именно так:
Строка 6 и строка 7: это обычные вызовы функций для ранее «динамически» созданных функцийconst add10 = (y) => 10 + y
add5
и add10
.
После понимания того, что делает каждая строка, разберемся в терминологии для add
, add5
и add10
:
add
— функция высшего порядка. Почему? Потому что она возвращает функцию.- Но
add5
иadd10
являются замыканиями. Почему? Потому что они имеют значения5
и10
соответственно, заключенные (связанные) в лексической области видимости их родителя и все еще доступные им. (Вот почему, когда мы вызываемadd5(1)
, он будет использовать уже переданное5
дляadd
).
3. Каррирование
Это механизм применения концепции ленивых вычислений. Его мы подробно рассматривали в отдельной части.Итоги
Функции первого класса — это не шаблон, это особенность языка программирования. Эта возможность позволяет легко обращаться с функциями как с переменными без ограничений. Наличие этой возможности делает язык более мощным и готовым к функциональному программированию. В таком языке мы можем создавать очень мощные утилиты, такие как функции высшего порядка, замыкания, каррирование и многое другое.Где должна быть бизнес-логика в React приложении
год назад·6 мин. на чтение
В этой статье мы подробно рассмотрим работу с бизнес-логикой в React
Мы уже подробно разбирали масштабируемую структуру React приложения, то, как называть наши файлы, когда использовать хуки для управления побочными эффектами и т.д.:
В этой статье мы подробно рассмотрим работу с бизнес-логикой.
Во многих случаях разработчики пишут бизнес логику прямо в компонентах. Даже опытные разработчики ограничиваются вынесением этих вычислений в кастомные хуки или какие-либо вспомогательные функции. Но все еще это оставляет проблему нерешенной. Дело в том, что даже если у нас есть более мелкие компоненты и логика перемещена в хуки или хэлперы, они буквально разбросаны повсюду неорганизованно. Возьмем, к примеру, приложение онлайн магазина, если мы хотим изменить логику в
Также иногда бывает нужно преобразовать аббревиатуры в текст такие как VIC или NSW, но нам нужно показать их в полном тексте на странице как Victoria или New South Wales.
Единственное, что нужно изменить, это заменить
cart
, скорее всего, нам также придется изменить модули product
и validation
. И нам обычно приходится менять как хэлперы, так и представления (не говоря уже о связанных с ними тестах).
Как обстоят дела в React
Рассмотрим проблему на более высоком уровне. Если вы внимательно посмотрите на React и согласитесь, что он отвечает только за визуальную часть нашего приложения, многие проблемы будут решены автоматически. Независимо от того, используем ли мы традиционные шаблоны MVC/MVP или их вариант MVVM, если React — это V, очевидно, нам нужно что-то еще, чтобы заполнить роль M или VM в приложении. Среди многих проектов я также обнаружил, что многие хорошие практики, которые мы используем в бэкенде, не признаны в мире фронтенда, такие как слоеная структура, паттерны проектирования и т. д. Одна из возможных причин заключается в том, что фронтенд относительно молодой и ему нужно некоторое время, чтобы наверстать упущенное. Например, в типичном приложении Spring MVC у нас были быcontroller
, service
и repository
, и каждый разработчик принимает причину такого разделения: controller
не содержит бизнес-логики, service
не знает, как модель отображается или сериализуется для пользователей, а repository
работает только о доступом к данным. Однако во фронтенд-приложениях на React из-за отсутствия встроенной поддержки (например, отсутствия контроллеров или слоя репозитория) мы вместо этого пишем этот код в компоненты. И это приведет к тому, что бизнес-логика будет повсюду. Итерации станут медленными, а качество кода низким.
Утечка бизнес-логики
Мы можем назвать эту ситуацию утечкой бизнес-логики, имея в виду, что бизнес-логика должна была быть размещена в правильное место, и по какой-то причине была размещена неправильно. Хотя у нас нет подходящего механизма для правильного размещения, в результате бизнес логика написана везде где удобно (в компонентах, хуках и вспомогательных функциях). Сложно уловить такую утечку в коде. Вы должны уделять больше внимания, чтобы увидеть такие ситуации. Вот несколько распространенных симптомов, которые я обнаружил:- Использование преобразователей данных
- x.y.z
- Защитное программирование
Использование преобразователей данных
Эту паттерн легко обнаружить: если вы делаетеmap
для преобразования данных, вы, вероятно, пересекаете два ограниченных контекста (что может привести к утечке логики). Мы все видели или, возможно, писали такой код, как:
В приведенном выше фрагменте то, что возвращает бэкэнд, не совсем соответствует тому, что потребляет UI, поэтому нам нужно преобразовать полученные данные. Мы можем использовать сервис, разработанный другой командой, или использовать сторонний сервис (например, Google Search API). Таким образом, казалось бы, безобидный код нарушил здесь несколько принципов:fetch(`https://example.com/api/addresses`) .then((r) => r.json()) .then((data) => { const addresses = data.map((item: RemoteAddress) => ({ street: item.streetName, address: item.streetAddress, postcode: item.postCode })) setAddresses(addresses) });
- Компонент должен знать тип
RemoteAddress
- Компоненту необходимо определить новый тип
Address
(setAddresses
) data.map
выполняет низкоуровневое сопоставление
Симптом x.y.z (нарушение закона Деметры)
Если вы используете более одного оператора точки.
, вероятно, это означает, что отсутствуют некоторые концепции. person.deliveryAddress
лучше, чем person.primaryAddress.street.streetNumber + person.primaryAddress.suburb
так как первый вариант правильно скрывает детали.
Приведенный ниже код показывает, что ProductDialog
слишком много знает о product
, и как только структура product
изменится, нам придется менять множество мест (тесты и компоненты)
Здесь мы имеем дело с данными, а не с моделью. Таким образом,const ProductDialog = (props) => { const { product } = props; if(product.item.type === 'Portion') { //do something } }
product.isPortion()
будет более значимым, чем проверка необработанных данных.
Защитное программирование
Во многих проектах люди склонны делать слишком много в компоненте, и это создает много шума в коде. Например:Обратите внимание, что мы проверяем на null и предоставляем запасное значение в компоненте. Однако мы должны выполнять этот тип логики в специально отведенном месте.const ProductDetails = (props) => { const { product } = props const { item } = product const { media } = item as MenuItem const title = (media && media.name) || '' const description = (media && media.description) || '' return ( <div> {/* product details */} </div> ) }
Как решить проблему?
На практике мы можем попробовать двухэтапный подход к решению проблемы.- Регулярный рефакторинг
- Создание моделей
Регулярный рефакторинг
Во-первых, мы можем выполнить рефакторинг, как обычно в других случаях, когда мы видим некоторую логику в компонентах React. Например, переместив логику/вычисления из:- Использования преобразователей данных
- x.y.z
- Защитного программирования
const transformAddress: Address = (address: RemoteAddress) => { return ({ street: datum.streetName, address: datum.streetAddress, postcode: datum.postCode }) } //... const addresses = data.map(transformAddress)
Точно так же мы можем использовать функцию, для проверкиconst states = { vic: "Victoria", nsw: "New South Wales", //... }; const transformAddress: Address = (address: RemoteAddress) => { return { street: address.streetName, address: address.streetAddress, postcode: address.postCode, state: states[address.state.toLowerCase()] }; };
title
и description
и вывода запасного значения:
По мере добавления все больше и больше логики, такойconst getTitle = (media) => (media && media.name) || '' const getDescription = (media) => (media && media.description) || ''
transformAddress
и getTitle
, они будут перемещаться в helpers.ts
, в конечном итоге у нас будет огромный файл. Это означает, что он станет нечитаемым и будет иметь высокие затраты на обслуживание. Мы можем разделить файл на модули, но связи между этими функциями могут затруднить их понимание. Это похоже на проблему, с которой мы сталкивались до объектно-ориентированного программирования - у нас слишком много модулей и функций в каждом модуле, и слишком сложно ориентироваться в них. Другими словами, нам нужен лучший способ организации этих вспомогательных функций.
К счастью, нам не нужно изобретать велосипеды. Нам может помочь объектно-ориентированное программирование. Просто используя классы и инкапсуляцию в ООП, мы можем легко сгруппировать эти функции и сделать код намного более читабельным. Чтобы сгруппировать код создадим модели.
Создание моделей
Короче говоря, создание моделей — это объединение данных и поведения, сокрытие деталей и обеспечение общего API для потребителей. Например, мы не должны использоватьproduct.item.type === 'Portion'
, вместо этого мы должны создать класс Product
, и у него есть isPortion
для их потребителей. Это очень распространено в бэкенд-сервисах, но не получило широкого распространения в мире фронтенда.
Причина в том, что, как упоминалось выше, люди упускают из виду, что React отвечает только за визуализацию. И здоровое фронтенд-приложение должно иметь и другие части. Ему нужны модели и логика для взаимодействия с серверной частью, даже для ведения логирования.
Возвращаясь к приведенному выше примеру, определив класс Address
для замены анонимной функции внутри data.map
, мы получим:
Нет никакой разницы в использовании:class Address { constructor(private addr: RemoteAddress) {} get street() { return this.addr.streetAddress; } get postcode() { return this.addr.postcode; } }
const AddressLine = ({ address }: { address: Address }) => ( <li> <div className="result">{address.street}</div> </li> );
transformAddress
на new Address
:
И для частного члена/функции для перевода названия штата:const addresses = data.map((addr: RemoteAddress) => new Address(addr))
Структура теперь намного точнее.private readonly states = { vic: "Victoria", nsw: "New South Wales", //... }; get state() { return this.states[this.addr.state.toLowerCase()]; }
states
теперь является приватным членом класса Address
. Класс хорош тем, что он объединяет всю связанную логику в одну часть, что делает его изолированным и простым в обслуживании.
Размещение всей связанной логики в одном месте имеет и другие преимущества. Во-первых, такое разделение делает тестирование простым и надежным, поскольку компоненты зависят от модели (а не от исходных данных).
Нам не нужно готовить данные с нулевым значением или значения вне границ предусмотренных значений для тестов компонентов. Точно так же модель тестирования больше фокусируется на данных и логике (пустое значение, проверка и запасное значение). Во-вторых, согласованность повышает вероятность его повторного использования в других сценариях. Наконец, если нам нужно переключиться на другую стороннюю службу, нам нужно только изменить модели, и представления могут остаться нетронутыми.
По мере того, как создается все больше и больше моделей, нам может понадобиться целый слой для них. Эта часть кода не знает о существовании компонентов пользовательского интерфейса и связана исключительно с бизнес-логикой.