- Парадигмы программирования
- Императивное программирование
- Неструктурное программирование
- Структурное программирование
- Процедурное программирование
- Объектно-ориентированное программирование
- Декларативное программирование
- Функциональное программирование
- Аппликативное
- Комбинаторное
- Логическое программирование
- Чистая архитектура. Часть II — Парадигмы программирования
- Парадигмы программирования
- Обзор парадигм
- Структурное программирование
- Объектно-ориентированное программирование
- Функциональное программирование
- Заключение
Парадигмы программирования
Вообще строгого определения нет, но по сути “парадигма программирования” определяет стиль написания исходного кода программ.
В некоторой степени “стиль” диктует так же способ формализации алгоритмов.
Императивное программирование
Императивное программирование характеризуется в основном:
- в исходном коде программы записываются инструкции (команды);
- инструкции должны выполняться последовательно;
- каждая инструкция может изменять некое глобальное “состояние” программы
При императивном подходе к составлению кода (в отличие от функционального подхода, относящегося к декларативной парадигме) широко используется присваивание. Наличие операторов присваивания увеличивает сложность модели вычислений и делает императивные программы подверженными специфическим ошибкам, не встречающимся при функциональном подходе.
Практически всё аппаратное обеспечение в основе своей императивное.
Неструктурное программирование
Характерно для наиболее ранних языков программирования.
В основном характеризуется:
- строки как правило нумеруются
- из любого места программы возможен переход к любой строке
Характерной особенностью неструктурного программирования является сложность реализации рекурсии.
Структурное программирование
В отличие от неструктурного программирования, характеризуется:
- ограниченным использованием условных и безусловных переходов
- широким использованием подпрограмм и прочих управляющих структур (циклов, ветвлений, и т.п.)
- блочной структурой
Концепция структурного программирования основана на теореме Бёма-Якопини:
Последовательность – это выполнение сначала одной подпрограммы, затем другой.
Ветвление – это выполнение либо одной, либо другой подпрограммы в зависимости от значения некого булева (логического) выражения.
Итерация – это многократное выполнение подпрограммы пока некое булево выражение истинно.
Процедурное программирование
Процедурное программирование можно рассматривать как небольшую вариацию на тему структурного программирования, основанную на концепции вызова процедуры.
Основная идея заключается в том, чтобы сделать подпрограммы более модульными за счёт:
Оба этих пункта реализуются за счёт использования стека вызовов.
Объектно-ориентированное программирование
Объектно-ориентированное программирование основано на концепции “объекта”.
Объекты могут содержать данные (поля, свойства, аттрибуты) и поведение (код, процедуры, методы).
Наиболее популярной формой ООП является ООП на основе классов. В данном подходе, все объекты являются экземплярами классов, и классы определяют так же тип объектов.
Одной из альтернатив является прототипное наследование. Прототипное наследование не использует классов. Вместо этого, одни объекты могут быть объявлены “прототипами” других объектов – при этом методы и поля прототипа становятся доступны как методы и поля нового объекта (если, конечно, новый объект их не переопределяет)
Декларативное программирование
Декларативное программирование — это парадигма программирования, в которой задаётся спецификация решения задачи, то есть описывается, что представляет собой проблема и ожидаемый результат. Противоположностью декларативного является императивное программирование, описывающее на том или ином уровне детализации, как решить задачу и представить результат.
Как следствие, декларативные программы не используют понятия состояния, то есть не содержат переменных и операторов присваивания.
К подвидам декларативного программирования также зачастую относят функциональное и логическое программирование — несмотря на то, что программы на таких языках нередко содержат алгоритмические составляющие.
“Чисто декларативные” компьютерные языки зачастую не полны по Тьюрингу — примерами служат SQL и HTML — так как теоретически не всегда возможно порождение исполняемого кода по декларативному описанию. Это иногда приводит к спорам о корректности термина “декларативное программирование”.
Функциональное программирование
- отсутствие неявных побочных эффектов
- ссылочная прозрачность
- отсутствие неявного состояния
- данные и функции – это концептуально одно и то же
Основано на лямбда-исчислении
Аппликативное
Аппликативное программирование — один из видов декларативного программирования, в котором написание программы состоит в систематическом осуществлении применения одного объекта к другому. Результатом такого применения вновь является объект, который может участвовать в применениях как в роли функции, так и в роли аргумента и так далее. Это делает запись программы математически ясной. Тот факт, что функция обозначается выражением, свидетельствует о возможности использования значений-функций — функциональных объектов — на равных правах с прочими объектами, которые можно передавать как аргументы, либо возвращать как результат вычисления других функций.
Комбинаторное
Комбинаторное программирование (англ. function-level programming) — парадигма программирования, использующая принципы комбинаторной логики.
Является особой разновидностью функционального программирования, но, в отличие от основного его направления, комбинаторное программирование не использует λ-абстракцию.
На практике это выливается в отсутствие “переменных”, содержащих данные.
Логическое программирование
Логическое программирование — парадигма программирования, основанная на автоматическом доказательстве теорем, а также раздел дискретной математики, изучающий принципы логического вывода информации на основе заданных фактов и правил вывода. Логическое программирование основано на теории и аппарате математической логики с использованием математических принципов резолюций.
Самым известным языком логического программирования является Prolog.
Чистая архитектура. Часть II — Парадигмы программирования
Эта серия статей – вольный и очень краткий пересказ книги Роберта Мартина (Дяди Боба) «Чистая Архитектура», выпущенной в 2018 году. Начало тут.
Парадигмы программирования
Дисциплину, которая в дальнейшем стала называться программированием, зародил Алан Тьюринг в 1938 году. В 1945 он уже писал полноценные программы, которые работали на реальном железе.
Первый компилятор был придуман в 1951 году Грейс Хоппер (бабушка с татуировкой Кобола). Потом начали создаваться языки программирования.
Обзор парадигм
Существует три основных парадигмы: структурное, объектно-ориентированное и функциональное. Интересно, что сначала было открыто функциональное, потом объектно-ориентированное, и только потом структурное программирование, но применяться повсеместно на практике они стали в обратном порядке.
Структурное программирование было открыто Дейкстрой в 1968 году. Он понял, что goto – это зло, и программы должны строиться из трёх базовых структур: последовательности, ветвления и цикла.
Объектно-ориентированное программирование было открыто в 1966 году.
Функциональное программирование открыто в 1936 году, когда Чёрч придумал лямбда-исчисление. Первый функциональный язык LISP был создан в 1958 году Джоном МакКарти.
Каждая из этих парадигм убирает возможности у программиста, а не добавляет. Они говорят нам скорее, что нам не нужно делать, чем то, что нам нужно делать.
Все эти парадигмы очень связаны с архитектурой. Полиморфизм в ООП нужен, чтобы наладить связь через границы модулей. Функциональное программирование диктует нам, где хранить данные и как к ним доступаться. Структурное программирование помогает в реализации алгоритмов внутри модулей.
Структурное программирование
Дейкстра понял, что программирование – это сложно. Большие программы имеют слишком большую сложность, которую человеческий мозг не способен контролировать.
Чтобы решить эту проблему, Дейсктра решил сделать написание программ подобно математическим доказательствам, которые также организованы в иерархии. Он понял, что если в программах использовать только if, do, while, то тогда такие программы можно легко рекурсивно разделять на более мелкие единицы, которые в свою очередь уже легко доказуемы.
С тех пор оператора goto не стало практически ни в одном языке программирования.
Таким образом, структурное программирование позволяет делать функциональную декомпозицию.
Однако на практике мало кто реально применял аналогию с теоремами для доказательства корректности программ, потому что это слишком накладно. В реальном программировании стал популярным более «лёгкий» вариант: тесты. Тесты не могут доказать корректности программ, но могут доказать их некорректность. Однако на практике, если использовать достаточно большое количество тестов, этого может быть вполне достаточно.
Объектно-ориентированное программирование
ООП – это парадигма, которая характеризуется наличием инкапсуляции, наследования и полиморфизма.
Инкапсуляция позволяет открыть только ту часть функций и данных, которая нужна для внешних пользователей, а остальное спрятать внутри класса.
Однако в современных языках инкапсуляция наоборот слабее, чем была даже в C. В Java, например, вообще нельзя разделить объявление класса и его определение. Поэтому сказать, что современные объектно-ориентированные языки предоставляют инкапсуляцию можно с очень большой натяжкой.
Наследование позволяет делать производные структуры на основе базовых, тем самым давая возможность осуществлять повторное использование этих структур. Наследование было реально сделать в языках до ООП, но в объектно-ориентированных языках оно стало значительно удобнее.
Наконец, полиморфизм позволяет программировать на основе интерфейсов, у которых могут быть множество реализаций. Полиморфизм осуществляется в ОО-языках путём использования виртуальных методов, что является очень удобным и безопасным.
Полиморфизм – это ключевое свойство ООП для построения грамотной архитектуры. Он позволяет сделать модуль независимым от конкретной реализации (реализаций) интерфейса. Этот принцип называется инверсией зависимостей, на котором основаны все плагинные системы.
Инверсия зависимостей так называется, что она позволяет изменить направление зависимостей. Сначала мы начинаем писать в простом стиле, когда высокоуровневые функции зависят от низкоуровневых. Однако, когда программа начинает становиться слишком сложной, мы инвертируем эти зависимости в противоположную сторону: высокоуровневые функции теперь зависят не от конкретных реализаций, а от интерфейсов, а реализации теперь лежат в своих модулях.
Любая зависимость всегда может быть инвертирована. В этом и есть мощь ООП.
Таким образом, между различными компонентами становится меньше точек соприкосновения, и их легче разрабатывать. Мы даже можем не перекомпилировать базовые модули, потому что мы меняем только свой компонент.
Функциональное программирование
В основе функционального программирования лежит запрет на изменение переменных. Если переменная однажды проинициализирована, её значение так и остаётся неизменным.
Какой профит это имеет для архитектуры? Неизменяемые данные исключают гонки, дедлоки и прочие проблемы конкурентных программ. Однако это может потребовать больших ресурсов процессора и памяти.
Применяя функциональный подход, мы разделяем компоненты на изменяемые и неизменяемые. Причём как можно больше функциональности нужно положить именно в неизменяемые компоненты и как можно меньше в изменяемые. В изменяемых же компонентах приходится работать с изменяемыми данными, которые можно защитить с помощью транзакционной памяти.
Интересным подходом для уменьшения изменяемых данных является Event Sourcing. В нём мы храним не сами данные, а историю событий, которые привели к изменениям этих данных. Так как в лог событий можно только дописывать, это означает, что все старые события уже нельзя изменить. Чтобы получить текущее состояние данных, нужно просто воспроизвести весь лог. Для оптимизации можно использовать снапшоты, которые делаются, допустим, раз в день.
Заключение
Таким образом, каждая из трёх парадигм ограничивает нас в чём-то:
- Структурное отнимает у нас возможность вставить goto где угодно.
- ООП не позволяет нам доступаться до скрытых членов классов и навязывает нам инверсию зависимостей.
- ФП запрещает изменять переменные.