Парадигмы программирования. Основы объектно-ориентированного подхода
Парадигма программирования (programming paradigm) – это совокупность идей и понятий, определяющих стиль написания компьютерных программ. Это способ концептуализации, определяющий организацию вычислений и структурирование работы, выполняемой компьютером.
Первоначальный подход к программированию не основывался какой-либо методологии. Программа состояла из последовательно выполняемых команд, а также меток, условного и безусловного перехода. Современная методология программирования включает большое количество парадигм, наиболее распространенными из которых являются следующие:
- Императивное программирование (imperative programming) описывает процесс получения результатов как последовательности инструкций изменения состояния программы.
- Функциональное программирование (functional programming) рассматривает вычисление как последовательность вызовов функций без сохранения состояния приложения.
- Структурное программирование (structured programming) определяет действия программы, которые необходимо принять для достижения желаемого результата.
- Процедурное программирование (procedural programming) предусматривает функциональный подход с хранением состояния программы.
- Модульное программирование (modular programming) предусматривает разделение программы на независимые логические и физические части, которые могут самостоятельно обрабатываться.
- Компонентное программирование (component-based programming) предусматривает сохранение модульной структуры программного обеспечения при выполнении программы.
- Объектно-ориентированное программирование (ООП, object-oriented programming) организует программу как совокупность объектов (структур данных, состоящих из полей данных и методов), а также их взаимодействия.
- Прототипное программирование (prototype-based programming) – разновидность объектно-ориентированного программирования, реализованного не через классы, а через наследование объектов производится путём клонирования существующего экземпляра объекта (прототипа).
- Обобщенное программирование (generic programming) заключается в таком описании данных и алгоритмов, которое можно применять к различным типам данных, не меняя это описание.
- Событийно-ориентированное программирование (event-driven programming) – управления вычислениями определяется через события (асинхронный ввод и сообщение из других программ или потоков и т.п.).
- Метапрограммирование (metaprogramming) предусматривает создание программ, которые порождают другие программы как результат своей работы, или программ, которые изменяют себя во время выполнения.
- Декларативное программирование (declarative programming) определяет логику вычисления без описания потока управления.
Существует также много других парадигм программирования – автоматное, аспектно-ориентированное, функционально-ориентированное и т. д.
1.2 Методология императивного программирования
1.2.1 Основные концепции
Императивное программирование – это парадигма программирования, согласно которой описывается процесс получения результатов как последовательность инструкций изменения состояния программы. Чаще императивное программирование, в котором определяется необходимая последовательность действий, противопоставляют декларативному программированию, которое предусматривает определение того, что мы желаем получить. В отличие от функционального программирования, императивная парадигма предполагает наличие состояния, которое может храниться, например, с помощью глобальных переменных.
Кроме первоначального (неструктурного) подхода, к императивному программированию относят процедурное и модульное программирование. Кроме того, в пределах объектно-ориентированной методологии императивный подход используют для реализации методов классов.
Для реализации "неструктурного" подхода в языке программирования необходимо наличие следующих средств:
- описание переменных;
- последовательное выполнение утверждений, в частности присвоение переменным определенных значений;
- метки;
- безусловный переход ( goto );
- условный переход ( if. goto ).
Вследствие отсутствия в языке Java оператора goto реализация такого "неструктурного" подхода на Java невозможна.
1.2.2 Реализация структурного подхода
Структурное программирование – это парадигма, предполагающая написание программы как набора блоков. Такими блоками являются ветвления, циклы, последовательность утверждений. Благодаря наличию циклов с предусловием, постусловием и с параметром программа может полностью быть реализована без условных и безусловных переходов.
Современные языки структурного программирования поддерживают отдельную область видимости блоков (внутри блоков могут создаваться локальные переменные, константы, типы и т.д.).
Реализация структурного программирования в Java базируется на использовании следующих конструкций:
- последовательное выполнение (аналогичное неструктурному программированию);
- разветвление – условное утверждение ( if , if . else ) и переключатель ( switch );
- циклы: с предусловием ( while ), с постусловием ( do . while ), с параметром ( for );
- программный блок – одно или несколько утверждений, взятых в фигурные скобки; блок определяет свою область видимости; внутри блока можно описывать переменные и константы; блоки можно вкладывать один в другой.
1.2.3 Реализация процедурного подхода
Реализация процедурного программирования предполагает наличие в языке программирования понятия подпрограммы (процедуры, функции), определяющей собственную область видимости и возвращающей определенный результат, а также средств вызова функций с последующим использованием этого результата. При вызове подпрограммы управление передается из точки вызова в код подпрограммы, а затем возвращается в точку вызова для выполнения последующих инструкций.
Для размещения данных отдельных подпрограмм (функций) в памяти компьютера, отведенной для приложения, организуется так называемый программный стек (стек вызовов) – область памяти, организованная по принципу стека (LIFO, last in – first out, "последним пришел – первым вышел"). В программном стеке хранится информация, необходимая для возврата управления из подпрограмм в вызывающую подпрограмму (основную программу), в частности, адрес точки возврата. Кроме адресов в программном стеке могут храниться аргументы подпрограмм, локальные переменные и другие временно создаваемые данные.
В языке Java процедурный подход реализован через использование статических функций, находящихся в области видимости классов.
1.2.4 Реализация модульного подхода
Модульное программирование предполагает разделение программного кода на отдельные модули, содержащие логически связанные элементы (типы, данные, подпрограммы). На логическом уровне языки поддерживают так называемые пространства имен. Пространство имен (namespace) – поименованная часть глобальной области видимости, в которой могут содержаться объявления и определения. Пространства имен помогают избежать конфликтов имен.
На физическом уровне в качестве модулей могут выступать библиотеки, сборки, объектные модули и т. д. (в зависимости от языка программирования и программной платформы).
Для реализации модульного подхода в Java используются пакеты, обеспечивающие группировку кода как на логическом, так и на физическом уровне.
1.3 Причины возникновения и преимущества объектно-ориентированного подхода
В семидесятые годы ХХ века индустрия разработки программного обеспечения столкнулась с вызовами, обусловленными существенным повышением сложности программных систем. Возникновение диалоговых систем с механизмами поведения привело к возникновению проблем, которые не могли быть решены традиционным процедурным путем. Возможность асинхронного ввода данных не согласовывалась с концепцией программирования, управляемого данными.
Программное обеспечение является по своей сути очень сложным. Сложность программных систем часто превосходит человеческий интеллектуальный потенциал. Как утверждает один из основателей объектно-ориентированной методологии Грейди Буч, эта сложность следует из четырех элементов:
- сложность предметной области;
- сложность управления процессом разработки;
- сложность обеспечения гибкости программного обеспечения;
- сложность управления поведением дискретных систем.
Мы можем преодолеть эти проблемы с помощью декомпозиции, абстракции и иерархии. Вместо функциональной декомпозиции, на которой построено процедурное программирование, объектно-ориентированная парадигма предлагает объектную декомпозицию. Кроме того, концепция классов позволяет обеспечить необходимый уровень абстракции данных и иерархичность представления объектов.
Впервые термины "объекты" и "объектно-ориентированный" в современном смысле объектно-ориентированного программирования появились исследованиях группы искусственного интеллекта Массачусетского технологического института в конце 1950-х — начале 1960-х годов. Понятия "объект" и "экземпляр" появились в глоссарии, разработанном Иваном Сазерлендом в 1961 г. и связаны с описанием светового пера (Sketchpad).
1.4 Составляющие объектно-ориентированной методологии
Основными составляющими объектно-ориентированной методологии являются объектно-ориентированный анализ, объектно-ориентированное проектирование и объектно-ориентированное программирование.
Объектно-ориентированный анализ предполагает создание объектно-ориентированной модели предметной области. При этом речь идет не о проектировании классов программного обеспечения, а об использовании аппарата объектно-ориентированной методологии для представления реальной системы.
Объектно-ориентированное проектирование представляет собой процесс описания классов будущего программного обеспечения с использованием формальных методов (как правило, графических), а также определения взаимодействия классов и объектов. Отделение процесса проектирования от непосредственного кодирования призвано преодолеть сложность программного обеспечения за счет контроля над связями между отдельными сущностями и позволяет создавать ПО, допускающее коллективную разработку и повторное использование кода. Эффективность процесса проектирования повышается за счет использования проектных образцов (паттернов, шаблонов проектирования).
Объектно-ориентированное программирование является одной из парадигм программирования и предполагает непосредственное создание классов и объектов, а также определение связей между ними, выполняемое с использованием одного из языков объектно-ориентированного программирования.
1.5 Основные принципы и концепции объектно-ориентированной парадигмы
Основополагающим принципом объектно-ориентированного подхода является абстракция данных. Абстракция – это придание объекту характеристик, которые отличают его от всех других объектов, четко определяя его концептуальные границы. Основная идея состоит в том, чтобы отделить способ использования составных объектов от деталей их реализации в виде более простых объектов, подобно тому, как функциональная абстракция разделяет способ использования функции и деталей её реализации в терминах более примитивных функций, таким образом, данные обрабатываются функцией высокого уровня с помощью вызова функций низкого уровня.
Такой подход является основой объектно-ориентированного программирования. Это позволяет работать с объектами, не вдаваясь в особенности их реализации. В каждом конкретном случае применяется тот или иной подход: инкапсуляция, полиморфизм или наследование. Например, при необходимости обратиться к скрытым данным объекта, следует воспользоваться инкапсуляцией, создав, так называемую, функцию доступа или свойство.
Этот принцип реализуется через понятие класса. Класс — это структурированный тип данных, набор элементов данных различных типов и функций для работы с этими данными. Объект – это экземпляр класса.
Данные объекта (поля, fields, иногда — элементы данных, data members) — это переменные, характеризующие состояние объекта.
Функции объекта (методы, methods) — это функции, которые имеют непосредственный доступ к данным объекта. Иногда говорят, что методы определяют поведение объекта. В отличие от обычных (глобальных) функций, необходимо сначала создать объект и вызвать метод в контексте этого объекта.
Объекты характеризуются жизненным циклом. Создание объектов предусматривает вызов специальной функции инициализации данных — так называемого конструктора. Конструкторы вызываются непосредственно после создания объекта в памяти. Большинство языков объектно-ориентированного программирования поддерживает механизмы корректной ликвидации объектов и с применением деструкторов. Деструктор — это специальная функция, которая вызывается непосредственно перед удалением объекта и освобождает системные ресурсы, которые были заняты в процессе создания и функционирования объекта.
Можно назвать три основных понятия, лежащих в основе объектно-ориентированного программирования. Это инкапсуляция, наследование и полиморфизм.
Инкапсуляция (сокрытие данных) — одна из трех парадигм объектно-ориентированного программирования. Содержание инкапсуляции заключается в сокрытии от внешнего пользователя деталей реализации объекта. В том числе доступ к данным (полям), которые обычно описаны с модификатором private, осуществляется через открытые функции доступа. В UML перед элементами класса (атрибутами и операциями) можно указать из видимости с помощью символов + (открытый), — (закрытый) и # (защищенный).
Наследование — это механизм создания производных классов от базовых. Создание производного класса предусматривает расширение путем добавления новых полей (атрибутов) и методов. В C++ есть так называемые закрытое и защищенное наследование. Эти формы наследования позволяют ограничить доступ к элементам базовых классов извне класса. В большинстве языков объектно-ориентированного программирования поддерживается только открытое наследование – элементы при наследовании сохраняют свою видимость. При этом закрытые элементы наследуются, но становятся недоступными для непосредственного обращения в производных классах.
Полиморфизм — это свойство классов, согласно которой поведение объектов может определяться на этапе компиляции, а на этапе выполнения. Классы, декларирующие идентичный набор функций, но реализованные под конкретные специфические требования, имеют название полиморфных классов.
Подключение тела функции к точке ее вызова называется связыванием. Если оно происходит до начала выполнения программы, речь идет о раннем связывании. Этот тип связывания присущ языкам процедурного типа, таким как C или Pascal. Позднее связывание означает, что подключение происходит во время выполнения программы и в объектно-ориентированных языках зависит от типов объектов. Позднее связывание еще называют динамическим, или связыванием времени выполнения. Для реализации полиморфизма используется механизм позднего связывания.
В языках объектно-ориентированного программирования позднее связывание реализуется через механизм виртуальных функций. Виртуальная функция (виртуальный метод, virtual method) — это функция, определенная в базовом классе, и перекрытая в походных, так, что конкретная реализация функции для вызова будет определяться во время выполнения программы. Выбор реализации виртуальной функции зависит от реального (а не объявленного при описании) типа объекта. Таким образом, поведение ранее созданных классов может быть изменено позже путем перекрытия виртуальных методов. Фактически полиморфными являются классы, которые содержат виртуальные функции.
Некоторые языки (например, C++) поддерживают так называемый полиморфизм времени компиляции. Несмотря на то, что поведение объектов определяется при компиляции, использование шаблонов позволяет определить общее поведение шаблонных классов и специфицировать это поведение для конкретных типов.
С объектно-ориентированной парадигмой тесно связана концепция программирования, управляемого событиями, в рамках которого общая организация программы предполагает создание и регистрацию объектов с последующим получением и обработкой асинхронных событий и обмена сообщениями между объектами.
1.6 Унифицированный язык моделирования
1.6.1 Общие сведения
Унифицированный язык моделирования (Unified Modeling Language, UML) — это графическая нотация для определения, описания, проектирования и документирования программных систем, бизнес-систем и других систем различной природы, в первую очередь связанных с программным обеспечением. UML включает ряд диаграмм для моделирования и проектирования сложных систем. Авторы языка – три видных исследователя в области объектно-ориентированного анализа и проектирования Грэйди Буч (Grady Booch), Джеймс Рамбо (James Rumbaugh) и Ивар Джекобсон (Ivar Jacobson). Графические элементы, определенные в UML, представлены на диаграммах, которые отражают различную точку зрения на моделируемую систему.
Первая версия UML была принята консорциумом OMG (Object Management Group) в январе 1997 года в качестве международного стандарта. Утвержденная же в сентябре версия UML 1.1 была принята на вооружение основными компаниями – производителями программного обеспечения, такими, как Microsoft, IBM, Hewlett-Packard и производителями CASE-средств, которые реализовали поддержку UML в своих программных продуктах.
Авторы и разработчики UML представляют его как язык для определения, представления, проектирования и документирования программных систем, бизнес-систем и других систем различной природы. UML определяет нотацию, представляющую собой совокупность графических объектов, которые используются в моделях. Универсальный язык объектного моделирования UML не зависит от языков программирования и, вследствие этого, может поддерживать любой объектно-ориентированный язык программирования.
Формальная спецификация последней версии UML 2.0 опубликована в августе 2005 года.
1.6.2 Конструктивные блоки UML
Сущности являются основой модели. Привязку сущностей друг к другу обеспечивают отношения, а диаграммы группируют наборы сущностей. В UML представлены четыре типа сущностей: структурные (structural), поведенческие (behavioral), группирующие (group), аннотационные (annotational).
Графическое изображение отдельных структурных и поведенческих сущностей, принятое в UML, приводится ниже в связи с диаграммами, на которых эти сущности чаще всего изображаются. Вместе с тем, большинство элементов может присутствовать практически на всех диаграммах.
Единственным представителем группирующей сущности является пакет (package). Пакет – это механизм общего назначения для организации элементов в виде единой группы. Структурные, поведенческие и даже другие группирующие сущности могут быть помещены внутрь пакета.
Аннотации являются поясняющей и комментирующей частью UML. Единственным типом аннотационной сущности является примечание (note). Аннотация соединяется пунктирной линией с сущностью, к которой она относится:
Отношения являются основными связующими строительными блоками в UML. Имеется 4 типа отношений:
- зависимость (dependency);
- ассоциация (association); ее разновидностью является агрегирование (aggregation);
- обобщение (generalization);
- реализация (realization).
Третьим компонентом UML являются диаграммы.
Диаграмма в UML — это графическое представление набора элементов, изображаемое в виде связанного графа с вершинами (сущностями) и ребрами (отношениями).
UML 1 предлагает следующий набор диаграмм для моделирования:
- структурные диаграммы (structure diagrams):
- диаграммы вариантов использования (use case diagrams) – для моделирования бизнес-процессов организации (требований к системе);
- диаграммы классов (class diagrams) – для моделирования статической структуры классов системы и связей между ними;
- диаграммы объектов (object diagrams) – для моделирования статической структуры экземпляров классов (объектов) и связей между ними;
- диаграммы взаимодействия (interaction diagrams):
- диаграммы последовательности (sequence diagrams) и диаграммы коммуникаций (communication diagrams) – для моделирования процесса обмена сообщениями между объектами;
- диаграммы компонентов (component diagrams) – для моделирования иерархии компонентов (подсистем) системы;
- диаграммы размещения (deployment diagrams) – для моделирования физической архитектуры системы.
UML 2 дополнительно предлагает следующие диаграммы:
- диаграммы пакетов (package diagrams) – для отображения зависимостей между пакетами, составляющими модель;
- диаграммы профилей (profile diagrams) – используются для визуализации механизмов расширения;
- диаграммы композитной структуры (composіte structure diagrams) – для моделирования внутренней структуры сущности;
- диаграммы обзора взаимодействия (іnteractіon overview diagrams) – диаграммы деятельности, узлами которых выступают диаграммы взаимодействия;
- диаграммы отображения во времени (tіmіng diagrams), или диаграммы синхронизации, используются для учета изменения состояния или значений параметров элементов при функционировании;
1.6.4 Диаграммы классов
Унифицированный язык моделирования (UML) предлагает графическое обозначение для класса в виде прямоугольника, разделенного, как минимум, на три части:
В приведенном примере Window – имя класса, атрибутам соответствуют поля (свойства) класса. Операциям соответствуют методы. Можно также указать типы атрибуты, типы параметров и результата операций:
Атрибуты определяют данные, описывающие экземпляр класса и определяют его состояние. В объектно-ориентированном программировании атрибуты отображаются в поля или элементы данных. Полный формат атрибута имеет следующий вид:
Видимость может быть следующей:
Можно использовать сокращенный формат – без видимости, без типа, без начального значения, или вообще только имя.
Множественность может быть определена в квадратных скобках сразу после имени атрибута:
Операции определяют действия, которые можно применить к классу или его объекта. Операция описывается уровнем доступа (видимости), списком параметров и типом результата:
Параметры в списке отделяют друг от друга запятыми. Каждый параметр имеет следующий формат:
Можно опустить все части спецификации операции, за исключением имени и круглых скобок.
Операции класса (статические) выделяются подчеркиванием.
UML 2 позволяет в случае необходимости добавлять четвертую часть к изображению класса. В этой части располагают пиктограммы вложенных классов.
Агрегирование – это специальная форма ассоциации, которая показывает, что сущность (экземпляр) содержится в другой сущности или не может быть создана и существовать без всеохватывающей сущности. Иногда используется более жесткая форма – композиция, которая предусматривает, что сущность полностью содержится в другой сущности. Агрегирование и композиция позволяют указывать множественность.
Отношение типа "зависимость" на диаграмме классов указывает, что зависимый класс пользуется функциональностью другого класса. На уровне языка программирования это может быть реализовано через вызов статических функций другого класса или создание временного объекта.
В UML для представления наследования используется связь типа обобщения (generalization). Стрелка специального вида ведет от более конкретного класса (производного) к более общему (базовому):
Для представления полиморфизма используются абстрактные классы и операции. Их имена записываются курсивом.
Для надписей на диаграммах классов можно использовать любой язык, но в тех случаях, когда диаграмма классов используется для генерации исходного кода, следует употреблять латинские буквы и преимущественно английскую мнемонику.
1.7 Языки объектно-ориентированного программирования
Первым языком программирования, в котором были предложены принципы объектной ориентированности, была Симула, который разработали Оле-Йохан Даль и Кристен Нюгорд в Норвежском вычислительного центре. В момент своего появления (в 1967 году), язык Симула предложил концепции объектов, классов, виртуальных методов и др., однако все это использовалось только в контексте задач имитационного моделирования. Впоследствии эти концепции были развиты Аланом Кэйем и Дэном Ингаллсом в языке Smalltalk. Именно он стал первым широко распространённым объектно-ориентированным языком программирования.
Язык программирования С++ был создан в 1983 году доктором Бьерном Страуструпом. Object Pascal появился в 1989 году, язык программирования Java был разработан в 1995 году. Существуют также C#, ADA, Oberon, Eiffel, Ruby, Python, Visual Basic, Modula-3, Object REXX и многие другие. Объектно-ориентированные средства присутствуют также в современных версиях PHP и JavaScript.
1.8 Прототипное программирование
Прототипное программирование – это вариант реализации объектно-ориентированного программирования, при котором вместо использования классов наследование производится путем клонирования существующего экземпляра объекта (прототипа). Каноническим примером прототип-ориентированного языка является язык Self. В дальнейшем этот стиль программирования был положен в основу таких языков программирования, как JavaScript, Lua, Io, REBOL и др.
Сторонники прототипного программирования считают, что этот подход обеспечивает лучший контроль над объектами и связан с меньшими накладными расходами, чем традиционный объектно-ориентированный подход, основанный на использовании классов.
Справочник современных концепций JavaScript: часть 1
Основы функционального программирования, реактивного программирования и функционального реактивного программирования на JavaScript
Кратко: В первой части серии «Справочник современных концепций JavaScript» мы познакомимся с функциональным программированием (FP), реактивным программированием (RP) и функциональным реактивным программированием (FRP). Для этого мы узнаем о чистоте, состоянии и его отсутствии, неизменяемости и изменяемости, императивном и декларативном программировании, функциях высшего порядка, observables и парадигмах FP, RP, FRP.
Введение
За последние годы JavaScript сильно развился и не думает на этом останавливаться. Однако множество концепций, рождающихся в JS-блогах и документациях, так и остаются невостребованными широким кругом разработчиков интерфейсов. В настоящей серии статей мы изучим концепции, требующие как среднего, так и продвинутого уровней подготовки, а также рассмотрим, как они вписываются в современный JavaScript.
Концепции
В этой статье мы рассмотрим концепции, имеющие решающее значение для понимания функционального программирования, реактивного программирования и функционального реактивного программирования и их использования с JavaScript:
- Чистота: чистые функции, нечистые функции, побочные эффекты
- Состояние: с ним и без него
- Неизменяемость и изменяемость
- Императивное и декларативное программирование
- Функции высшего порядка
- Функциональное программирование
- Observables: горячие и холодные
- Реактивное программирование
- Функциональное реактивное программирование
Чистота
Чистые функции
Возвращаемое значение чистой функции зависит только от ее входных данных (аргументов) и не влечет никаких побочных эффектов. С одним и тем же входящим аргументом результат всегда будет одинаковый. Пример:
Функция half(x) принимает число x и возвращает значение половины x . Если мы передадим этой функции аргумент 8 , она вернет 4 . После вызова чистая функция всегда может быть заменена результатом своей работы. Например, мы могли бы заменить half(8) на 4 : где бы эта функция не использовалась в нашем коде, подмена никак не повлияла бы на конечный результат. Это называется ссылочной прозрачностью.
Чистые функции зависят только от того, что им передано. Например, чистая функция не может ссылаться на переменные из области видимости родителя, если они явно не передаются в нее в качестве аргументов. Но и даже тогда функция не может изменять что-либо в родительской области видимости.
- Чистые функции должны принимать аргументы.
- Одни и те же входные данные (аргументы) всегда произведут одинаковые выходные данные (вернут одинаковый результат).
- Чистые функции основываются только на внутреннем состоянии и не изменяют внешнее (примечание:console.logизменяет глобальное состояние).
- Чистые функции не производят побочных эффектов.
- Чистые функции не могут вызывать нечистые функции.
Нечистые функции
Нечистая функция изменяет состояние вне своей области видимости. Любые функции с побочными эффектами (см. далее) — нечистые, ровно как и процедурные функции без возвращаемого значения.
Рассмотрим следующие примеры:
Побочные эффекты в JavaScript
Когда функция или выражение изменяет состояние вне своего контекста, результат является побочным эффектом. Примеры побочных эффектов: вызов API, манипулирование DOM, вывод alert, запись в базу данных и так далее. Если функция производит побочные эффекты, она считается нечистой. Функции, вызывающие побочные эффекты, менее предсказуемы и их труднее тестировать, поскольку они приводят к изменениям вне их локальной области видимости.
Подводя итог: чистота
Много качественного кода состоит из нечистых функций, процедурно вызывающихся чистыми. Это все равно несет массу преимуществ для тестирования и неизменяемости. Ссылочная прозрачность также обладает удобством для мемоизации: кэширование и сохранение результатов вызова функций, а затем переиспользование кэшированных результатов. Однако определить, когда функции действительно чисты, может быть непросто.
Дополнительную информацию о чистоте можно найти на следующих ресурсах:
Состояние
Состояние — информация, к которой программа имеет доступ и с которой может работать в определенный момент времени. Сюда входят данные, хранящиеся в памяти, порты ввода/вывода, базы данных и так далее. Например, содержимое переменных в приложении в любой данный момент времени репрезентативно для состояния приложения.
С состоянием
Программы, приложения или компоненты с состоянием хранят в памяти данные о текущем состоянии. Они могут изменять состояние, а также имеют доступ к его истории. Следующий пример демонстрирует это:
Без состояния
Функции или компоненты без состояния выполняют задачи, словно каждый раз их запускают впервые. Они не ссылаются или не используют в своем исполнении раннее созданные данные. Отсутствие состояния обеспечивает ссылочную прозрачность. Функции зависят только от их аргументов и не имеют доступа, не нуждаются в знании чего-либо вне их области видимости. Чистые функции не имеют состояния. Пример:
Приложения без состояния все еще управляют состоянием. Однако они возвращают свое текущее состояние без изменения предыдущего состояния. Это принцип функционального программирования.
Подводя итог: состояние
Управление состоянием важно для любого сложного приложения. Функции или компоненты с состоянием изменяют состояние и его историю, их труднее тестировать и отлаживать. Функции без состояния полагаются только на свои входные данные для создания данных выходных. Программа без состояния возвращает новое состояние, а не модифицирует существующее состояние.
Дополнительную информацию о состоянии можно найти на следующих ресурсах:
Неизменяемость и изменяемость
Концепции неизменяемости и изменяемости более туманны в JavaScript, чем в некоторых других языках программирования. Тем не менее, вы много услышите о неизменяемости при чтении о функциональном программировании в JS. Важно знать, что эти термины означают в классическом понимании, и как они реализуются в JavaScript. Определения достаточно просты:
Неизменяемый
Если объект является неизменяемым, его значение не может быть изменено после создания.
Изменяемый
Если объект изменяем, его значение может быть изменено после создания.
Реализация: неизменяемость и изменяемость в JavaScript
В JavaScript строки и числовые литералы реализованы неизменяемыми. Это легко понять, если рассмотреть, как мы работаем с ними:
Используя метод .substring() на нашем Hello! , строка не изменяет исходную строку. Вместо этого она создает новую строку. Мы могли бы переопределить значение переменной str на что-то другое, но, как только мы создали нашу строку Hello! , она навсегда останется Hello! .
Числовые литералы также неизменяемы. Следующий пример всегда будет иметь одинаковый результат:
Ни при каких обстоятельствах 1 + 2 не может стать чем-либо, кроме 3 .
Это демонстрирует, что в JavaScript присутствует реализация неизменяемости. Однако разработчики JS знают, что язык позволяет изменить многое. Например, объекты и массивы изменяемы. Рассмотрим следующий пример:
В этих примерах исходные объекты изменены. Новые объекты не возвращаются.
Чтобы узнать больше об изменяемости в других языках, ознакомьтесь с Изменяемые и неизменяемые объекты.
На практике: изменяемость в JavaScript
Функциональное программирование в JavaScript хорошо развивается. Но по своей сущности JS — очень изменчивый язык, состоящий из множества парадигм. Ключевая особенность функционального программирования — неизменяемость. Другие функциональные языки выбросят ошибку, когда разработчик попытается изменить неизменяемый объект. Тогда как мы можем примирить врожденную изменяемость JS при написании функционального или функционального реактивного JS?
Когда мы говорим о функциональном программировании в JS, слово «неизменяемое» используется много, но разработчик обязан всегда держать ее в голове. Например, Redux полагается на одно неизменяемое дерево состояний. Однако сам JavaScript способен изменять объект состояния. Чтобы реализовать неизменяемое дерево состояний, нам нужно каждый раз при изменении состояния возвращать новый объект состояния.
Для неизменяемости объекты JavaScript также могут быть заморожены с помощью Object.freeze(obj) . Обратите внимание, что это "неглубокая" заморозка — значения объектов внутри замороженного объекта все еще могут быть изменены. Для гарантированной неизменяемости такие функции "глубокой" заморозки, как Mozilla deepFreeze() и npm deep-freeze могут рекурсивно замораживать объекты. Замораживание наиболее применимо в тестах, а не в приложении. Тесты будут оповещать разработчиков о возникновении изменений, чтобы их можно было исправить, и избежать загромождающего Object.freeze в основном коде.
Существуют также библиотеки, поддерживающие неизменяемость в JS. Mori предоставляет постоянные структуры данных на основе Clojure. Immutable.js от Facebook также предоставляет неизменяемые коллекции для JS. Библиотеки утилит, такие как Underscore.js и lodash, предоставляют методы и модули для более функционального стиля программирования (а стало быть направленного на неизменяемость).
Подводя итог: неизменяемость и изменяемость
В целом, JavaScript — язык с сильной изменяемостью. Некоторые стили JS-кодирования опираются на эту врожденную изменяемость. Однако, при написании функционального JS, реализация неизменяемости требует внимательности. Если вы что-то нечаянно модифицируете, JS не будет выбрасывать ошибки. Тестирование и библиотеки могут помочь, но работа с неизменяемостью в JS требует практики и методологии.
Неизменяемость имеет свои преимущества. В результате получается код, который проще понимать. Он также обеспечивает персистентность, возможность хранения более старых версий структур данных и копирование только изменившихся частей.
Недостатком неизменяемости является то, что многие алгоритмы и операции не могут быть эффективно реализованы.
Дополнительную информацию о неизменяемости и изменяемости можно найти на следующих ресурсах:
Императивное и декларативное программирование
Хотя некоторые языки были разработаны как императивные (C, PHP) или декларативные (SQL, HTML), JavaScript и другие (такие как Java и C# могут поддерживать обе парадигмы программирования.
Большинство разработчиков, знакомых с даже самым простым JavaScript, писали императивный код: инструкции, информирующие компьютер как достичь желаемого результата. Если вы использовали цикл for , вы писали императивный JS.
Декларативный код сообщает компьютеру, что вы хотите достичь, а не как, и компьютер сам заботится о том, как достичь конечного результата без явного описания этого разработчиком. Если вы использовали Array.map , вы писали декларативный JS.
Императивное программирование
Императивное программирование описывает логику работы программы в явных командах с операторами, изменяющими состояние программы.
Рассмотрим функцию, увеличивающую каждое число в массиве целых чисел. Императивным примером в JavaScript может быть:
Эта функция описывает логику работы как: мы выполняем обход массива и явно увеличиваем каждое число, помещая его в новый массив. Затем мы возвращаем результирующий массив. Это пошаговое описание логики функции.
Декларативное программирование
Декларативное программирование описывает, что выполняет логика программы без описания того, как это сделать.
Очень простой пример декларативного программирования может быть продемонстрирован с помощью SQL. Мы можем запросить таблицу базы данных ( People ) для людей с фамилией Smith следующим образом:
Этот код легко читается и описывает то, что мы хотим достичь. Нет описания того, как результат должен быть достигнут. Компьютер об этом позаботится сам.
Теперь рассмотрим функцию incrementArray() , которую мы императивно реализовали выше. Давайте сейчас реализуем это декларативно:
Мы показываем, что хотим достичь, но не как это работает. Метод Array.map() возвращает новый массив с результатами выполнения обратного вызова для каждого элемента из переданного массива. Этот подход не изменяет существующие значения и не включает в себя последовательную логику, раскрывающую, как он создает новый массив.
Примечание: в JavaScript map, reduce и filter — декларативные, функциональные методы массивов. Библиотеки утилит, такие как lodash, предоставляют такие методы takeWhile, uniq, zip и другие в дополнение к map , reduce и filter .
Подводя итог: императивное и декларативное программирование
JavaScript допускает как парадигмы императивного, так и декларативного программирования. Большая часть кода JS, который мы читаем и пишем, императивная. Однако, с ростом популярности функционального программирования в JS, декларативные подходы распространяются все больше.
Декларативное программирование имеет очевидные преимущества в отношении краткости и читаемости, но в тоже время может походить на магию. Многие новички в JavaScript могут на базе накопленного опыта писать императивный JS перед глубоким погружением в декларативное программирование.
Дополнительную информацию об императивном и декларативном программировании можно найти на следующих ресурсах:
Функции высшего порядка
Функция высшего порядка — это функция, которая принимает другую функцию в качестве аргумента или возвращает функцию в результате.
В JavaScript функции являются объектами первого класса. Они могут храниться и передаваться как значения: мы можем присвоить функцию переменной или передать функцию другой функции.
Обратный вызов — один из примеров использования функции в качестве аргумента. Обратные вызовы могут быть встроенными анонимными функциями или именованными функциями:
Мы также можем передать функцию в качестве аргумента любой другой созданной нами функции, а затем выполнить этот аргумент:
Примечание: при передаче именованной функции в качестве аргумента, как и в двух приведенных выше примерах, мы не используем круглые скобки () . Таким образом мы передаем функцию как объект. Круглые скобки выполняют функцию и передают результат вместо самой функции.
Функции высшего порядка также могут возвращать другую функцию:
Подводя итог: функции высшего порядка
Природа JavaScript функций как объектов первого класса делает их основой функционального программирования в JS.
Дополнительную информацию о функциях высшего порядка можно найти на следующих ресурсах:
Функциональное программирование
Теперь мы узнали о чистоте, отсутствии состояния, неизменяемости, декларативном программировании и функциях высшего порядка. Все эти концепции важны для понимания парадигмы функционального программирования.
На практике: функциональное программирование на JavaScript
Функциональное программирование охватывает приведенные выше концепции следующими способами:
- Основные функции реализованы с использованием чистых функций без побочных эффектов.
- Данные неизменяемы.
- Функциональные программы не имеют состояния.
- Императивный код контейнера управляет побочными эффектами и выполняет декларативный, чистый код ядра.
Примечание: если бы мы попытались написать JavaScript веб-приложение, состоящее только из чистых функций без побочных эффектов, оно не смогло бы взаимодействовать с окружением и поэтому не было бы особенно полезным.
Давайте рассмотрим пример. Скажем, у нас есть текст, и мы хотим посчитать количество слов в нем. Мы также хотим найти ключевые слова длиной более пяти символов. Используя функциональное программирование, наш результирующий код может выглядеть примерно так:
Код доступен для запуска здесь — JSFiddle: Functional Programming with JavaScript. Он разбит на понятные, декларативные функции с четким назначением. Если мы пройдем по нему и прочитаем комментарии, то никаких дополнительных разъяснений кода не потребуется. Каждая функция ядра — модульная и зависит только от ее входных данных (чистая). Последняя функция обрабатывает ядро для генерации общих выходных данных. Функция processCopy() — нечистый контейнер, выполняющий ядро и управляющий побочными эффектами. Мы использовали функцию высшего порядка, принимающую другие функции в качестве аргументов для поддержания функционального стиля.
Подводя итог: функциональное программирование
Неизменяемость данных и отсутствие состояния гарантируют, что состояние программы не изменяется. Вместо этого возвращаются новые значения. Чистые функции используются для функциональности ядра. Чтобы запустить программу и обработать необходимые побочные эффекты, нечистые функции могут императивно вызывать чистые.
Дополнительную информацию о функциональном программировании можно найти на следующих ресурсах:
Observables
Observables похожи на массивы, за исключением того, что элементы не хранятся в памяти, а поступают асинхронно с течением времени (потоки). Мы можем подписаться на observables и реагировать на их события. JavaScript оbservables — это реализация шаблона observer. Реактивные расширения (обычно называемые с префиксом Rx) предоставляют observables для JS через RxJS.
Для демонстрации концепции observables давайте рассмотрим простой пример: изменение размера окна браузера. В этом контексте observables максимально понятны. Изменение размера окна браузера испускает поток событий в течение определенного периода времени (пока окно принимает нужный размер). Мы можем создать observable и подписаться на него, чтобы реагировать на поток событий изменения размера:
Пример кода выше демонстрирует, что, по мере изменения размера окна, мы можем ограничивать observable поток и подписаться на его изменения, чтобы реагировать на новые значения в коллекции. Это пример горячего observable.
Горячие observable
Пользовательские интерфейсные события (нажатие кнопки, перемещения мыши и т.д.) — горячие. Горячие observable всегда будут срабатывать, даже если мы специально не реагируем на них подпиской. Пример изменения размера окна выше — горячий observable: resize$ observable отрабатывает вне зависимости от того, существует подписка или нет.
Холодные observable
Холодные observable срабатывают только тогда, когда мы подписываемся на него. Если мы подпишимся снова, он начнет все сначала.
Давайте создадим observable массив чисел от 1 до 5 :
Мы можем подписаться на только что созданный source$ observable. После подписки, значения последовательно посылаются к observer. Обратный вызов onNext логирует значения: Next: 1 , Next: 2 , и т.д. до завершения: Completed! . Созданный нами холодный observable source$ не отрабатывает до того, пока мы не подпишемся на него.
Подводя итог: observables
Observables являются потоками. Мы можем наблюдать любой поток: от событий изменения размеров в существующих массивах до ответов API. Мы можем создать observables практически из всего, что угодно. Promise — тот же observable, но отдающий только одно значение, а observables могут возвращать много значений с течением времени.
Мы можем работать с observables по-разному. RxJS использует множество операторов. Observables часто визуализируется с помощью точек на линии, как показано на сайте RxMarbles. Поскольку поток состоит из асинхронных событий с течением времени, легко осмыслять это линейным способом и использовать именно такие зрительные образы, чтобы понять реактивные операторы. Например, следующие изображение от RxMarbles иллюстрирует оператор фильтра:
Дополнительную информацию об observables можно найти на следующих ресурсах:
Реактивное программирование
Реактивное программирование связано с декларативным (что делать, а не как) наблюдением и реагированием на поступающие события во времени.
Реактивное программирование часто связано с Reactive Extensions, API для асинхронного программирования с observable потоками. Реактивные расширения (с префиксом Rx) предоставляют библиотеки для различных языков, включая JavaScript (RxJS).
На практике: реактивное программирование на JavaScript
Вот пример реактивного программирования с observables. Предположим, у нас есть вход, где пользователь может ввести шестизначный код подтверждения, и мы хотим вывести его, когда будет введено последнее число. Наш HTML может выглядеть так:
Мы будем использовать RxJS и для реализации нашей функциональности создадим поток входных событий, вот так:
Этот код можно запустить на JSFiddle: Reactive Programming with JavaScript. Мы наблюдаем события из элемента confCodeInput . Затем мы используем оператор map для получения значения из каждого входного события. Далее мы фильтруем любые результаты, не являющиеся шестью символами, чтобы они не появлялись в возвращенном потоке. Наконец, мы подписываемся на наш confCodes$ observable и выводим подошедший вариант. Обратите внимание, что это произошло в ответ на событие, декларативно, — это суть реактивного программирования.
Подводя итог: реактивное программирование
Парадигма реактивного программирования включает наблюдение и реагирование на события в асинхронных потоках данных. RxJS используется в Angular и набирает популярность как решение JavaScript для реактивного программирования.
Дополнительную информацию о реактивном программировании можно найти на следующих ресурсах:
Функциональное реактивное программирование
Говоря простыми словами, функциональное реактивное программирование можно свести к декларативному реагированию на события или поведение с течением времени. Чтобы понять принципы FRP более подробно, давайте взглянем на формулировку FRP. Затем мы рассмотрим его использование применительно к JavaScript.
Что такое функциональное реактивное программирование?
Более полное определение от родоначальника FRP Конала Эллиота звучит так: функциональное реактивное программирование — денотативное и продолжительное во времени. Эллиот предпочитает описывать эту парадигму программирования как денотативное программирование с продолжительностью во времени, а не «функциональное реактивное программирование».
Функциональное реактивное программирование, в его самом простом, оригинальном определении, имеет два фундаментальных свойства:
- денотатив: значение каждой функции или типа является точным, простым и независимым от реализации (функциональная часть)
- продолжительно во времени: переменные имеют конкретное значение короткий промежуток времени: между любыми двумя точками есть бесконечное число других точек; обеспечивает гибкость трансформации, эффективность, модульность и точность (реактивная часть)
Чтобы понять продолжительность во времени, рассмотрим аналогию с использованием векторной графики. Векторная графика имеет бесконечное разрешение. В отличие от растровой графики (дискретное разрешение) векторная графика масштабируется безгранично, она никогда не перерисовывается или становится нечеткой. «Выражения FRP описывают целые эволюции значений во времени, представляя эти эволюции непосредственно как значения первого класса», — Конал Эллиот.
Функциональное реактивное программирование должно быть:
- динамическим: может реагировать во времени или на изменение входных данных
- изменяющимся во времени: реактивное поведение может изменяться с продолжительностью во времени, а реактивные значения изменяются дискретно
- эффективным: минимизировать объем обработки, необходимый при изменении входных данных
- осведомленным о прошлом: чистые функции сопоставляют предыдущее значение состояния со следующим; изменения состояния относятся к локальному элементу, а не к глобальному состоянию программы
Здесь можно рассмотреть слайды Колала Эллиота о cущности и происхождении FRP. Язык программирования Haskell поддается истинному FRP благодаря своей функциональной, чистой и ленивой природе. Эван Кзаплиски, создатель Elm, дает большой обзор FRP в своем выступлении «Управление временем и пространством: понимание многих подходов FRP».
В самом деле, давайте коротко поговорим об Elm Эвана Кзаплиски. Elm — это функциональный, типизированный язык для создания веб-приложений. Он компилируется в JavaScript, CSS и HTML. Архитектура Elm послужила вдохновением для контейнера состояния Redux приложений JS. Первоначально Elm позиционировался истинным функционально реактивным языком программирования, но начиная с версии 0.17 он реализовывал подписки вместо сигналов в интересах облегчения изучения и использования языка. На этом Elm простился с FRP.
На практике: функциональное реактивное программирование на JavaScript
Традиционное определение FRP может быть трудным для понимания, особенно для разработчиков, не имеющих опыта работы с такими языками, как Haskell или Elm. Однако этот термин чаще всего появляется в интерфейсной экосистеме, поэтому давайте проясним его применение в JavaScript.
Для согласования всего, что вы, возможно, читали о FRP в JS, важно понять, что Rx, Bacon.js, Angular и другие не согласуются с двумя основными принципами определения FRP Конала Эллиота. Эллиот заявляет, что Rx и Bacon.js не являются FRP. Вместо этого они «композиционные системы событий, вдохновленные FRP».
Функциональное реактивное программирование в своей реализации в JavaScript относится к программированию в функциональном стиле при создании и реагировании на потоки. Это довольно далеко от оригинальной формулировки Эллиота (которая специально исключает потоки как компонент), но тем не менее вдохновляется традиционными FRP.
Также очень важно понять, что JavaScript по сути взаимодействует с пользователем и пользовательским интерфейсом, DOM и часто с базой данных. Побочные эффекты и императивный код де факто являются для него стандартом, даже при использовании функционального или функционального реактивного подхода. Без императивного или нечистого кода веб-приложение JS с пользовательским интерфейсом не было бы очень полезным, поскольку оно не могло бы взаимодействовать со своей средой.
Давайте взглянем на пример, чтобы продемонстрировать основные принципы FRP-вдохновленного JavaScript. Этот пример использует RxJS и печатает движения мыши в течение десяти секунд:
Вы можете проверить этот код в действии в JSFiddle: FRP-вдохновленном JavaScript. Запустите скрипт и, пока идет подсчет до 10, наведите указатель мыши в экран с результатом. Вы должны увидеть координаты мыши вместе со счетчиком. Тогда на экран выведется, где была ваша мышь во время каждого 1-секундного интервала времени.
Давайте кратко обсудим эту реализацию шаг за шагом.
Сначала мы создаем observable time$ . Это таймер, добавляющий значение в коллекцию каждые 1000 миллисекунд (каждую секунду). Нам нужно обработать событие таймера, чтобы извлечь его значение и добавить к результирующему потоку.
Затем мы создаем move$ observable из события document.mousemove . Движение мыши продолжительно во времени. В любой точке последовательности существует бесконечное количество точек между ними. Мы ограничиваем эту бесконечность, так что результирующий поток становится более управляемым. Затем мы обрабатываем событие и возвращаем объект с значениями x и y , чтобы сохранить координаты мыши.
Затем мы хотим объединить потоки time$ и move$ . Для этого используем оператор объединения. Так мы можем определить, какие движения мыши произошли в течение каждого интервала времени. Мы будем называть результирующий observable source$ . Мы также ограничим observable source$ так, чтобы он завершился через десять секунд ( 10000 миллисекунд ).
Теперь, когда у нас есть объединенный поток времени и движения, мы создаем подписку на observable source$ , чтобы мы могли реагировать на него. В обратном вызове onNext мы проверяем, является ли значение числом или нет. Если это так, мы вызываем функцию createTimeset() . Если это объект координат, вызываем addPoint() . В обратных вызовах onError и onCompleted мы просто логируем информацию.
Давайте рассмотрим функцию createTimeset(n) . Мы создаем новый элемент div для каждого второго интервала, помечаем его и добавляем в DOM.
В функции addPoint(pointObj) мы выводим в div последние координаты в последнем временном интервале. Это связывает каждый набор координат с соответствующим временным интервалом. Теперь мы можем увидеть, где мышь находилась в конкретный момент времени.
Примечание: эти функции нечистые: у них нет возвращаемого значения и они производят побочные эффекты. Побочные эффекты — манипуляции DOM. Как упоминалось ранее, JavaScript, который мы пишем для наших приложений, часто взаимодействует с областью видимости вне его функций.
Подводя итог: функциональное реактивное программирование
FRP представляет собой написание действий, которые, используя чистые функции, реагируют на события и переводят состояние с предыдущего момента времени к следующему. FRP в реализации JavaScript не придерживается двух основных принципов FRP Конала Эллиота, но в абстрагировании от оригинальной концепции есть определенный смысл. JavaScript сильно зависит от побочных эффектов и императивного программирования, но мы, безусловно, можем использовать преимущества концепций FRP для улучшения нашего JS.
Наконец, рассмотрим эту цитату из первого издания Eloquent JavaScript: «Fu-Tzu написал небольшую программу, использующую глобальное состояние и сомнительные переплетения, и, прочитав ее, студент спросил:« Вы предупреждали нас против этих методов, но я нахожу их в вашей программе. Как такое могло случиться?». Фу-Цзы ответил: «Нет необходимости забирать водяной шланг, когда дом не горит». Это не следует рассматривать как поощрение неаккуратного программирования, а скорее как предупреждение против невротического соблюдения эмпирических правил .
Дополнительную информацию о функциональном реактивном программировании можно найти на следующих ресурсах:
Заключение
Мы закончим еще одной отличной цитатой из первого издания Eloquent JavaScript: «Студент долгое время сидел за своим компьютером, мрачно хмурился и пытался написать красивое решение сложной проблемы, но не мог найти правильный подход. Фу-Цу ударил его по затылку и крикнул: ‘Введите что-нибудь!’. Студент начал писать уродливое решение, и после того, как он закончил, он внезапно понял прекрасное решение».
Понятия, необходимые для понимания функционального программирования, реактивного программирования и функционального реактивного программирования, может быть трудно осознать, не говоря уже о полном овладении. Написание кода, использующего основные принципы какой-либо парадигмы — это первый шаг, даже если он вначале не является полностью верным. Практика освещает путь вперед, а также помогает пересматривать эти концепции.
Используя этот Справочник в качестве отправной точки, вы можете начать использовать представленные концепции и парадигмы программирования для повышения своего уровня владения JavaScript. Если по описанным темам что-либо еще неясно, пожалуйста, обратитесь к ссылкам в каждом разделе за дополнительными ресурсами. Позже мы рассмотрим новые концепции в следующей статье Справочника современных концепций JavaScript!
Парадигмы программирования: примеры парадигм для новичков
Что такое парадигмы программирования? Это не более, чем просто замысловатое название для популярных способов и стилей организации процесса написания программного кода.
Я постараюсь разбить эту тему на части и дать простое пояснение по каждой парадигме. Таким образом, вы сможете легко понять, о чем говорят люди, когда произносят такие слова, как «объектно-ориентированный», «функциональный» или «декларативный». Давайте начнем!
Что такое парадигма программирования?
Парадигмы программирования – это различные способы и стили, которые используются для организации программы или языка программирования. Каждая парадигма состоит из определенных структур, функций и взглядов на то, как следует решать известные задачи программирования.
Вопрос о том, почему существует так много различных парадигм программирования, схож с вопросом о том, почему существует так много языков программирования. Определенные парадигмы лучше подходят для определенных типов задач. Именно поэтому имеет смысл использовать разные парадигмы для разных типов проектов.
Кроме того, методики, которые составляют каждую парадигму, развивались с течением времени. Благодаря достижениям как в области программного, так и аппаратного обеспечения появились различные подходы к решению задач, которых раньше просто не было.
И последняя причина – я думаю, это просто творческое начало в человеке. По своей натуре, нам просто нравится создавать новые вещи, улучшать то, что другие когда-то создали, и адаптировать инструменты под себя и свои предпочтения или просто делать их более эффективными (в нашем понимании).
Все это привело к тому, что на сегодняшний день мы имеем огромное количество вариантов, которые могут помочь нам написать и структурировать ту или иную программу.
Чем парадигма программирования не является?
Парадигмы программирования – это не языки и не инструменты. Вы не сможете ничего «создать» с помощью парадигмы. Они больше похожи на некий набор образцов и руководящих принципов, о которых условились большое количество людей, которым они следовали и которые они подробно изложили.
Язык программирования не всегда привязан к определенной парадигме. Есть языки, которые были созданы с учетом определенной парадигмы и имеют функции, которые облегчают программирование в этом контексте больше, чем другие (хороший пример – Haskel и функциональное программирование).
Однако существуют и «многопарадигмальные» языки. Это означает, что вы можете адаптировать свой код, чтобы он подходил под какую-то из парадигм (хороший пример – JavaScript и Python).
При этом парадигмы программирования не являются взаимоисключающими в том смысле, что вы можете без каких-либо проблем использовать приемы из различных парадигм одновременно.
Популярные парадигмы программирования
Теперь, когда вы знаете, что такое парадигмы программирования, а что к ним не относится, давайте рассмотрим самые популярные из них, их характеристики и сравним их.
Имейте в виду, что этот список не полный. Существуют и другие парадигмы программирования, которые мы здесь рассматривать не будем. Здесь я расскажу вам только о самых популярных и широко используемых.
Императивное программирование
Императивное программирование – это набор подробных инструкций, которые даются компьютеру, чтобы тот выполнил их в заданном порядке. Этот тип программирования называется «императивным», потому что мы некоторым образом указываем компьютеру (как программисты), что он должен делать.
Императивное программирование концентрируется на описании того, как программа работает, шаг за шагом.
Допустим, вы хотите испечь торт. Ваша императивная программа для такого рода задачи может выглядеть следующим образом:
Воспользуемся конкретным примером и предположим, что мы хотим отфильтровать массив чисел так, чтобы остались только числа, которые больше 5. Наш императивный код тогда будет выглядеть следующим образом:
Обратите внимание, что мы указываем программе, что нужно перебрать каждый элемент массива, сравнить каждый из них с 5 и, если элемент больше 5, то поместить его в конечный массив.
Наши инструкции предельно детализированы и конкретны, и именно это и является императивным программированием.
Процедурное программирование
Процедурное программирование – это производное от императивного программирования только с функциями (также известных как «процедуры» или «подпрограммы»).
Процедурное программирования предлагает пользователю разделить выполнение программы на функции, чтобы оптимизировать модульный принцип организации.
Вернемся к нашему примеру с тортом. Процедурная программа для этого примера будет выглядеть следующим образом:
Как вы можете видеть, благодаря реализации функций, мы можем просто прочитать три вызова функций в конце файла и понять, что делает наша программа.
Такое упрощение и абстрактное представление является одним из преимуществ процедурного программирования. Однако внутри функций находится все тот же императивный код.
Функциональное программирование
Функциональное программирование продвигает концепцию создания функций немного дальше.
В функциональном программировании функции рассматриваются как «полноправные граждане». Это означает, что их можно присваивать переменным, передавать в качестве аргумента и возвращать в качестве результата других функций.
Еще одна ключевая концепция – это идея чистых функций. Чистая функций – это функция, которая, чтобы получить результат, полагается только на свои входные данные. И при одних и тех же входных данных всегда будет один и тот же результат. Кроме того, эти функции не имеют никаких побочных эффектов (то есть не вносят никаких изменений вне контекста функции).
С учетом всех этих концепций, функциональное программирование призывает писать программы с помощью функций. Оно также поддерживает идею о том, что модульность кода и отсутствие побочных эффектов облегчают определение и разделение обязанностей внутри кодовой базы. Таким образом, это облегчает сопровождение кода.
Вернемся к примеру с фильтрацией массива. В императивной парадигме мы можем использовать внешнюю переменную для хранения результата функции, что по сути может считаться побочным эффектом.
Для того, чтобы преобразовать это в функциональное программирование, мы можем сделать следующее:
Это практически тот же самый код, но мы проворачиваем все итерации внутри функции, в которой мы также сохраняем и массив результатов. Таким образом, мы можем гарантировать, что функция не будет ничего менять за своими пределами. Она создает переменную только для обработки своей собственной информации, и после завершения своей работы удаляет ее.
Декларативное программирование
Декларативное программирование скрывает всю сложность и приближает языки программирования к человеческому языку и мышлению. Это абсолютная противоположность императивному программированию, хотя бы потому что программист дает инструкции не о том, как компьютеру следует решать задачу, а о том, какой требуется результат.
Будет намного понятнее, если мы приведем пример. Воспользуемся примером с фильтрацией массива. Декларативный подход здесь будет выглядеть следующим образом:
Обратите внимание, что, используя функцию фильтрации filter , мы явно не указываем компьютеру перебирать массив или сохранять значения в отдельном массиве. Мы просто говорим о том, что мы хотим («filter») и условие, которое необходимо выполнить («num > 5»).
Что хорошего в таком подходе? Его легче читать и понимать, и зачастую он более емкий в записи. Хорошими примерами декларативного кода являются функции filter , map , reduce и sort в JavaScript.
Еще один хороший пример – современные фреймворки/библиотеки JS, такие как React. Посмотрите, например, на этот код:
Здесь у нас есть кнопка (button) с приемником событий, который запускает функцию console.log при нажатии кнопки.
Синтаксис JSX (то, что использует React) совмещает HTML и JS. Это упрощает и ускоряет написание приложений. Но это не то, что браузеры читают и выполняют. Код React позже преобразуются в обычный HTML и JS, а вот это уже то, с чем работают браузеры.
JSX является декларативным, поскольку его цель заключается в том, чтобы предоставить разработчикам более удобный и эффективный интерфейс для работы.
Здесь также важно отметить, что в декларативном программировании компьютер все равно обрабатывает информацию как императивный код.
Если снова вернуться к примеру с массивом, то компьютер по-прежнему выполняет итерацию по массиву, как в цикле for , но нам, как программистам, не нужно писать это напрямую. Декларативное программирование скрывает всю сложность от программиста.
Объектно-ориентированное программирование
Одной из самых популярных парадигм программирование является объектно-ориентированное программирование (ООП).
Основная концепция ООП заключается в разделении понятий на сущности, которые описываются как некие объекты. Каждая сущность группирует заданный набор информации (свойств) и действий (методов), которые может выполнять эта сущность.
ООП широко использует классы. Классы — это способ создания новых объектов с помощью макета или шаблона, который задает программист. Объекты, которые были созданы с помощью класса, называются экземплярами.
Вернемся к примеру с приготовлением пищи на псевдокоде. Предположим, что в нашей пекарне у нас есть главный повар (по имени Фрэнк) и помощник повара (по имени Энтони). У каждого их них есть определенные обязанности. Если бы мы использовали ООП, то наша программа бы выглядеть следующим образом:
Преимущество ООП заключается в том, что оно облегчает понимание программы за счет четкого разделения задач и обязанностей.
Итоги
Как мы увидели, парадигмы программирования – это различные способы решения задач программирования и организации нашего кода.
Одними из самых популярных и широко используемых на сегодняшний день парадигм являются императивная, процедурная, функциональная, декларативная и объектно-ориентированная. Знание о том, что они из себя представляют, полезно для общего развития, а также для лучшего понимания других тем, связанных с программированием.
Python и функциональное программирование
Функциональное программирование — это одна из парадигм программирования. Вычисления в ней понимаются не как последовательность изменения состояний, но как вычисление значений функций в их математическом понимании. То есть функции в ФП — это не подпрограммы, а отображения элементов одного множества на другое по определенным правилам.
ФП основывается на взаимодействии с функциями. Функции, функции everywhere!
Функциональное программирование и его преимущества
Функции содержат вызовы других функций, а также инструкции, которые управляют последовательностью этих вызовов.
Вычисления начинаются с вызова некоторой функции. Она, в свою очередь, тоже вызывает функции, которые входят в её определение в соответствии с внутренней иерархией (часто вызовы происходят рекурсивно).
my_list = list(map(lambda x, y: x + y, [1, 2], [3, 4])) print(my_list) > [4, 6]
Каждый вызов возвращает значение, но помещается оно не в переменную, а в саму функцию, которая этот вызов совершила. После этого функция продолжает работу. Такой процесс продолжается до того момента, как та самая функция, с которой начались вычисления, не вернёт пользователю конечный результат.
Функциональное программирование часто определяют как программирование, в котором нет побочных эффектов
Отсутствие побочного эффекта означает, что функция полагается только на данные внутри себя, и не меняет данные, находящиеся вне функции. Вычисленный результат — есть единственный эффект выполнения любой функции.
Поскольку нет присваивания, переменные, однажды получившие значение, больше никогда его не меняют. Это как если бы перед именем переменной стоял модификатор final . Переменные в ФП — это просто сокращенная запись содержащихся в них значений. Это очень похоже на исконный математический смысл переменных.
Из-за отсутствия побочных эффектов, в функциональном программировании нет явного контроля над порядком выполнения операций. На вычисляемый результат ничто не влияет, и нам не важно, когда его вычислять.
К преимуществам такого способа написания кода можно отнести:
- Надёжность — отсутствие побочных эффектов, исключение мутации данных и отсутствие ошибок, вроде случайного присваивания неверного типа переменной — всё это повышает надежность программы.
- Лаконичность — в большинстве случаев, программа, написанная в функциональном стиле, будет на порядок короче аналогичного кода с применением других парадигм. Некоторые разработчики отмечают, что код ФП-программ ещё и более понятный. Но это чистый субъективизм.
- Удобство тестирования — функциональная программа — мечта юнит-тестера. Так как функции не производят побочных эффектов, не меняют объекты и всегда возвращают строго детерминированный результат – тестировать их легко и просто. Всего лишь нужно вычислить значение функции на разных наборах аргументов.
- Оптимизация — ФП позволяет писать код в декларативном стиле. Таким образом, последовательность выполнения операций в явном виде не задаётся, а автоматически синтезируется в ходе вычислений. Это даёт возможность применять сложные методы автоматической оптимизации.
- Параллелизм — ФП по определению гарантирует отсутствие побочных эффектов. А в любом вызове функции всегда допустимо параллельное вычисление двух различных параметров. Причём порядок их вычисления не влияет на результат вызова.
def vs лямбда-выражение
А как там у нас в Питоне с ФП? Всё отлично. Python — современный язык, который полностью поддерживает парадигму функционального программирования.
Функция в Python может быть определена через классический оператор def :
def add_one(a, b): return a + b + 1
А можно эту запись заменить эквивалентным лямбда-выражением:
add_one = lambda a, b: a + b + 1
Это второй способ определения функций.
Встроенные функции высших порядков
Функции высшего порядка могут принимать другие функции в качестве аргумента, или же возвращать функцию как результат. В Питоне есть несколько встроенных функций высшего порядка:
Принимает функцию-аргумент и применяет её ко всем элементам входящей последовательности.
# напечатаем квадраты чисел от 1 до 5 my_list = list(map(lambda x: x**2, [1, 2, 3, 4, 5])) print(my_list) > [1, 4, 9, 16, 25]
filter()
Как следует из названия, filter() фильтрует последовательность по заданному условию.
# отфильтруем список с целью получить только чётные значения my_list = list(filter(lambda x: x % 2 == 0, [11, 22, 33, 44, 55, 66])) print(my_list) > [22, 44, 66]
apply()
Применяет входящую функцию к позиционным и именованным аргументам. Эти аргументы задаются списком и словарём соответственно.
♀️ В Python 3 вместо имени функции применяется специальный синтаксис:
def print_things(q, w, e, r, t=None, y=1): print(q, w, e, r, t, y) print_things(*[0, 2, 4, 6], **<'t': 8, 'y': 10>) > 0 2 4 6 8 10
Упаковывает итерируемые объекты в один список кортежей. При работе ориентируется на объект меньшей длины:
Модуль functools
Functools — это библиотека, которая содержит дополнительные функции высших порядков. Подключай и используй.
reduce()
Принимает функцию и последовательность. Запускает цепь вычислений, применяя функцию ко всем элементам последовательности. Сводит набор к единственному значению.
from functools import reduce # посчитаем сумму элементов списка res = reduce(lambda sum, x: sum + x, [0.1, 0.3, 0.6]) print(res) > 1.0
С помощью reduce() можно делать так:
import functools # посчитаем с reduce количество вхождений строки в список uncle_ben = ['С большой силой', 'приходит', 'большая ответственность'] complete = functools.reduce(lambda a, x: a + x.count('ответственность'), uncle_ben, 0) print(complete) > 1
partial()
Функция служит для частичного назначения аргументов. На входе и на выходе — тоже функции.
import functools # допустим у нас есть какая-то функция def set_stars(amount, type): print(type, amount) # уменьшаем число аргументов set_stars с partial print_neutron_star = functools.partial(set_stars, type='Neutron stars: ') print_neutron_star(1434) > Neutron stars: 1434
cmp_to_key()
Возвращает ключевую функцию для компарации объектов.
# сортировка по убыванию def num_compare(x, y): return y — x print(sorted([4, 43, 1, 22], key=functools.cmp_to_key(num_compare))) > [43, 22, 4, 1]
update_wrapper()
Используется для обновления метаданных функции-обертки, данными из некоторых атрибутов оборачиваемой функции. Обеспечивает лучшую читаемость и возможность повторного использования кода.
@lru_cache
lru_cache — это декоратор. То есть "обёртка", которая может изменить поведение функции, не меняя её код. Lru_cache даёт выбранной функции кэширование, чтобы фиксировать результаты тяжеловесных вычислений, запросов или других операций.
from functools import lru_cache # функция ищет num-е число Фибоначчи @lru_cache(maxsize=None) def fib_rec(num): if num < 2: return num return fib_rec(num — 1) + fib_rec(num — 2) print(fib_rec(100)) print(fib_rec.cache_info()) >354224848179261915075 > CacheInfo(hits=98, misses=101, maxsize=None, currsize=101)
@total_ordering
Это декоратор класса, в котором задаются методы сравнения.
@wraps
wraps — это ещё один декоратор. Он применяется к функции-обертке. wraps обновляет функцию-обертку так, чтобы она выглядела как оборачиваемая функция. При этом копируются атрибуты __name__ и __doc__ .
Замыкания
Замыкания неразрывно связаны с концепцией вложенных функций. Это такие функции, в теле которых есть ссылки на переменные, объявленные вне определения этой функции и не являющиеся её параметрами. Можно сказать, что функция запоминает свои внешние переменные и может получить к ним доступ. JS-разработчики сталкиваются с замыканиями ежедневно.
def substractor(f_num): def sub(s_num): return s_num — f_num return sub # функция, которая отнимает 5 из аргумента sub_five = substractor(5) print(sub_five(10)) > 5 # аналог substractor = lambda f_num: lambda s_num: s_num — f_num sub_four = substractor(4) print(sub_four(8)) > 4
Итераторы
Python 3 itertools позволяет создавать собственные итераторы, которые работают быстрее и более оптимально используют память.
Доступные итераторы библиотеки itertools :
- accumulate();
- chain();
- chain.from_iterable();
- compress();
- dropwhile();
- filterfalse();
- groupby();
- islice();
- starmap();
- takewhile();
- tee();
- zip_longest().
Все вышеперечисленные итераторы конечны. Но в модуле также представлено три бесконечных итератора. При их использовании не забывайте про break .
- count();
- cycle();
- repeat().
Наконец в модуле есть трио комбинаторных генераторов;
- combinations();
- combinations_with_replacement();
- product().
Таким образом, библиотека itertools — это мощнейший инструмент для создания и использования итераторов, о котором студенты, изучающие C++, могут лишь мечтать!
Ленивые вычисления
Такое название неспроста. В процессе lazy evaluations вычисления происходят лишь тогда, когда это нужно, а не заранее. Такой подход позволяет оперировать с бесконечными объектами. Расчёт происходит не постоянно, и в определенный момент времени вычисленными будут только те элементы, которые нужны прямо сейчас. Если возникнет необходимость в получении других элементов этого объекта, то они тоже будут вычислены и сохранены.
import evalcache # всё то вычисление чисел Леонарда Пизанского, но теперь «ленивое» lazy = evalcache.Lazy(cache=<>, onuse=True) @lazy def lazy_fib(n): if n < 2: return n return lazy_fib(n — 1) + lazy_fib(n — 2)
Искусственный интеллект, big data и блокчейны последние пять лет переживают эпоху взрывного роста. В связи с этим, перед программистами встала проблема больших потоков данных. Параллельные и децентрализованные вычисления позволяют увеличить скорость обработки big data на порядки. Однако когда речь идёт о параллелизме, ООП внезапно спотыкается. Знакомая нам парадигма уже не может предложить разработчикам и пользователям оптимальную скорость, в то время как функциональный код справляется с этой задачей на ура.
Чистые функции, функции высокого порядка, замыкания и неизменяемые состояния очень хорошо подходят для разработки высоконагруженных и децентрализованных программ.
К примеру, штат из 55 сотрудников Facebook, используя функциональный Erlang, способен поддерживать 2 миллиарда пользователей WhatsApp-а. Данных с каждым днём становится всё больше, поэтому популярность ФП будет только расти, и этот рост уже хорошо прослеживается по числу релевантных вакансий на сайтах с вакансиями.