Первая серия:
=
Программирование и проектирование
Каждая программная система характеризуется двумя главными параметрами: сложностью и хаотичностью.
Сложность как правило определяется количеством базовых сущностей, которые связаны друг с другом (например, число классов в проекте). Я в разных заметках периодически говорил про более строгие формальные критерии (силу математической теории, заложенной в систему), но эта сила хорошо коррелирует с количеством сущностей: чем их меньше, тем простота и выразительность системы выше.
Хаотичность показывает, насколько недетерминировано/непредсказуемо поведение системы. Тут можно привести в качестве примера сильно непредсказуемой системы такое агрегатное состояние вещества, как газ (взаимодействие молекул в замкнутом пространстве и их огромное количество удаётся оценить лишь статистически), а в качестве очень предсказуемой системы -- часы (небольшой набор компонент, взаимодействующих друг с другом детерминированно).
В современной науке хорошо изучены системы уровня часов (практически все они достаточно простые), и именно поэтому мосты, которые часто приводятся в качестве примера в программной инженерии -- это вроде бы сложные в инженерном плане системы, однако они почти никогда не падают. А программы вроде как на много порядков проще конструктивнее, всё ещё вызывают сложности с их безошибочным созданием.
С другой стороны, хорошо изучены, только иными методами, системы с высокой непредсказуемостью (различные физические явления) практически независимо от их сложности.
Программирование остаётся где-то посередине: и предсказуемость некоторая имеется, и сложность тоже колеблется от небольшой до очень высокой. И что самое интересное, фронтир в computer science пытается учитывать оба оригинальных научных подхода. С одной стороны, программы можно рассматривать как очень сложные машины с большим количеством деталей, работа которых хорошо известна, и надо только из них правильно собрать итоговую систему. С другой стороны, программы можно рассматривать как "агрегаты" в физическом смысле через моделирование, симуляцию.
Одно из практических следствий создания огромных программных систем порядка сотен миллионов строк заключается в том, что используемые для этого языки программирования, масштабирующиеся на такую реально высокую сложность, способны с удовлетворительным качеством моделировать некоторые качественно важные, фундаментальные приёмы и аспекты конструирования очень сложных систем.
Фактически современные языки уже не просто произвольные инженерные конструкции. Они стали инструментами исследования границ предельной на сегодня сложности, причём достаточно объективным способом. Поэтому очень полезным будет изучение их в научном смысле, т. е. через разбирательство основных понятий, из которых составляются базовые парадигмы программирования, через понимание того, как концепции программирования разрабатываются и соединяются.
Каждой задаче -- своё множество парадигм
Cложность языка -- единственное, что мы понимаем под сложностью -- это ровно то, что минимально необходимо для решения задачи. Чем больше всего разного нам нужно для решения, тем, соответственно, и выше сложность. И никаких дополнительных "сложностей" не существует, это главный путь для борьбы с техническими недостатками языка. Кто проходил мои курсы программирования с нуля, хорошо знает, как часто я делаю акцент на минимальном числе используемых концепций.
Языки программирования не создавались в вакууме, они первоначально предназначались для решения вполне определённых классов проблем. И даже языки, которые принято считать универсальными, создавались фактически для определённых классов массовых задач: инженерных и научных вычислений, всевозможных учётных, отчётных, аналитических, офисных систем. Но для каждого такого класса задач можно найти конкретную парадигму, в которой эти задачи будут решаться продуктивнее и выразительнее всего. Не существует единой парадигмы для всех классов задач.
На практике этот достаточно очевидный момент выражается, например, в типовой концепции архитектурных слоёв: слой базы данных, слой серверной логики, слой обработки запросов, слой клиентской логики. Интуитивно проектировщики понимают качественную разницу между этими слоями, понимают, что для каждого из них требуется, как минимум, отдельный фреймворк. Типичное инженерное решение тут -- использовать, например, декларативный язык SQL на сервере баз данных, императивный язык Java для серверной логики (внутри которого может использоваться например Stream API, выполненный в функциональной парадигме), язык C++ для обработки запросов и язык JavaScript для клиентской логики. Главный недостаток такого шаблонного подхода в том, что под конкретные задачи лучше всего выбирать значительно более выразительные, компактные и продуктивные языки и фреймворки, точно отвечающие наиболее подходящей парадигме -- и получать выигрыш в трудоёмкости в сотни, а то и тысячи раз. Но для этого надо научиться правильно смотреть на слои архитектуры по научному, через спектр всех доступных парадигм.
Как выбирать языки, библиотеки и фреймворки?
Многие популярные языки программирования поддерживают несколько парадигм, которые можно поделить на две группы: парадигмы, поддерживающие programming in small, и парадигмы, поддерживающие programming in large. Обычно парадигма для programming in small выбирается с прицелом на непосредственное кодирование для класса задач, наиболее часто решаемых данным языком. А парадигма для programming in large требуется для проектирования, создания больших систем, для поддержки абстракций и модульности.
Например, язык F# реализует функциональную парадигму, но расширен классическим ООП. Язык SQL поддерживает реляционный механизм для запросов к базам данных, и транзакционный интерфейс для параллельных обновлений в базе. Причём, как правило, дополнительно создаются ORM-расширения (объектно-реляционные раскладки), позволяющие обращаться к базе не через декларативные команды SQL, а в формате ООП из языков наподобие Java или Python.
Декларативное программирование -- мякотка всего
По мере того, как в computer science находятся всё более сильные универсальные концепции, понимание языков программирования удаётся поднять до более выразительных абстракций. Так, если в 1970-1980-х годах университетские курсы по компиляторам формировались в основном вокруг алгоритмов синтаксического анализа, то сегодня изучают уже гораздо мощные концепции: системы типов, потоки данных, языковые "фичи". Схожая эволюция происходит и с самими языками программирования. Например, стиль программирования на Java и C# в 2010-м весьма существенно отличался от сегодняшнего стиля, дополнившегося в частности множеством возможностей из функционального программирования.
Однако некоторые глубинные стратегические аспекты этой эволюции остаются серьёзно недооценёнными, а без них профессиональное развитие программиста будет неполноценным. Прежде всего, это декларативное программирование, которое заложено в самую сердцевину, в самое ядро самой концепции программирования и всех языков программирования, в той или иной форме. Точнее, декларативная вычислительная модель, из которой естественно следуют парадигмы функционального и логического программирования, это всё мы подробно разберём на моём отдельном курсе.
Совсем уж примитивно говоря, декларативное программирование -- это stateless-программирование (когда мы не используем переменные, состояния которых могут меняться в процессе работы программы). Например, функциональное программирование -- это частный случай декларативного подхода.
Во-вторых, декларативное программирование как минимум будет оставаться в фокусе внимания многие годы, и скорее всего, само программирование как умение, с учётом бурного развития искусственного интеллекта, будет приобретать всё больше декларативных черт. Кроме того, декларативная парадигма отлично отвечает требованиям создания распределённых, безопасных, устойчивых и защищённых от сбоев систем (так как существенный объём вычислений происходит под капотом, умным рантаймом, движками логического вывода), и создание таких систем очень нуждается в поддержке со стороны программных инструментов.
В-третьих, декларативному программированию свойственна детерминистическая параллельность, играющая огромную роль в параллельных вычислениях. Ведь все классические клиент-серверные, веб-системы, работающие в реальном времени, как вы понимаете, недетерминированы: если на сервере ожидают активации несколько тредов, в каком порядке они будут запущены, решает ОС, поэтому программирование таких систем с полноценной параллельной логикой сильно затруднено.
А вот когда имеется детерминистическая параллельность, это прямой путь для полноценного использования многоядерных процессоров, причём такой же простой, как и функциональное программирование, и при этом не ограниченный конфликтами конкуренции параллельных потоков (race condition).
Заключительное замечание, что при создании параллельных систем в качестве парадигмы по умолчанию следует использовать обмен сообщениями, а не расшаренные состояния, которые ошибочно выбираются в 90% случаев, так как программисты испорчены stateful-императивщиной :-)
Далее: изучаем две фундаментальные концепции программирования, которые порождают все основные стили и парадигмы программирования
Высшая школа программирования Сергея Бобровского