Урок 34 из 84 · ООП
Концепции ООП
Содержание урока
На прошлом уроке мы подошли к основной идее ООП: повышение уровня абстракции за счёт объединения процедур и данных в "объект". Эта идея остаётся неизменной, какой бы конкретный язык, реализующий объектный подход, мы бы ни выбрали. Однако способы достичь объектности у разных языков разные. Мы будем рассматривать ООП применительно к Python, но имейте в виду, что в других языках отдельные моменты программирования в объектном стиле будут отличаться, порой — сильно!
Объекты и классы
Итак, в Python объекты — это значения, создаваемые на основе шаблона — класса. Программист описывает с помощью специального синтаксиса содержимое класса и потом во время исполнения создаёт объекты — экземпляры (instances) этого класса. У класса есть свои данные — атрибуты класса. К ним имеют доступ все экземпляры класса. При этом экземпляры имеют свои атрибуты — атрибуты экземпляра. Эти данные доступны только объекту владельцу.
Технически в Python любой объект может получить доступ к содержимому любого другого объекта, если имеет ссылку на него. Но на уровне добровольных соглашений такой доступ можно ограничивать.
Некоторые атрибуты могут быть функциями. В этом случае такие атрибуты называют методами. Вы уже пользовались методами списков и словарей, так что некоторое представление о методах у вас имеется!
Инкапсуляция
Инкапсуляция — это упаковывание данных и поведения (процедур, работающих с данными) в один объект. Инкапсуляция преследует всё ту же цель — сокрытие сложности за абстракцией. Но просто так складывать всё подряд в какой-то объект не следует. Данных и поведения должно быть столько, сколько необходимо и достаточно: объект должен хранить все свои и только свои данные самостоятельно и предоставлять все необходимые средства для манипуляции этими данными.
Представим для примера, что у нас смоделирован объект "человек". Человек может иметь домашних питомцев. Так вот с точки зрения инкапсуляции неверно будет хранить питомцев где-то в отдельном списке: питомцы принадлежат человеку, это часть данных модели человека в нашей системе. Делаем вывод: список питомцев должен быть инкапсулирован в объект "человек".
Теперь представим, что нам нужно прикреплять питомцев к уже существующему человеку. Если мы дадим прямой доступ к списку питомцев и разрешим делать с этим списком любые преобразования, возможные для обычных списков, то мы можем получить человека, питомцами которого являются два десятка чисел и строк, а также пароход, файл и None! По-хорошему, программист, работающий с нашей моделью человека, вообще не должен знать, что питомцы хранятся в списке, ему достаточно (и необходимо) метода "добавить питомца" — вот это и есть абстракция!
Полиморфизм
Полиморфизм ("многообразие форм" по-гречески) позволяет смотреть на разные объекты так, чтобы с определённой точки зрения они были похожи. Под похожестью здесь я подразумеваю одинаковое поведение, то есть возможность выполнить одни и те же действия.
Например, для того, чтобы произвести перекличку, мне достаточно знать, что все опрашиваемые субъекты могут назвать себя. И мне в данном случае не важно, у кого я спрашиваю имя — у человека, робота или говорящего динозавра.
Так вот, код, который работает с разными объектами без точного знания того, с чем он работает в данный момент, и использующий только лишь их (объектов) общие свойства, называется полиморфным. А возможность писать такой код — и есть полиморфизм.
Как вы можете уже догадаться, полиморфизм тоже связан с абстракцией: полиморфному коду достаточно знать об объектах только важные ему сведения, от остального знания код абстрагируется!
В приведённом выше примере про человека и питомцев мы можем ввести такую абстракцию: питомцем может считаться что угодно, чем человек может владеть и о чём может заботиться, получая от заботы удовольствие (очень абстрактное описание понятия "питомец", не правда ли?). Под эту абстракцию подойдёт, например, камень, или игрушечный робот — его можно любить, о нём можно заботиться (мыть камень, менять батарейки у робота). Вот такая вот забавная абстрактная модель взаимоотношений человека с питомцем.
Наследование
Наследование — свойство объектной модели устанавливать связь между классами так, что классы-потомки получают (наследуют) те же свойства и поведение, которыми обладают классы-предки. Одни и те же классы могут быть потомками одних классов и при этом являться предками для других — так получаются "иерархии классов".
Иногда такие иерархии повторяют иерархии объектов реального мира, в других ситуациях сущности, описываемые классами могут описывать что-то, не имеющее аналогов за пределами модели. Но во всех случаях мы опять же получаем инструмент для абстрагирования: если мы знаем, что некий класс реализует нужное нам поведение, то мы можем предполагать, что и все его потомки, в том числе и непрямые, будут этим поведением обладать!
Всё в том же примере системы с людьми и питомцами все питомцы (соответствующие классы) семейства кошачьих могут иметь общего предка (тоже класса) "Абстрактная Кошка". Про которую известно, что она умеет прыгать и пить молоко. Это знание позволит нам, всего лишь проверив принадлежность конкретного питомца к Абстрактным Кошкам, понять, чем мы можем питомца кормить и хватит ли невысокого забора вокруг жилища, чтобы питомец не убежал.
Тут вы можете даже увидеть связь наследования с полиморфизмом: если мы знаем, что питомец — кошка, то может статься, что большего нам знать и не нужно. Получается полиморфный код, работающий только с кошками, но зато с любыми!
ООП и реальный мир
Во многих источниках информации об ООП говорят о том, что объектный подход интуитивен, ведь он позволяет описывать реальный мир. После знакомства с подходом при подобном его позиционировании разработчик часто начинает строить иерархии классов, подобные существующим в реальности: "млекопитающие — кошачьи — тигры". Вот только у ООП нет такой цели — моделировать жизнь!
Программы пишутся для компьютеров. И пишутся в основном не биологами или астрономами, а программистами. Поэтому гораздо больше шансов встретить объект-дерево, моделирующий не "берёзу", а "красно-чёрное" дерево. Программисту нужно управлять сложностью кода, а не каталогизировать флору и фауну, вот он и абстрагирует сущности, выделяя их из кода, из алгоритмов!
ООП — это всего лишь развитие существовавших и до появления самого термина "Объектно Ориентированное Программирование" инструментов. Объекты в голове программиста существовали уже тогда, когда он использовал только структуры данных и процедуры для работы с ними. Выделение подхода ООП всего лишь систематизировало уже привычные техники, внесло общую терминологию и общие техники для решения типичных задач (те самые "паттерны проектирования").
Предметная область — те самые игра, игроки, персонажи, битвы — тоже поддаётся моделированию с помощью объектов. Но моделирование это должно делаться не для красоты самой модели, не для составления каталога игровых сущностей, а для упрощения написания кода, решающего конкретную задачу (написания игры)!
Объекты-одиночки и глобальные переменные
Модули и классы в Python являются пространствами имён. И эти пространства имён открыты для изменения. Во время работы программы вы можете добавить в модуль или класс новые атрибуты:
>>> import module
>>> module.new_attribute = 42
>>> module.new_attribute
42
>>> from classes import C
>>> C.foo = 'bar'
>>> C.foo
'bar'
Также же вы можете хранить в атрибутах изменяемые сущности — например, списки и словари — и время от времени вносить в них изменения.
>>> class D:
... L = []
...
>>> D.L.append(100)
>>> D.L
[100]
>>> import module
>>> module.X
{}
>>> module.X['k'] = 'v'
>>> module.X
{'k': 'v'}
Вы могли слышать, что пользоваться глобальными переменными плохо. Так говорят именно из-за того, что очень сложно следить за изменениями в одном месте кода и предсказывать влияние этих изменений на остальные части программы. Так вот, изменяемые (и добавляемые) атрибуты классов и модулей — это те самые глобальные переменные!
Кому-то эти возможности могут показаться очень удобными, но не нужно заблуждаться: кажущееся удобство практически всегда приводит к тому, что вы тратите много времени на поиск мест, которые внесли подобные изменения, и что-то в итоге сломалось. Ошибиться очень легко, а затронута будет большая часть программы. Ведь изменения, вносимые в класс, влияют на все экземпляры этого класса — не только на будущие, но и на уже созданные!
С модулями всё становится ещё опаснее: объект модуля всегда один на всю программу, потому что Python запоминает все модули, которые куда-то импортировались хотя бы раз, и при последующих импортах модуля возвращает ссылку на уже загруженный модуль. Соответственно, изменения, вносимые в модуль, затрагивают все модули, которые импортируют данный! Представьте, что у вас есть код, который строит логику, исходя из значения константы в атрибуте модуля, и вдруг в совершенно другом месте программы происходит изменение этой константы — такие ситуации очень тяжело отлаживать.
Поэтому заранее предостерегаю вас: не используйте изменяемые объекты-одиночки, коими являются большинство классов и все модули! Исключения возможны, но они обычно предполагают, что вы знаете, что делаете, и готовы к последствиям.
Одиночка или singleton — название приёма, который позволяет во всех местах программы работать с неким объектом, который всегда существует в единственном экземпляре, но при этом объект не передаётся явно между местами в коде, которые его используют. Пример одиночки: объект, хранящий конфигурацию программы: она загружается из внешнего источника (например, файла) при первом обращении к объекту, а затем все последующие обращения к конфигурации сразу же получают доступ к уже загруженному объекту.