ООП
Nван Makul1 часть
Объектно-ориентированное программирование: на пальцах
Что такое классы в объектно-ориентированном программировании
Зачем нужны абстракции и интерфейсы
Объектно-ориентированное программирование: на пальцах
Статья не мальчика, но мужа.
Настало время серьёзных тем: сегодня расскажем про объектно-ориентированное программирование, или ООП. Это тема для продвинутого уровня разработки, и мы хотим, чтобы вы его постигли.
Из этого термина можно сделать вывод, что ООП — это такой подход к программированию, где на первом месте стоят объекты. На самом деле там всё немного сложнее, но мы до этого ещё доберёмся. Для начала поговорим про ООП вообще и разберём, с чего оно начинается.
Обычное программирование (процедурное)
Чаще всего под обычным понимают процедурное программирование, в основе которого — процедуры и функции. Функция — это мини-программа, которая получает на вход какие-то данные, что-то делает внутри себя и может отдавать какие-то данные в результате вычислений. Представьте, что это такой конвейер, который упакован в коробочку.
Например, в интернет-магазине может быть функция «Проверить email». Она получает на вход какой-то текст, сопоставляет со своими правилами и выдаёт ответ: это правильный электронный адрес или нет. Если правильный, то true, если нет — то false.
Функции полезны, когда нужно упаковать много команд в одну. Например, проверка электронного адреса может состоять из одной проверки на регулярные выражения, а может содержать множество команд: запросы в словари, проверку по базам спамеров и даже сопоставление с уже известными электронными адресами. В функцию можно упаковать любой комбайн из действий и потом просто вызывать их все одним движением.
Что не так с процедурным программированием
Процедурное программирование идеально работает в простых программах, где все задачи можно решить, грубо говоря, десятком функций. Функции аккуратно вложены друг в друга, взаимодействуют друг с другом, можно передать данные из одной функции в другую.
Например, вы пишете функцию «Зарегистрировать пользователя интернет-магазина». Внутри неё вам нужно проверить его электронный адрес. Вы вызываете функцию «Проверить email» внутри функции «Зарегистрировать пользователя», и в зависимости от ответа функции вы либо регистрируете пользователя, либо выводите ошибку. И у вас эта функция встречается ещё в десяти местах. Функции как бы переплетены.
Тут приходит продакт-менеджер и говорит: «Хочу, чтобы пользователь точно знал, в чём ошибка при вводе электронного адреса». Теперь вам нужно научить функцию выдавать не просто true — false, а ещё и код ошибки: например, если в адресе опечатка, то код 01, если адрес спамерский — код 02 и так далее. Это несложно реализовать.
Вы залезаете внутрь этой функции и меняете её поведение: теперь она вместо true — false выдаёт код ошибки, а если ошибки нет — пишет «ОК».
И тут ваш код ломается: все десять мест, которые ожидали от проверяльщика true или false, теперь получают «ОК» и из-за этого ломаются.
Теперь вам нужно:
- либо переписывать все функции, чтобы научить их понимать новые ответы проверяльщика адресов;
- либо переделать сам проверяльщик адресов, чтобы он остался совместимым со старыми местами, но в нужном вам месте как-то ещё выдавал коды ошибок;
- либо написать новый проверяльщик, который выдаёт коды ошибок, а в старых местах использовать старый проверяльщик.
Задача, конечно, решаемая за час-другой.
Но теперь представьте, что у вас этих функций — сотни. И изменений в них нужно делать десятки в день. И каждое изменение, как правило, заставляет функции вести себя более сложным образом и выдавать более сложный результат. И каждое изменение в одном месте ломает три других места. В итоге у вас будут нарождаться десятки клонированных функций, в которых вы сначала будете разбираться, а потом уже нет.
Это называется спагетти-код, и для борьбы с ним как раз придумали объектно-ориентированное программирование.
Объектно-ориентированное программирование
Основная задача ООП — сделать сложный код проще. Для этого программу разбивают на независимые блоки, которые мы называем объектами.
Объект — это не какая-то космическая сущность. Это всего лишь набор данных и функций — таких же, как в традиционном функциональном программировании. Можно представить, что просто взяли кусок программы и положили его в коробку и закрыли крышку. Вот эта коробка с крышками — это объект.
Программисты договорились, что данные внутри объекта будут называться свойствами, а функции — методами. Но это просто слова, по сути это те же переменные и функции.
Объект можно представить как независимый электроприбор у вас на кухне. Чайник кипятит воду, плита греет, блендер взбивает, мясорубка делает фарш. Внутри каждого устройства куча всего: моторы, контроллеры, кнопки, пружины, предохранители — но вы о них не думаете. Вы нажимаете кнопки на панели каждого прибора, и он делает то, что от него ожидается. И благодаря совместной работе этих приборов у вас получается ужин.
Объекты характеризуются четырьмя словами: инкапсуляция, абстракция, наследование и полиморфизм. Если интересно, что это такое, приглашаем в кат:
Инкапсуляция, абстракция, наследование, полиморфизм
Инкапсуляция — объект независим: каждый объект устроен так, что нужные для него данные живут внутри этого объекта, а не где-то снаружи в программе. Например, если у меня есть объект «Пользователь», то у меня в нём будут все данные о пользователе: и имя, и адрес, и всё остальное. И в нём же будут методы «Проверить адрес» или «Подписать на рассылку».
Абстракция — у объекта есть «интерфейс»: у объекта есть методы и свойства, к которым мы можем обратиться извне этого объекта. Так же, как мы можем нажать кнопку на блендере. У блендера есть много всего внутри, что заставляет его работать, но на главной панели есть только кнопка. Вот эта кнопка и есть абстрактный интерфейс.
В программе мы можем сказать: «Удалить пользователя». На языке ООП это будет «пользователь.удалить ()» — то есть мы обращаемся к объекту «пользователь» и вызываем метод «удалить». Кайф в том, что нам не так важно, как именно будет происходить удаление: ООП позволяет нам не думать об этом в момент обращения.
Например, над магазином работают два программиста: один пишет модуль заказа, а второй — модуль доставки. У первого в объекте «заказ» есть метод «отменить». И вот второму нужно из-за доставки отменить заказ. И он спокойно пишет: «заказ.отменить ()». Ему неважно, как другой программист будет реализовывать отмену: какие он отправит письма, что запишет в базу данных, какие выведет предупреждения.
Наследование — способность к копированию. ООП позволяет создавать много объектов по образу и подобию другого объекта. Это позволяет не копипастить код по двести раз, а один раз нормально написать и потом много раз использовать.
Например, у вас может быть некий идеальный объект «Пользователь»: в нём вы прописываете всё, что может происходить с пользователем. У вас могут быть свойства: имя, возраст, адрес, номер карты. И могут быть методы «Дать скидку», «Проверить заказ», «Найти заказы», «Позвонить».
На основе этого идеального пользователя вы можете создать реального «Покупателя Ивана». У него при создании будут все свойства и методы, которые вы задали у идеального покупателя, плюс могут быть какие-то свои, если захотите.
Идеальные объекты программисты называют классами.
Полиморфизм — единый язык общения. В ООП важно, чтобы все объекты общались друг с другом на понятном им языке. И если у разных объектов есть метод «Удалить», то он должен делать именно это и писаться везде одинаково. Нельзя, чтобы у одного объекта это было «Удалить», а у другого «Стереть».
При этом внутри объекта методы могут быть реализованы по-разному. Например, удалить товар — это выдать предупреждение, а потом пометить товар в базе данных как удалённый. А удалить пользователя — это отменить его покупки, отписать от рассылки и заархивировать историю его покупок. События разные, но для программиста это неважно. У него просто есть метод «Удалить ()», и он ему доверяет.
Такой подход позволяет программировать каждый модуль независимо от остальных. Главное — заранее продумать, как модули будут общаться друг с другом и по каким правилам. При таком подходе вы можете улучшить работу одного модуля, не затрагивая остальные — для всей программы неважно, что внутри каждого блока, если правила работы с ним остались прежними.
Плюсы и минусы ООП
У объектно-ориентированного программирования много плюсов, и именно поэтому этот подход использует большинство современных программистов.
- Визуально код становится проще, и его легче читать. Когда всё разбито на объекты и у них есть понятный набор правил, можно сразу понять, за что отвечает каждый объект и из чего он состоит.
- Меньше одинакового кода. Если в обычном программировании одна функция считает повторяющиеся символы в одномерном массиве, а другая — в двумерном, то у них большая часть кода будет одинаковой. В ООП это решается наследованием.
- Сложные программы пишутся проще. Каждую большую программу можно разложить на несколько блоков, сделать им минимальное наполнение, а потом раз за разом подробно наполнить каждый блок.
- Увеличивается скорость написания. На старте можно быстро создать нужные компоненты внутри программы, чтобы получить минимально работающий прототип.
А теперь про минусы:
- Сложно понять и начать работать. Подход ООП намного сложнее обычного процедурного программирования — нужно знать много теории, прежде чем будет написана хоть одна строчка кода.
- Требует больше памяти. Объекты в ООП состоят из данных, интерфейсов, методов и много другого, а это занимает намного больше памяти, чем простая переменная.
- Иногда производительность кода будет ниже. Из-за особенностей подхода часть вещей может быть реализована сложнее, чем могла бы быть. Поэтому бывает такое, что ООП-программа работает медленнее, чем процедурная (хотя с современными мощностями процессоров это мало кого волнует).
Что такое классы в объектно-ориентированном программировании
Глубокое погружение в самую сложную и неинтуитивную область программирования.
В этом цикле статей мы говорим об объектно-ориентированном программировании — передовом и очень распространённом подходе к разработке. Это стоит знать всем, кто серьёзно относится к программированию и хочет зарабатывать в этой области.
Если не читали предыдущую статью, вот краткое содержание:
Кратко: что мы уже знаем из прошлой статьи
- ООП — это подход к программированию. Такой набор практик и принципов, которыми пользуются хорошие разработчики. Противопоставление этому подходу — традиционное процедурное программирование.
- В процедурном программировании мы пишем функции, которые выполняют какие-то задачи. И при необходимости вызываем одни функции из других. В программе функции живут отдельно, данные — отдельно.
- Главная проблема процедурного программирования — сложно писать и поддерживать большие проекты. Любой мало-мальски сложный продукт будет требовать сотен функций, которые будут связаны между собой. Получится «спагетти-код».
- В ООП функции и данные группируются в объекты. Объекты более-менее независимые и общаются друг с другом по строго определённым правилам.
- Данные в ООП хранятся внутри объектов и называются свойствами объектов. Например, у объекта user может быть свойство name со значением ‘Иван’.
- Функции в ООП тоже хранятся внутри объектов и называются методами объектов. Например, у объекта user может быть метод sendEmail(), который отправляет этому юзеру письмо.
- Можно представить, что в ООП взяли «спагетти-код» с тефтелями и разложили из огромного чана порционно по контейнерам. Теперь в каждом контейнере есть спагетти и тефтели, и каждый программист может работать над своим контейнером-объектом, а не ковыряться в общем чане со спагетти.
Одно из преимуществ ООП — не нужно много раз писать один и тот же код. Можно однажды придумать какую-то красивую штуку и потом заново её использовать буквально одной строкой. Для этого и нужны классы.
Что за классы
Вот одно из формальных определений класса: «Класс — это элемент ПО, описывающий абстрактный тип данных и его частичную или полную реализацию»
Если более по-русски, то класс — это шаблон кода, по которому создаётся какой-то объект. Это как рецепт приготовления блюда или инструкция по сборке мебели: сам по себе класс ничего не делает, но с его помощью можно создать новый объект и уже его использовать в работе.
Если пока непонятно, погружайтесь в пример:
Сила примера
Призовём на помощь силу примеров и поговорим про сотовые телефоны.
Допустим, вы делаете мобильники и хотите выпустить на рынок новую модель. Чтобы люди могли сразу пользоваться вашим устройством и быстро к нему привыкли, у телефона должен быть экран, кнопки включения и громкости, камеры спереди и сзади, разъём для зарядки и слот для сим-карты.
Но одного железа недостаточно — нужно соединить его между собой так, чтобы всё работало без сбоёв. Кроме этого, нужно предусмотреть, что происходит при нажатии на кнопки, что выводится на экран и как пользователь будет управлять этим телефоном.
Следующий этап — описать каждую деталь, из которой состоит телефон, каждую микросхему и плату, и объяснить, как детали работают друг с другом. Последний шаг — написать руководство пользователя, где будет полностью рассказано, что можно делать с телефоном, как запустить звонилку или отправить смс.
Мы только что сделали новый класс для телефона — полный набор нужных знаний, описаний, свойств и инструкций, который описывает нашу модель. Все эти инструкции и описания — это ещё не телефон, но из них этот телефон можно сделать.
В программировании у класса есть наборы данных — в нашем случае это комплектующие для телефона. Ещё есть функции для работы с классами, которые называются методами — это то, как пользователь будет работать с нашим телефоном, что он будет на нём делать и каким образом.
Классы на практике
Все примеры дальше мы будем делать на Python, потому что это стильно, модно и молодёжно. А сам Python — очень объектно-ориентированный язык, почти всё в нём — это объекты. Вот и опробуем.
Как установить Python на компьютер и начать на нём писать
Допустим, мы пишем интернет-магазин с системой скидок. Нам нужно работать с пользователями — постоянными покупателями. Пользователь у нас будет объектом: у него будет имя, возраст и адрес доставки по умолчанию. Мы заведём класс, который поможет нам инициировать нового покупателя.
class User: """Класс для всех покупателей""" user_count = 0 def __init__(name, age, adress): self.name = name self.age = age self.adress = adress user.user_count += 1
Python
Copy
Здесь сказано: «Вот класс для покупателя. У него есть три свойства: имя, возраст и адрес». Теперь мы можем заводить новых покупателей одной строкой:
# Создаём первого покупателя
user1 = User ('Вася', 23,'Чебоксары')
# Создаём второго покупателя
user2 = User ('Маша', 19,'Белгород')
Объясняем объекты
Главное понятие современных подходов к программированию.
Мы тут постепенно раскладываем по полочкам объектно-ориентированное программирование. В первой части мы говорили про спагетти-код и предназначение ООП. Дальше разбирали классы — то, из чего делаются объекты. Теперь, наконец-то, сами объекты.
На примере Айфона
Вот лаборатория, конструкторское бюро. Там у нас сидят дизайнеры, инженеры и программисты. Они сделали чертежи и инструкции, как собирать новые Айфоны. Если все эти инструкции собрать, их совокупность можно назвать классом. Это такая «идея Айфона».
Из предыдущей статьи мы знаем, что в классе мы можем прописать свойства и методы.
Свойства — это, грубо говоря, данные. У Айфона может быть «свойство» в виде записанной на него операционной системы.
Методы — это, грубо говоря, «что это устройство может делать». У Айфона могут быть «методы» типа позвонить, отправить смс, запустить камеру, сделать снимок.
Всё это множество свойств и методов составляет общий класс, который мы потом используем для создания нашего воображаемого Айфона.
Когда прописан класс, завод может взять его (то есть все эти чертежи и спецификации) и сделать много копий айфонов. Каждый телефон, который выходит с завода, имеет чётко прописанные характеристики: размер памяти, диагональ экрана, цвет корпуса и так далее. Это всё было заранее продумано и записано в инструкциях по изготовлению. При этом все телефоны получаются одновременно одинаковые и разные.
Одинаковые: все телефоны одной модели не отличаются почти ничем. У них одинаковое железо, характеристики и возможности. Если взять с конвейера два случайных телефона, между ними не будет никакой разницы. Все они работают одинаково — так, как задумали проектировщики.
Разные: при этом у каждого айфона есть уникальный серийный номер и другие уникальные идентификаторы. Какие-то модели могут отличаться цветом корпуса или иметь слот на 2 сим-карты, как версии для китайского рынка.
После того как айфоны выходят с завода и попадают в руки людям, каждый телефон начинает наполняться разными данными: фото, видео, музыка, книги и программы. При обычном использовании в мире нет двух телефонов с полностью одинаковыми данными внутри. Но при этом у всех одинаково запускается камера, одинаковым способом меняются обои и отправляются смс. Это означает, что все свойства и методы работы с данной моделью одинаковы для всех телефонов этой модели. Достаточно понять, как работать с одним, — и вы понимаете, как работать со всеми точно такими же.
Вся эта аналогия с Айфонами нужна, чтобы понять смысл объектов:
- Есть класс — чертёж будущего объекта.
- На основе этого класса можно сделать объект.
- Созданный объект имеет всё, что в него вложено из класса.
- После создания с объектом можно делать всё, что в нём было заложено классом.
- Ещё с объектом можно делать всё остальное, что позволяет язык программирования.
Как создаются объекты
В общем виде на JavaScript объект задаётся вот такой конструкцией. Пока что она абстрактная, просто пример:
class MyClass prop1 = value1; // свойства класса prop2 = value2; constructor(...) { // команда-конструктор, с её помощью создаются новые объекты // содержимое конструктора } method1(...) {} // методы класса, то, что класс умеет делать, и как с ним работать method2(...) {} // ... }
Теперь сделаем конкретный класс, который отвечает за телефон:
class iPhone{ constructor(memory,color) { // при создании будем указывать размер встроенной памяти и цвет this.memory = memory; // создаём телефон с заданной памятью… this.color = color; // … и с заданным цветом whois() { // метод, который выводит информацию о телефоне console.log('Память: ' + this.memory + 'Гб. Цвет: ' + this.color); } } }
У нас появилась команда this, которая отвечает за свойства конкретного класса. С её помощью класс обращается внутри себя к нужным значениям. Ещё мы сделали метод whois, который выводит значение памяти и цвет. Каждый объект, который мы создадим на основе этого класса, будет уметь это делать — для этого достаточно написать имя объекта, поставить точку, а потом написать имя метода.
Нам осталось только создать новый объект и проверить, что за данные у него получились внутри и как работает метод:
iPhone_object = new iPhone(64,'Black'); // создаём новый объект — айфон с определённой памятью и цветом iPhone_object.whois(); // проверяем, как работает метод класса в готовом объекте
В других языках принцип создания объектов практически такой же: делаем класс, в нём описываем всё, что нужно, а потом создаём объект на основе этого класса.