Чем ооп лучше процедурного
«Забытые» парадигмы программирования
Получилось так, что те парадигмы, которые раньше потом и кровью пробивались в свет через орды приверженцев традиционных методов постепенно забываются. Эти парадигмы возникли на заре программирования и то, почему они возникали, какие преимущества они давали и почему используются до сих пор полезно знать любому разработчику.
Ладно. Введение это очень весело, но вы его все равно не читаете, так что кому интересно — добро пожаловать под кат!
Императивное программирование
Исторически сложилось так, что подавляющее большинство вычислительной техники, которую мы программируем имеет состояние и программируется инструкциями, поэтому первые языки программирования в основном были чисто императивными, т.е. не поддерживали никаких парадигм кроме императивной.
Это были машинные коды, языки ассемблера и ранние высокоуровневые языки, вроде Fortran.
Ключевые моменты:
В этой парадигме вычисления описываются в виде инструкций, шаг за шагом изменяющих состояние программы.
В низкоуровневых языках (таких как язык ассемблера) состоянием могут быть память, регистры и флаги, а инструкциями — те команды, что поддерживает целевой процессор.
В более высокоуровневых (таких как Си) состояние — это только память, инструкции могут быть сложнее и вызывать выделение и освобождение памяти в процессе своей работы.
В совсем высокоуровневых (таких как Python, если на нем программировать императивно) состояние ограничивается лишь переменными, а команды могут представлять собой комплексные операции, которые на ассемблере занимали бы сотни строк.
Основные понятия:
Порожденные понятия:
— Присваивание
— Переход
— Память
— Указатель
Языки поддерживающие данную парадигму:
Как основную:
— Языки ассемблера
— Fortran
— Algol
— Cobol
— Pascal
— C
— C++
— Ada
Как вспомогательную:
— Python
— Ruby
— Java
— C#
— PHP
— Haskell (через монады)
Стоит заметить, что большая часть современных языков в той или иной степени поддерживает императивное программирование. Даже на чистом функциональном языке Haskell можно писать императивно.
Структурное программирование
Структурное программирование — парадигма программирования (также часто встречающееся определение — методология разработки), которая была первым большим шагом в развитии программирования.
Основоположниками структурного программирования были такие знаменитые люди как Э. Дейкстра и Н. Вирт.
Языками-первопроходцами в этой парадигме были Fortran, Algol и B, позже их приемниками стали Pascal и C.
Ключевые моменты:
Эта парадигма вводит новые понятия, объединяющие часто используемые шаблоны написания императивного кода.
В структурном программировании мы по прежнему оперируем состоянием и инструкциями, однако вводится понятие составной инструкции (блока), инструкций ветвления и цикла.
Благодаря этим простым изменениям возможно отказаться от оператора goto в большинстве случаев, что упрощает код.
Иногда goto все-же делает код читабельнее, благодаря чему он до сих пор широко используется, несмотря на все заявления его противников.
Основные понятия:
— Блок
— Цикл
— Ветвление
Языки поддерживающие данную парадигму:
Как основную:
Как вспомогательную:
— C#
— Java
— Python
— Ruby
— JavaScript
Поддерживают частично:
— Некоторые макроассемблеры (через макросы)
Опять-же большая часть современных языков поддерживают структурную парадигму.
Процедурное программирование
Опять-же возрастающая сложность программного обеспечения заставила программистов искать другие способы описывать вычисления.
Собственно еще раз были введены дополнительные понятия, которые позволили по-новому взглянуть на программирование.
Этим понятием на этот раз была процедура.
В результате возникла новая методология написания программ, которая приветствуется и по сей день — исходная задача разбивается на меньшие (с помощью процедур) и это происходит до тех пор, пока решение всех конкретных процедур не окажется тривиальным.
Ключевые моменты:
Процедура — самостоятельный участок кода, который можно выполнить как одну инструкцию.
В современном программировании процедура может иметь несколько точек выхода (return в C-подобных языках), несколько точек входа (с помощью yield в Python или статических локальных переменных в C++), иметь аргументы, возвращать значение как результат своего выполнения, быть перегруженной по количеству или типу параметров и много чего еще.
Основные понятия:
Порожденные понятия:
— Вызов
— Аргументы
— Возврат
— Рекурсия
— Перегрузка
Языки поддерживающие данную парадигму:
Как основную:
— C
— C++
— Pascal
— Object Pascal
Как вспомогательную:
— C#
— Java
— Ruby
— Python
— JavaScript
Поддерживают частично:
— Ранний Basic
Стоит отметить, что несколько точек входа из всех этих языков поддерживаются только в Python.
Модульное программирование
Который раз увеличивающаяся сложность программ заставила разработчиков разделять свой код. На этот раз процедур было недостаточно и в этот раз было введено новое понятие — модуль.
Забегая вперед скажу, что модули тоже оказались неспособны сдержать с невероятной скоростью растущую сложность ПО и в последствии появились пакеты (это тоже модульное программирование), классы (это уже ООП), шаблоны (обобщенное программирование).
Программа описанная в стиле модульного программирования — это набор модулей. Что внутри, классы, императивный код или чистые функции — не важно.
Благодаря модулям впервые в программировании появилась серьезная инкапсуляция — возможно использовать какие-либо сущности внутри модуля, но не показывать их внешнему миру.
Ключевые моменты:
Модуль — это отдельная именованная сущность программы, которая объединяет в себе другие программные единицы, близкие по функциональности.
Например файл List.mod включающий в себя класс List
и функции для работы с ним — модуль.
Папка Geometry, содержащая модули Shape, Rectangle и Triangle — тоже модуль, хоть и некоторые языки разделяют понятие модуля и пакета (в таких языках пакет — набор модулей и/или набор других пакетов).
Модули можно импортировать (подключать), для того, чтобы использовать объявленные в них сущности.
Основные понятия:
Порожденные понятия:
Языки поддерживающие данную парадигму:
Как основную:
— Haskell
— Pascal
— Python
Как вспомогательную:
— Java
— C#
— ActionScript 3
Поддерживают частично:
— C/C++
В некоторых языках для модулей введены отдельные абстракции, в других же для реализации модулей можно использовать заголовочные файлы (в C/C++), пространства имен, статические классы и/или динамически подключаемые библиотеки.
Вместо заключения
В данной статье я не описал популярные сейчас объектно-ориентированное, обобщенное и функциональное программирование. Просто потому, что у меня есть свое, довольно радикальное мнение на этот счет и я не хотел разводить холивар. По крайней мере сейчас. Если тема окажется полезной для сообщества я планирую написать несколько статей, изложив основы каждой из этих парадигм подробно.
Также я ничего не написал про экзотические парадигмы, вроде автоматного, аппликативного, аспект/агент/компонент-ориентированного программирования. Я не хотел делать статью сильно большой и опять-же если тема будет востребована, я напишу и об этих парадигмах, возможно более подробно и с примерами кода.
Digitrode
цифровая электроника вычислительная техника встраиваемые системы
Разница между процедурно-ориентированным и объектно-ориентированным программированием
Мы все знаем, что существуют 2 подхода к написанию программы – процедурно-ориентированное программирование (ПОП) и объектно-ориентированное программирование (ООП). Вы можете написать программу, используя любой из этих способов, но между ними есть заметные различия. Эти 2 подхода являются результатом эволюции разработки программного обеспечения, длившейся многие десятилетия. С момента изобретения компьютера было опробовано множество подходов и методов написания программ. Сюда можно отнести такие методы, как программирование сверху вниз (Top-Down programming), программирование снизу вверх (Bottom-Up programming), модульное программирование (Modular programming), структурное программирование (Structured programming) и другие. Основной целью всех этих методов было одно — сделать процесс программирования эффективнее. Это означает, сделать процесс написания сложных программ менее трудным, легко понимаемым, легко расширяемым/модифицируемым и имеющим как можно меньшую вероятность появления ошибок.
Говоря простыми словами, разницу между ПОП и ООП можно объяснить так: программист достаточно хорошо может справиться с задачей разработки программы средней сложности с помощью методики ПОП, но когда программа становится сложнее или классифицируется как задача повышенной сложности, то будет непросто написать эффективный код с помощью ПОП. Весь процесс программирования станет сложнее, займет больше времени, вылезет больше ошибок, нужно будет больше времени на их устранение и т.д. В этом случае ООП оказывается гораздо эффективнее ПОП. Весьма сложные программы могут быть разработаны гораздо эффективнее с использованием метода ООП. Будем надеяться, что вы получили общее представление о разнице между обоими подходами. Не существует конкретного правила, в каких случаях какой метод нужно применять. Все отводится на усмотрение программиста. Однако, в отрасли разработки программного обеспечения в основном следуют методике ООП, поскольку она способствует организации совместной работы. Основной причиной этого является повторное использование кода. Часть кода, разработанная одним программистом, может быть повторно использована любое количество раз любым количеством других программистов. Это делает разработку программного обеспечения более быстрой и эффективной.
Рассмотрим теперь каждую методику в отдельности.
Процедурно-ориентированное программирование
Слово процедура является здесь ключевым элементом. Оно означает набор процедур, который представляет собой набор подпрограмм или набор функций. Мы все знаем о функциях в языке C. C это процедурно-ориентированный язык. В ПОП основное внимание уделяется функциям или подпрограммам. Функции содержат набор команд, выполняющих определенную задачу. Функции в программе вызываются повторно для выполнения определенных задач. Например, программа может включать в себя сбор данных от пользователя (чтение), выполнения какого-то рода расчетов собранных данных (вычисление) и отображения результатов по запросу пользователя (вывод). Все эти три задачи, чтение, вычисление и вывод результатов, могут быть написаны в программе с помощью трех различных функций, выполняющих три различные задачи.
Структура методики ПОП
Проблема при применении ПОП заключается в обработке данных. В ПОП данным не придается никакого значения. Под данными мы понимаем информацию, полученную от пользователя, новые результаты, полученные после вычислений, и т.д. Если вы знакомы с программированием на C, то можете вспомнить классы памяти (storage classes) этого языка. В C элемент данных (переменная) должен быть объявлен как GLOBAL для того, чтобы он мог быть доступен двум или более функциям программы. Что происходит, когда 2 или более функций работают с одним и тем же элементом данных? Если в программе насчитывается 10 функций, то все эти 10 функций могут получить доступ к глобальной переменной. Вполне возможно, что одна функция может случайно изменить значение этой глобальной переменной. И если она является ключевым элементом программы, любые такие случайные манипуляции повлияют на всю программу. Тогда будет слишком трудно отлаживать и выявлять функцию, вызывающую проблемы, особенно если программа очень большая.
Работа с данными и функциями в ПОП
Одной из наиболее важных особенностей языка C являются структуры. Тот, кто знаком с этим языком, вспомнит, что структуры в C объявляются с помощью ключевого слова struct. Структуры позволяют упаковать вместе разные типы данных в единое целое. Программист может упаковать целочисленные данные, числа с плавающей точкой (float), массивы и прочие виды данных в единый объект с помощью структуры. Способ программирования с помощью структур был впервые введен в языке C, и это послужило основной причиной столь широкой популярности данного языка. И какова же истинная причина этого? Структура достаточно хорошо моделирует требования реального мира в компьютерной программе. Проблемой, связанной со структурами, было то, что она работала только с данными. Структура не позволяет упаковать вместе связанные функции наряду с данными. Все функции, манипулирующие с элементами данных внутри структуры должны быть написаны отдельно внутри программы. По этой причине программа на C имеет большую зависимость от функций.
В случае применения подхода ПОП проблема рассматривается в виде последовательности задач, которые могут быть реализованы как чтение, выполнение вычислений, вывод результата и т.п. Все задачи сначала анализируются, а затем разрабатываются функции/процедуры для выполнения всех этих задач.
Объектно-ориентированное программирование
Метод ООП отличается от ПОП в своем базовом подходе. ООП был разработан с сохранением всех лучших черт метода структурного программирования, который был дополнен множеством концепций, способствующих эффективному программированию. ООП дает много возможностей, и он создает совершенно новый путь в написании программ. В общем, ООП перенял все лучшие черты методики ПОП, вроде функций/процедур, структур и т.д.
Первой особенностью ООП, о которой упомянули бы программисты, является сокрытие данных (инкапсуляция). ООП придает огромное значение данным. Программист может скрыть действительно важные ключевые данные от внешнего мира, используя инструменты ООП. Базовая концепция ООП основана на понятии, схожем с понятием структуры в ПОП и называемым классом. Класс представляет собой важную особенность ООП, он позволяет упаковать вместе различные типы данных наряду с различными функциями, манипулирующими элементами данных этого класса. Элементы данных внутри класса могут быть объявлены как локальные (private) или глобальные (public). Для того, чтобы спрятать данные от внешнего мира, программист должен объявить их как private. В общем, класс действительно схож со структурой в языке C. Как и любая структура, он объединяет в единое целое различные объекты. Основное отличие между классом и структурой кроется в функциях. Структуры не позволяют объединять в себе данные и функции (структуры работают только с данными), тогда как классы позволяют упаковывать данные вместе со связанными с ними функциями. Кроме того, имеются еще различия, вроде сокрытия данных с помощью private/public. Структуры не облегчают сокрытие данных. В структуре к ее элементам получают доступ с помощью так называемых структурных переменных. В ООП используют другое понятие для доступа к данным и функциям внутри класса — объект. Данные и функции внутри класса называются членами или элементами класса. К элементу класса может быть получен доступ из внешнего мира (вне класса) только с помощью объекта класса.
Возможность сокрытия данных называется инкапсуляцией данных. Таким образом, один из главных недостатков ПОП решается в ООП. ООП тесно связывает данные с определенным классом и его объектами. Здесь нет необходимости в глобальных типах данных, как в ПОП, и, следовательно, данные не могут свободно «течь» по всей программе. Это гарантирует то, что не произойдет какой-либо случайной модификации важных данных.
Еще одной особенностью, привнесенной в ООП, является возможность повторного использования кода. Это просто означает то, что кусок кода, который был написан ранее, может быть использован в будущем. Это стало возможным благодаря особенности классов под названием наследование. Благодаря наследованию один класс может приобрести свойства другого класса. Это можно объяснить с помощью простого примера. Возьмем систему школьного управления. Изначально руководство решило разработать программу, сфокусированную только на учеников (без учета данных об учителях). Программист превосходно справился со своей работой, и в процессе программирования он объявил класс для сбора персональных данных, таких как имя, возраст, пол, адрес и т.д. Через год руководство школы решило включить в список данные об учителях. Теперь программист способен добавить эти данные за очень короткое время, поскольку он может использовать многие части кода, которые он написал ранее, благодаря наследованию. Класс персональных данных имеет общий характер (возраст, пол и пр. те же самые для человека вне зависимости от того, учитель он или ученик). Программист может наследовать данный класс новому классу, а также расширять этот новыми записями, например, записью о квалификации учителя.
У ООП имеется еще много возможностей, вроде полиморфизма (перегрузки операторов и функций), динамического связывания и т.д. Обо всем этом можно почитать в соответствующей литературе.
Объектно-ориентированное программирование: на пальцах
Статья не мальчика, но мужа.
Настало время серьёзных тем: сегодня расскажем про объектно-ориентированное программирование, или ООП. Это тема для продвинутого уровня разработки, и мы хотим, чтобы вы его постигли.
Из этого термина можно сделать вывод, что ООП — это такой подход к программированию, где на первом месте стоят объекты. На самом деле там всё немного сложнее, но мы до этого ещё доберёмся. Для начала поговорим про ООП вообще и разберём, с чего оно начинается.
Обычное программирование (процедурное)
Чаще всего под обычным понимают процедурное программирование, в основе которого — процедуры и функции. Функция — это мини-программа, которая получает на вход какие-то данные, что-то делает внутри себя и может отдавать какие-то данные в результате вычислений. Представьте, что это такой конвейер, который упакован в коробочку.
Например, в интернет-магазине может быть функция «Проверить email». Она получает на вход какой-то текст, сопоставляет со своими правилами и выдаёт ответ: это правильный электронный адрес или нет. Если правильный, то true, если нет — то false.
Функции полезны, когда нужно упаковать много команд в одну. Например, проверка электронного адреса может состоять из одной проверки на регулярные выражения, а может содержать множество команд: запросы в словари, проверку по базам спамеров и даже сопоставление с уже известными электронными адресами. В функцию можно упаковать любой комбайн из действий и потом просто вызывать их все одним движением.
Что не так с процедурным программированием
Процедурное программирование идеально работает в простых программах, где все задачи можно решить, грубо говоря, десятком функций. Функции аккуратно вложены друг в друга, взаимодействуют друг с другом, можно передать данные из одной функции в другую.
Например, вы пишете функцию «Зарегистрировать пользователя интернет-магазина». Внутри неё вам нужно проверить его электронный адрес. Вы вызываете функцию «Проверить email» внутри функции «Зарегистрировать пользователя», и в зависимости от ответа функции вы либо регистрируете пользователя, либо выводите ошибку. И у вас эта функция встречается ещё в десяти местах. Функции как бы переплетены.
Тут приходит продакт-менеджер и говорит: «Хочу, чтобы пользователь точно знал, в чём ошибка при вводе электронного адреса». Теперь вам нужно научить функцию выдавать не просто true — false, а ещё и код ошибки: например, если в адресе опечатка, то код 01, если адрес спамерский — код 02 и так далее. Это несложно реализовать.
Вы залезаете внутрь этой функции и меняете её поведение: теперь она вместо true — false выдаёт код ошибки, а если ошибки нет — пишет «ОК».
И тут ваш код ломается: все десять мест, которые ожидали от проверяльщика true или false, теперь получают «ОК» и из-за этого ломаются.
Задача, конечно, решаемая за час-другой.
Но теперь представьте, что у вас этих функций — сотни. И изменений в них нужно делать десятки в день. И каждое изменение, как правило, заставляет функции вести себя более сложным образом и выдавать более сложный результат. И каждое изменение в одном месте ломает три других места. В итоге у вас будут нарождаться десятки клонированных функций, в которых вы сначала будете разбираться, а потом уже нет.
Это называется спагетти-код, и для борьбы с ним как раз придумали объектно-ориентированное программирование.
Объектно-ориентированное программирование
Основная задача ООП — сделать сложный код проще. Для этого программу разбивают на независимые блоки, которые мы называем объектами.
Объект — это не какая-то космическая сущность. Это всего лишь набор данных и функций — таких же, как в традиционном функциональном программировании. Можно представить, что просто взяли кусок программы и положили его в коробку и закрыли крышку. Вот эта коробка с крышками — это объект.
Программисты договорились, что данные внутри объекта будут называться свойствами, а функции — методами. Но это просто слова, по сути это те же переменные и функции.
Объект можно представить как независимый электроприбор у вас на кухне. Чайник кипятит воду, плита греет, блендер взбивает, мясорубка делает фарш. Внутри каждого устройства куча всего: моторы, контроллеры, кнопки, пружины, предохранители — но вы о них не думаете. Вы нажимаете кнопки на панели каждого прибора, и он делает то, что от него ожидается. И благодаря совместной работе этих приборов у вас получается ужин.
Объекты характеризуются четырьмя словами: инкапсуляция, абстракция, наследование и полиморфизм. Если интересно, что это такое, приглашаем в кат:
Инкапсуляция — объект независим: каждый объект устроен так, что нужные для него данные живут внутри этого объекта, а не где-то снаружи в программе. Например, если у меня есть объект «Пользователь», то у меня в нём будут все данные о пользователе: и имя, и адрес, и всё остальное. И в нём же будут методы «Проверить адрес» или «Подписать на рассылку».
Абстракция — у объекта есть «интерфейс»: у объекта есть методы и свойства, к которым мы можем обратиться извне этого объекта. Так же, как мы можем нажать кнопку на блендере. У блендера есть много всего внутри, что заставляет его работать, но на главной панели есть только кнопка. Вот эта кнопка и есть абстрактный интерфейс.
Например, над магазином работают два программиста: один пишет модуль заказа, а второй — модуль доставки. У первого в объекте «заказ» есть метод «отменить». И вот второму нужно из-за доставки отменить заказ. И он спокойно пишет: «заказ.отменить()». Ему неважно, как другой программист будет реализовывать отмену: какие он отправит письма, что запишет в базу данных, какие выведет предупреждения.
Наследование — способность к копированию. ООП позволяет создавать много объектов по образу и подобию другого объекта. Это позволяет не копипастить код по двести раз, а один раз нормально написать и потом много раз использовать.
Например, у вас может быть некий идеальный объект «Пользователь»: в нём вы прописываете всё, что может происходить с пользователем. У вас могут быть свойства: имя, возраст, адрес, номер карты. И могут быть методы «Дать скидку», «Проверить заказ», «Найти заказы», «Позвонить».
На основе этого идеального пользователя вы можете создать реального «Покупателя Ивана». У него при создании будут все свойства и методы, которые вы задали у идеального покупателя, плюс могут быть какие-то свои, если захотите.
Идеальные объекты программисты называют классами.
Полиморфизм — единый язык общения. В ООП важно, чтобы все объекты общались друг с другом на понятном им языке. И если у разных объектов есть метод «Удалить», то он должен делать именно это и писаться везде одинаково. Нельзя, чтобы у одного объекта это было «Удалить», а у другого «Стереть».
При этом внутри объекта методы могут быть реализованы по-разному. Например, удалить товар — это выдать предупреждение, а потом пометить товар в базе данных как удалённый. А удалить пользователя — это отменить его покупки, отписать от рассылки и заархивировать историю его покупок. События разные, но для программиста это неважно. У него просто есть метод «Удалить()», и он ему доверяет.
Такой подход позволяет программировать каждый модуль независимо от остальных. Главное — заранее продумать, как модули будут общаться друг с другом и по каким правилам. При таком подходе вы можете улучшить работу одного модуля, не затрагивая остальные — для всей программы неважно, что внутри каждого блока, если правила работы с ним остались прежними.
Плюсы и минусы ООП
У объектно-ориентированного программирования много плюсов, и именно поэтому этот подход использует большинство современных программистов.
А теперь про минусы:
Что дальше
Впереди нас ждёт разговор о классах, объектах и всём остальном важном в ООП. Крепитесь, будет интересно!