Научившись строить абстракции с помощью функций, мы переходим к следующему уровню понимания: построению абстракций с помощью данных. В этом курсе мы познакомимся с некоторыми базовыми принципами проектирования программ. С тем, как моделировать и представлять в программе объекты реального (и воображаемого) мира. Примером для проектирования нам послужит создание библиотеки для работы с графическими примитивами, такими как: точки, отрезки, фигуры. Эта библиотека, с одной стороны, достаточно понятна для всех (в том числе визуально), с другой, очень просто представляется в коде. Основные темы этой части повествования:
- Предметная область (Domain Model)
- Онтология
- Уровни проектирования (Барьеры абстракции)
- Инварианты
Вторая часть курса вытекает из первой и знакомит с объектно-ориентированным программированием. Мы сможем создавать собственные типы данных и использовать их в своих программах. Эта часть курса содержит большое количество нового синтаксиса и терминов. Основные темы:
- Объекты
- Классы
- Интерфейсы
- Сокрытие данных (Data hiding)
- Инкапсуляция
- Автозагрузка
Данные темы крайне важны даже для начинающего разработчика на PHP потому, что с этими понятиями он начинает сталкиваться буквально с первых дней на новой работе. С другой стороны, требуется немало времени перед тем, как вы сможете действительно качественно использовать изучаемые подходы и техники в работе. К теме объектно-ориентированного программирования мы вернемся в наших курсах еще не раз и углубим не только наше понимание, но и разберем оставшиеся понятия вместе с их синтаксисом, например, абстрактные классы и трейты.
PHP: Введение в ООП → Онтология
Программы, которые пишут программисты, всегда создаются под определенную предметную область. Например бухгалтерский софт основывается на правилах ведения бухгалтерии, а сайт для просмотра сериалов — на понятиях из телеиндустрии, таких как “сезон” или “эпизод”. То же самое относится и ко всему остальному: бронирование авиабилетов, отелей, поиск туров, продажа и покупка автомобилей и так далее.
Понимание предметной области, для которой вы пишите программу, также важно, как и умение программировать. Это не значит, что ее нужно знать досконально - иногда область может быть по-настоящему сложной (например, та же бухгалтерия или технологическое производство), но общее понимание все же требуется.
Рассмотрим как пример Хекслет, так как вы с ним достаточно хорошо знакомы. Вы неплохо знаете его предметную область, хотя вряд ли думали о ней так, как мы сделаем сейчас. В первую очередь, для ее понимания нужно выделить ключевые понятия. У обучающих ресурсов это, как правило, “курс” и “урок”, но на самом деле их гораздо больше. В случае Хекслета еще можно выделить профессию, челендж (практика после курса), code review, квиз (набор вопросов и ответов), участника курса (вы становитесь участником когда вступаете в курс), проект. Этот список можно продолжать еще долго. Вероятно вы удивитесь, но на Хекслете более 200 подобных понятий, которые нередко называют сущностями, и все они описаны в коде. Но сущности не существуют сами по себе, они находятся в некоторых взаимоотношениях по отношению друг к другу. Например Квиз содержит (агрегирует) в себе вопросы, которые, в свою очередь, содержат в себе ответы. Профессия состоит из курсов, а курсы из уроков, уроки - из теории, квиза и практики. Эти связи имеют конкретные названия. Например один урок может находиться только в одном курсе, но курс содержит множество уроков. Такая связь называется “один ко многим” (one-to-many или o2m). В свою очередь, один курс могут проходить множество пользователей, и один пользователь может проходить много курсов. Такая связь уже называется “многие ко многим” (many-to-many или m2m). Реже встречается связь “один к одному” (one-to-one или o2o). На Хекслете такой связью связан пользователь и эксперт.
В реальности все еще чуть-чуть сложнее, потому что одна и та же сущность с точки зрения разных систем может выглядеть совершенно по-разному. Пользователь Хекслета с точки зрения бухгалтера (а у нас есть бухгалтер!) и с точки зрения ментора — две большие разницы.
Описание объектов рассматриваемой области и связей между ними называется онтологией предметной области. Эту онтологию хорошо знают эксперты соответствующей области (в бухгалтерии — бухгалтер, в обучении — преподаватель), но, в отличие от программистов, они часто представляют ее на интуитивном уровне, неформально. На практике программисты (или бизнес-аналитики и менеджеры) общаются с заказчиками, которые могут сами выступать в роли экспертов и строят вместе с ними формальную онтологию (этот процесс происходит постоянно в процессе развития проекта и не выделяется в отдельный этап проектирования). То есть выделяют конкретные термины, договариваются о том, что они означают и как связаны друг с другом. Затем, используя ER-модель, программист формирует необходимую модель данных. ER-модель используется при высокоуровневом (концептуальном) проектировании баз данных. На этом этапе уже проявляются зачатки архитектуры будущего приложения.
Кстати, далеко не всегда можно однозначно сказать, какая связь существует между двумя сущностями. Иногда программисты думают наперед и сразу формируют более сложные связи, например, m2m, а не o2m, что сказывается на сложности кода. Чем сложнее связь, тем больше кода и выше стоимость ее создания и поддержки. Сложность связей можно описать так (правее — сложнее): o2o, o2m, m2m. Иногда программисты ошибаются при выборе той или иной связи, что обычно говорит о недостаточно хорошем понимании предметной области. Приведу интересный пример. Предположим, что в системе нужно реализовать пользователя и заграничный паспорт. Интуитивно кажется, что между этими понятиями связь один к одному, ведь один пользователь может иметь один заграничный паспорт. Так? Не совсем: паспорт может поменяться, если он был утерян или закончился его срок действия. К тому же, в некоторых странах (в России недавно приняли такой закон) разрешено владение одновременно несколькими заграничными паспортами.
С другой стороны, реальный мир всегда сложнее и полнее, чем любая модель и задача программиста состоит не в том, чтобы создать универсальную и всеобъемлющую модель некоторой области, а в том, чтобы понять потребности конкретного бизнеса, выделить для них только значимые части рассматриваемой предметной области и перенести ее в код.
В зависимости от языка меняется способ представления сущностей в коде. В некоторых определяются типы (используя АТД, интерфейсы или классы), в других - структуры, а третьи вообще не предоставляют никаких вариантов, кроме словарей (ассоциативных массивов). Собственно, ООП имеет непосредственное отношение к рассматриваемой теме. Со следующего урока мы начнем изучать различные предметные области и строить подходящие модели данных, попутно изучая новые возможности PHP.
Наиболее полно рассматриваемая тема раскроется тогда, когда мы начнем изучать ORM. Сейчас достаточно того, что у вас есть общее представление о предметной области, сущностях и связях. Не забудьте почитать информацию по приведенным ссылкам - чем больше вопросов появится на этом этапе, тем лучше.
Дополнительные материалы
PHP: Введение в ООП → Точки на координатной плоскости
Одна из самых удобных тем для тренировки навыков моделирования — геометрия. В отличие от других частей математики, она представима визуально и интуитивно понятна для всех. Для начала вспомним базовые понятия, с которыми нам придется иметь дело.
Координатная плоскость (декартова) — плоскость, на которой задана система координат. Координаты задаются на двух пересекающихся под прямым углом прямых x
и y
.
6 | y
5 |
4 |
3 | . (2, 3)
2 |
1 |
|
----------------------------------------------------
| 1 2 3 4 5 6 x
|
|
|
|
|
|
Самый простой примитив, который можно расположить на плоскости — точка. Ее положение определяется двумя координатами, и в математике она записывается так: (2, 3)
, где первое число — координата по x
, а второе — по y
. В коде ее можно представить как массив, состоящий из двух элементов.
<?php
$point = [2, 3]; // x = 2, y = 3
Этого уже достаточно для выполнения полезных, с точки зрения геометрии, действий. Например, для поиска симметричной точки относительно x
, достаточно инвертировать второе число.
6 | y
5 |
4 |
3 | . (2, 3)
2 |
1 |
|
----------------------------------------------------
| 1 2 3 4 5 6 x
-1 |
-2 |
-3 | . (2, -3)
-4 |
-5 |
-6 |
<?php
$point = [2, 3]; // x = 2, y = 3
[$x, $y] = $point;
$symmetricalPoint = [$x, -$y];
Иногда бывает нужно найти точку, находящуюся между двумя другими точками ровно посередине (обычно говорят, что нужно найти середину отрезка). Такая точка вычисляется через поиск среднего арифметического каждой из координат. То есть x
такой точки равен (x1 + x2) / 2
, а y
равен (y1 + y2) / 2
.
<?php
function getMiddlePoint($p1, $p2)
{
$x = ($p1[0] + $p2[0]) / 2;
$y = ($p1[1] + $p2[1]) / 2;
return [$x, $y];
}
$point1 = [2, 3];
$point2 = [-4, 0];
print_r(getMiddlePoint($point1, $point2)); // [-1, 1.5]
Подобных операций в геометрии очень много. С точки зрения организации кода, все функции, связанные с работой точек, логично поместить в неймспейс Points.
В свою очередь, точки объединяются в отрезки. Каждый отрезок задается парой точек, одна из которых — начало, другая — конец отрезка. Какая из них - начало, а какая - конец — не важно. Отрезок в коде можно представить аналогично точке - массивом из двух элементов, в котором каждый элемент — точка.
<?php
$point1 = [3, 4];
$point2 = [-8, 10];
$segment = [$point1, $point2];
PHP: Введение в ООП → Семантика массивов
В предыдущем уроке я сказал, что массив — самый простой способ представить точку. Но правильный ли это способ? Давайте разберемся.
Когда мы говорим про конструкции языка, то, помимо синтаксиса, нужно всегда помнить о семантике, то есть о том, для решения каких задач она создана. На практике же инструменты часто используются не по назначению, что создает код, который сложнее понять и отлаживать, а значит и поддерживать.
Массив (индексированный), по своей сути, — коллекция, набор некоторых однотипных значений, подразумевающих возможность перебора и одинаковой обработки. Кроме того, эти значения друг с другом жестко не связаны и могут существовать независимо. В массиве, как правило, отсутствует позиционность, то есть жестко зафиксированные места для его значений.
Применительно к нашей графической библиотеке массив подходит, например, для хранения коллекции точек или набора отрезков. А вот точка не является коллекцией. Несмотря на то, что оба числа — координаты точки, они обозначают разные вещи: одна из них показывает значение координаты x
, другая — y
. Кроме того, они жестко связаны друг с другом, а код, который работает с конкретной точкой, всегда ожидает, что массив состоит из двух элементов, каждый из которых имеет четкую позицию. Другими словами, массив используется как структура для описания составного объекта (то есть такого, который описывается не одним значением, а несколькими, в данном случае — двумя числами-координатами).
В такой ситуации правильно использовать ассоциативный массив.
<?php
$point = ['x' => 2, 'y' => 3];
$symmetricalPoint = ['x' => -$point['x'], 'y' => $point['y']];
Кода стало чуть больше, но семантика важнее. Такой код проще понимать, потому, что вместо странных $point[0]
или $point[1]
появляются совершенно понятные с первого взгляда конструкции: $point['x']
. Даже при выводе на экран, сразу понятно, о чем идет речь. Представьте, как будет выглядеть представление отрезка на массивах. Как массив массивов. Понять, что это отрезок, нереально без понимания контекста. Единственное, что частично спасает ситуацию — хорошие имена переменных, но этого мало. Использование правильной (подходящей под задачу) структуры данных намного важнее.
<?php
$point1 = ['x' => 3, 'y' => 4];
$point2 = ['x' => -8, 'y' => 10];
$segment = ['beginPoint' => $point1, 'endPoint' => $point2];
Запомните простое правило: код, который заставляет думать (неговорящие имена, плохие абстракции, неправильные структуры данных, сильная зависимость от контекста) — плохой код (при этом важно не путать easy и simple).
Использование ассоциативного массива сразу дает еще одно крайне важное преимущество — расширяемость. Индексированный массив, используемый как структура, хрупок. Поменять местами значение аргументов нельзя - сломается весь код, который рассчитывал на определенный порядок, либо придется все переписывать. Расширить тоже просто так не получится, часть кода, конечно, продолжит работать, но часть может сломаться (например, [$x, $y] = $point
). А использование ассоциативного массива не полагается на порядок ключей и уж точно не зависит от их количества. В любой момент можно добавить новый ключ, и программа почти наверняка останется работоспособной.
Дополнительные материалы
PHP: Введение в ООП → Создание абстракции
Декартова система координат — не единственный способ графического описания. Другой способ — полярная система координат.
Полярная система координат — двухмерная система координат, в которой каждая точка на плоскости однозначно определяется двумя числами — полярным углом и полярным радиусом. Полярная система координат особенно полезна в случаях, когда отношения между точками проще изобразить в виде радиусов и углов; в более распространённой декартовой, или прямоугольной, системе координат, такие отношения можно установить только путём применения тригонометрических уравнений. © Wikipedia
Вообразите себе ситуацию. Вы разрабатываете графический редактор (photoshop!), и ваша библиотека для работы с графическими примитивами построена на базе декартовой системы координат. В какой-то момент вы понимаете, что переход на полярную систему поможет сделать систему проще и быстрее. Какую цену придется заплатить за такое изменение? Вам придется переписать практически весь код.
<?php
$point = ['x' => 2, 'y' => 3];
$symmetricalPoint = ['x' => -$point['x'], 'y' => $point['y']];
Связано это с тем, что ваша библиотека не скрывает внутреннюю структуру. Любой код, использующий точки или отрезки, знает о том, как они устроены внутри. Это относится как к коду, который создает новые примитивы, так и к коду, который извлекает из них составные части. Изменить ситуацию и спрятать реализацию достаточно просто, используя функции.
<?php
$point = makeDecartPoint(3, 4);
$symmetricalPoint = makeDecartPoint(-getX($point), getY($point));
В примере мы видим три функции makeDecartPoint
, getX
и getY
. Функция makeDecartPoint
называется конструктором, потому что она создает новый примитив, функции getX
и getY
— селекторами (selector), от слова “select”, что в переводе означает “извлекать” или “выбирать”. Такое небольшое изменение ведет к далеко идущим последствиям. Главное, что в прикладном коде (том который использует библиотеку) отсутствует работа со структурой напрямую.
<?php
// То есть мы работаем не так
$point = [1, 4]; // мы знаем что это массив
print_r($point[1]); // прямое обращение к массиву
// А так
$point = makeDecartPoint(3, 4); // мы не знаем как устроена точка
print_r(getY($point)); // мы получаем доступ к частям только через селекторы
Глядя на код, даже нельзя сказать, что из себя представляет точка с точки зрения конструкций языка (для этого можно воспользоваться отладочной печатью). Так мы построили абстракцию данных. Суть этой абстракции, заключается в том, что мы скрываем внутреннюю реализацию. В англоязычной литературе для этого используется термин “data hiding”. А вот один из способов реализовать абстракцию для работы с точкой:
<?php
function makeDecartPoint($x, $y)
{
return [
'x' => $x,
'y' => $y
];
}
function getX($point)
{
return $point['x'];
}
function getY($point)
{
return $point['y'];
}
Теперь мы можем менять реализацию без необходимости переписывать весь код (однако, переписывание частей все же может понадобиться). Несмотря на то, что мы используем функцию makeDecartPoint
, которая создает точку на основе декартовой системы координат (то есть принимает на вход координаты x
и y
), внутри она вполне может представляться в полярной системе координат. Другими словами, во время конструирования происходит трансляция из одного формата в другой.
<?php
function makeDecartPoint($x, $y)
{
// конвертация
return [
'angle' => atan2($y, $x),
'radius' => sqrt($x ** 2 + $y ** 2)
];
}
Начав однажды работать через абстракцию данных, назад пути нет. Придерживайтесь всегда тех функций, которые вы создали сами, либо тех, которые вам предоставляет используемая библиотека.
PHP: Введение в ООП → Интерфейсы
В it широко распространен термин “Интерфейс”, который по смыслу похож на то, как мы используем это слово применительно к различным объектам нашей жизни. Например, пользовательский интерфейс представляет собой совокупность элементов управления сайтом, банкоматом, телефоном и так далее. Интерфейсом пульта управления от телевизора являются кнопки. Интерфейсом автомобиля можно назвать все рычаги управления и кнопки. Резюмируя, можно сказать, что интерфейс определяет способ взаимодействия с системой.
Создание грамотных интерфейсов не так уж просто, как может показаться на первый взгляд. Я бы даже сказал, что это крайне сложно. Каждый день своей жизни мы встречаемся с неудобными интерфейсами, начиная от способов открывания дверей и заканчивая работой лифтов. Чем сложнее система (то есть, чем больше возможных состояний), тем сложнее сделать интерфейс. Даже в примитивном примере с кнопкой включения телевизора (два состояния - вкл/выкл), можно реализовать либо две кнопки, либо одну, которая ведет себя по разному в зависимости от текущего состояния.
В программировании все устроено похожим образом. Интерфейсом называют набор функций (имена и их сигнатуры, то есть количество и типы входящих параметров, а также возвращаемое значение), не зависящих от конкретной реализации. Такое определение один в один совпадает с понятием абстрактного типа данных. Например, для точек интерфейсными являются все функции, которые мы реализовывали в практике, и которые описывались в теории. Но функции бывают не только интерфейсные, но и вспомогательные, которые не предназначены для вызывающего кода и используются исключительно внутри абстракции.
<?php
function makeUser($name, $birthday)
{
return [
'name' => $name,
'birthday' => $birthday
];
}
function getAge($user)
{
return calculateAge($user['birthday']);
}
function isAdult($user)
{
return getAge($user) >= 18;
}
// Внутренняя функция не являющаяся частью интерфейса абстракции User
function calculateAge($birthday)
{
$secondsInYear = 31556926;
return floor((time() - strtotime($birthday)) / $secondsInYear);
}
В сложных абстракциях (которые могут быть представлены внешними библиотеками), количество не интерфейсных функций значительно больше, чем интерфейсных. Вплоть до того, что интерфейсом библиотеки может являться одна-две функции, но в самой библиотеке их сотни. То, насколько хороша ваша абстракция, определяется в том числе тем, насколько удобен ее интерфейс.
Как вы увидите позже, в PHP существует конструкция имеющая имя Interface
. Она используется для явного описания интерфейсов, а также выполняет ряд дополнительных функций. Подробнее о ней поговорим позже, когда дойдем до классов.
PHP: Введение в ООП → Уровневое проектирование
Рассмотрим еще одну простую систему — рациональные числа и операции над ними. Напомню, что рациональное число — это число, которое может быть представлено в виде дроби, где a — это числитель дроби, b — знаменатель дроби. Причем b не должно быть нулём, поскольку деление на ноль не допускается.
Рациональные числа в PHP не поддерживаются, поэтому построить абстракцию для них придется самостоятельно. Как обычно, нам понадобятся конструктор и селекторы:
<?php
$num = makeRational(1, 2); // одна вторая
$numer = getNumer($num); // => 1
$denom = getDenom($num); // => 2
С помощью трех функций мы определили рациональное число. Одна функция собирает его из частей, другие позволяют каждую часть извлечь. Чем при этом, с точки зрения языка, является $num
— абсолютно не важно. Хоть функцией (и такое возможно), хоть массивом (индексированным или ассоциативным). Можно даже использовать строки:
<?php
function makeRational($numer, $denom)
{
return "{$numer}/{$denom}";
}
function getNumer($rational)
{
return explode('/', $rational)[0];
}
function getDenom($rational)
{
return explode('/', $rational)[1];
}
print_r(makeRational(10, 3)); // => 10/3
Несмотря на то, что мы научились представлять рациональные числа, эта абстракция сама по себе малоприменима. Абстракция становится полезна тогда, когда появляется возможность оперировать ей. Для рациональных чисел базовыми операциями можно считать арифметические, например, сложение, вычитание или умножение. Умножение рациональных чисел — самая простая операция. Для ее выполнения нужно перемножить числители и знаменатели:
3/4 * 4/5 = (3 * 4)/(4 * 5) = 12/20
Самое интересное начинается в процессе реализации. Если предположить, что реальная структура рационального числа выглядит так: ['numer' => 2, 'denom' => 3]
, то, чисто технически, решение может быть таким:
<?php
function mul($rational1, $rational2)
{
return [
'numer' => $rational1['numer'] * $rational2['numer'],
'denom' => $rational1['denom'] * $rational2['denom']
];
}
С точки зрения вызывающего кода все нормально, абстракция сохранена. На вход в mul
подаются рациональные числа, на выходе — рациональное число. А вот внутри никакой абстракции нет, обращение с рациональными числами строится на основе знания их устройства. Любое изменение внутренней реализации рациональных чисел потребует переписывания всех операций, работающих с рациональными числами напрямую, без селекторов и конструкторов. Данный код нарушает принцип одного уровня абстракции (single layer abstraction).
При разработке сложных систем используется подход — уровневое проектирование . Он заключается в том, что системе придается структура при помощи последовательных уровней. Каждый из уровней строится путем комбинации частей, которые на данном уровне рассматриваются как элементарные. Части, которые строятся на каждом уровне, работают как элементарные на следующем уровне.
Уровневое проектирование пронизывает всю технику построения сложных систем. Например, при проектировании компьютеров резисторы и транзисторы сочетаются (и описываются при помощи языка аналоговых схем), и из них строятся и-, или- элементы и им подобные, служащие основой языка цифровых схем. Из этих элементов строятся процессоры, шины и системы памяти, которые в свою очередь служат элементами в построении компьютеров при помощи языков, подходящих для описания компьютерной архитектуры. Компьютеры, сочетаясь, дают распределенные системы, которые описываются при помощи языков описания сетевых взаимодействий, и так далее. © SICP
<?php
function mul($rational1, $rational2)
{
return makeRational(
getNumer($rational1) * getNumer($rational2),
getDenom($rational1) * getDenom($rational2)
);
}
В нашем примере базовым уровнем являются типы, встроенные в сам язык; на их основе сформирован уровень для представления рациональных чисел, а затем - уровень, на котором реализованы арифметические операции над рациональными числами. Такая же структура и у абстракции для работы с графикой.
PHP: Введение в ООП → Инварианты
Абстракция, благодаря data hiding, позволяет нам не думать о деталях реализации и сосредоточиться на ее использовании. Более того, при необходимости реализацию абстракции можно всегда переписать, не боясь сломать использующий ее код (почти). Но есть еще одна важная причина, по которой нужно использовать абстракцию — соблюдение инвариантов.
Инвариант в программировании — логическое выражение, определяющее непротиворечивость состояния (набора данных).
Разберемся на примере. Когда мы описали конструктор и селекторы для рациональных чисел, то неявно подразумевали выполнение следующих инвариантов:
<?php
$num = makeRational($numer, $denom);
$denom == getDenom($num); // => true
$numer == getNumer($num); // => true
Другими словами, мы ожидаем, что, передав в конструктор числитель и знаменатель, мы получим их (те же числа), если применим селекторы к этому рациональному числу. Именно так определяется корректность работы данной абстракции. Практически этот код является тестами.
Инварианты существуют относительно любой операции, и иногда довольно хитрые. Например, рациональные числа можно сравнивать между собой, но не прямым способом, потому, что одни и те же дроби можно представлять разными способами: 1/2
и 2/4
. Код который не учитывает этого факта, работает некорректно.
<?php
$num1 = makeRational(2, 4);
$num2 = makeRational(8, 16);
print_r($num1 == $num2); // => false
Задача приведения дроби к нормальной форме называется нормализацией . Реализовать ее можно разными способами. Самый очевидный — выполнять нормализацию во время создания дроби, внутри функции makeRational
. Другой — выполнять нормализацию уже при обращении через функции getDenom
и getNumer
. Последний способ обладает недостатком — вычисление нормальной формы происходит на каждый вызов. Избежать этого можно, используя технику мемоизации.
Учитывая новые вводные, становится понятно, что инвариант, связывающий конструктор и селекторы, нуждается в модификации. Функции getDenom
и getNumer
должны вернуть не переданные значения, а значения после нормализации (если дробь уже нормализована, то это будут те же самые значения).
<?php
$num = makeRational(10, 20);
getDenom($num); // => 1
getNumer($num); // => 2
Как бы там ни было, становится понятно, что абстракция не только прячет от нас реализацию, но и отвечает за соблюдение инвариантов. Любая работа в обход абстракции чревата тем, что не будут учтены внутренние преобразования. Многие языки в своем арсенале имеют средства для data hiding . Эти механизмы защищают данные от прямого доступа. В PHP за это отвечают модификаторы доступа к свойствам объектов public
, protected
и private
, с которыми мы скоро познакомимся.
Data hiding можно организовать и без специальных средств, только за счет функций высшего порядка. Данный способ основан на создании абстракций только с помощью анонимных функций, замыканий и передачи сообщений (подробнее в SICP).
Хочу сразу предостеречь вас от следования культу карго. Несмотря на то, что идея с data hiding выглядит очень здраво, в реальности подобные механизмы крайне легко обходятся с помощью Reflection API и даже без них, просто за счет ссылочных данных. Поэтому подобная защита — она больше “от дурака”. Второй момент связан с тем, что в мире немало языков (пример - JavaScript), в которых все нормально с абстракциями, но нет механизмов для data hiding и ничего страшного не произошло. Другими словами, на практике, при использовании абстракций, никто особо и не пытается их нарушать специально. И я склоняюсь к мысли, что значение принудительного data hiding сильно преувеличено.
PHP: Введение в ООП → Структуры
Рассмотренные темы: модели данных, абстракция с помощью данных, data hiding (для соблюдения инвариантов) неспецифичны для PHP. Они касаются всех в одинаковой степени и в большинстве языков могут быть реализованы исключительно с помощью функций. С другой стороны, почти каждый язык предоставляет готовые механизмы для моделирования и создания абстракций с помощью данных. В PHP для этого используются классы.
С этого момента мы начнем погружаться в тему Объектно-ориентированного программирования, и c ней сопряжены определенные сложности. Во-первых, ООП в PHP содержит невероятно большое количество новых конструкций (а значит - синтаксиса) и терминов, с которыми нам предстоит познакомиться. Во-вторых, за всеми ими прячется идея или идеи, которые очень сложно рассмотреть сквозь нагромождение кода. В-третьих, вокруг ООП ходит множество домыслов и философии, искажающих реальное положение вещей. По этой причине, наше погружение пойдет медленно. Я специально буду опускать некоторые детали или недоговаривать до поры, чтобы не нагружать вас лишней информацией, сбивающей с толку. Если у вас уже есть какой-то опыт или вы набрались знаний в интернете, то, вероятно, в вашей голове сформировалась некоторая модель, сквозь которую вы смотрите на ООП. Велика вероятность того, что эта модель идет в разрез с последующим материалом.
Кроме того, многие тратят годы на то, чтобы понять ООП, и не всегда понимают. Неизбежна ситуация, когда вам будет казаться, что вы все поняли, а через минуту - что вы вообще ничего не поняли. Мозгу нужно время и, главное, практика для адаптации. Самый лучший способ по настоящему понять ООП — изучить и прорешать вторую главу в SICP
В предыдущих уроках мы познакомились с универсальным способом реализовывать абстракции — ассоциативным массивом. Он не требует написания никакого дополнительного кода, его можно брать и заполнять так, как нам вздумается в зависимости от ситуации. С другой стороны, он не обеспечивает типобезопасности. Так как любая сущность представляется этим массивом, то по ошибке можно вызывать функцию, предназначенную для одной абстракции, например, точки, на другой абстракции, например, сегменте.
<?php
$num1 = makeRational(10, 20);
// Этот код выполнится, но в $num2 окажется что-то странное
$num2 = makeRational($num1, 20);
Что при этом произойдет - непонятно и зависит от того, насколько удачно совпали структуры. И если такое произошло, то функция внезапно может отработать без ошибок и даже что-то вернуть. В итоге программа продолжит работать некорректно, вместо того, чтобы завершиться с ошибкой. Кроме того, не существует универсального способа проверить тип данных, так как все есть массив. Описанные проблемы далеко не всегда являются проблемами. То есть нет движения за отказ от ассоциативных массивов. Но есть ситуации, где действительно лучше использовать специализированные средства.
Отдельного внимание заслуживают языки со статической типизацией. В этих языках нельзя просто так взять и начать использовать ассоциативный массив как в динамических языках. Любые составные данные должны быть типизированы и описаны, причем до начала использования. По этой причине в таких языках для работы с составными данными используются, например, записи, структуры или классы.
Перед тем как переходить непосредственно к объектам, посмотрим на структуры языка СИ, так вы лучше поймете происходящее. К счастью, их синтаксис достаточно прост и нагляден.
Примеры несколько упрощены, так как использование указателей, в данном случае, помешает нам понять суть.
// Описание структуры Точка
typedef struct
{
int x;
int y;
} point;
Здесь typedef
и struct
ключевые слова языка, а point
— название, которое я выбрал самостоятельно для именования структуры. x
и y
— элементы (члены) структуры.
Структура всегда имеет название, которое может являться ее типом как в примере выше (благодаря typedef). В теле структуры (все что между {}
) описываются элементы структуры, имена и их типы.
int main()
{
// Создание переменной p1 типа point.
point p1 = { .x = 0, .y = 1 };
// Печать на экран. Доступ к частям структуры происходит через точку p1.x и p1.y
printf("%d", p1.x); // => 0
printf("%d", p1.y); // => 1
// Изменение значения в структуре
p1.x = 20;
p1.y = -10;
}
Доступ к элементам структуры происходит через точку. Например, если в программе присутствует переменная p1
, являющаяся структурой point
, то для чтения x
нужно написать p1.x
, а для чтения y
— p1.y
. Практически то же самое и в случае присваивания - для изменения x
нужно написать p1.x = 5
.
Структура, как и ассоциативный массив, не дает data hiding. Создание функций для конструирования, извлечения и модификации структуры — целиком и полностью ответственность программиста. И, также, как и ассоциативный массив, тип “структура” в СИ — рекурсивная (древовидная) структура данных. Другими словами, элементом структуры может быть другая структура.
typedef struct
{
point center; // point это структура
int radius;
} circle;
int main()
{
// При создании круга одновременно создается точка
circle c1 = { .center = { .x = 3, .y = 3 }, .radius = 10 };
// Доступ к вложенным членам происходит через дополнительные точки
printf("%d", c1.center.x); // => 3
printf("%d", c1.radius); // => 10
// изменение значения в структуре
c1.center.y = 20;
c1.radius = -10;
}
С помощью структур в СИ описывают практически любые композитные (составные) данные. Посмотрев на определения структур, можно сказать, с чем работает данная программа, какие сущности в ней выделены и как они связаны между собой.
PHP: Введение в ООП → Классы
Аналогом структур из СИ в PHP являются классы (как вы увидите позже, классы устроены намного сложнее). По крайней мере, в первом приближении.
<?php
// Обратите внимание на стиль - где ставятся открывающие и закрывающие скобки:
class Point
{
public $x;
public $y;
}
Определение класса подозрительно похоже на определение структуры. За ключевым словом class
следует имя класса, затем в фигурных скобках перечисляются элементы класса. Если в структурах их элементы назывались членами, то в PHP их принято называть свойствами. Такое именование характерно для большинства классовых языков. В PHP классы должны начинаться с заглавной буквы.
Одно из отличий классов от структур связано с наличием встроенного механизма data hiding. Ключевое слово public
делает свойства публичными, то есть доступными снаружи для чтения и модификации. Это поведение аналогично тому, как ведут себя элементы структур. Кроме public
есть и другие варианты, но мы их рассмотрим позже, когда поговорим об инкапсуляции и методах.
Определив класс, можно начать создавать объекты или, как их еще называют, экземпляры (instance) класса. На текущий момент достаточно рассматривать объект как конкретную структуру данных с конкретными данными.
<?php
// Создаем объект типа Point
$point = new Point();
// По умолчанию значения равны null
print_r($point->x); // => null
print_r($point->y); // => null
// Обратите внимание на синтаксис. Такой вызов неверный: $point->$x.
$point->x = 5;
$point->y = 10;
print_r($point->x); // => 5
print_r($point->y); // => 10
Создание объекта выглядит как вызов функции, к которому добавили ключевое слово new
, и, как вы увидите позже, это так и есть. В остальном все работает как и в структурах, только для разделения используется не точка, а стрелка. При обращении к свойствам, знак $
перед именем свойства не ставится.
Попробуйте создать данный класс на repl.it. Создайте несколько экземпляров, распечатайте их, измените свойства.
Если распечатать объект на экран print_r($point)
, то можно увидеть его структуру и значения всех свойств.
Point Object
(
[x] => 5
[y] => 10
)
Классы как рекурсивная структура данных
Как и в случае со структурами, значением свойства объекта может быть другой объект. Ограничений на вложенность никаких нет: объекты, содержащие объекты, которые содержат объекты — это нормально.
<?php
class Circle
{
public $center;
public $radius;
}
$circle = new Circle();
$circle->radius = 3;
$circle->center = new Point();
$circle->center->x = 5;
$circle->center->y = 10;
print_r($circle->center->x); // => 5
print_r($circle->radius); // => 3
Типы данных
В PHP около 10 встроенных типов данных, с большинством которых мы уже знакомы, например со строками или массивами. Объекты в этом списке представлены типом object
.
<?php
gettype($circle); // object
gettype($point); // object
С другой стороны, каждый класс может рассматриваться как пользовательский тип данных, а его объекты-значения (инстансы) - как данные этого типа. Далее в процессе обучения я использую понятие “тип” как синоним понятия “класс”. На синтаксическом уровне классы, наравне с обычными типами, могут использоваться для описания входных и выходного типов данных функций.
<?php
function showUser(User $user) {
// ...
}
Такое определение вызовет ошибку при передаче в функцию любых посторонних данных.
Вывод
Классы — основной способ описывать программные абстракции в PHP; следовательно, объекты — основной способ их использования. Знакомиться с этими понятиями непросто из-за обилия новых терминов, конструкций языка, того, что PHP, во многих аспектах, с объектами, ведет себя не так, как с другими данными. Но просто выучить эти особенности недостаточно для понимания того, что такое ООП и как писать в этом стиле. Эту ситуацию можно сравнить с игрой в шахматы. Знание того, как ходят фигуры, не делает из вас шахматиста. Обучение самой игре - процесс долгий и достаточно сложный. Большая часть этого курса посвящена изучению базовых правил, а вот практика отрабатывается дальше, в курсах, посвященных веб-разработке и ORM. Поэтому не переживайте, что, даже зная, как описывать классы и создавать объекты, вы еще некоторое время не будете понимать, как строить полноценные программы.
PHP: Введение в ООП → Автозагрузка классов
Принято определять ровно один класс на файл. Более того, в этом файле больше не может быть никаких инструкций, не считая определения неймспейса. Чисто технически, язык не запрещает нарушать это правило, но лучше следовать стандартам кодирования.
Классы немного по-другому работают с неймспейсами. Если неймспейс содержит только функции, то его определение обычно оканчивается именем файла (без расширения). Во всех наших упражнениях именно такая структура.
<?php
// file: src/solution.php
namespace App\solution;
function ...
В случае с классами неймспейс не содержит имени файла. Его роль выполняет само название класса. Причем файл должен называться в точности как класс и с учетом регистра.
<?php
// file: src/Point.php
namespace App;
class Point
{
}
Использовать этот класс в другом неймспейсе можно так:
<?php
namespace AnotherApp;
use App\Point;
$point = new Point();
либо так:
<?php
namespace AnotherApp;
$point = new \App\Point();
Полное описание требований к тому, как правильно стилистически именовать классы и как соотносить их с файловой структурой, приведено в стандарте PSR4. Этот стандарт важно соблюдать по двум причинам. Первая связана с единым подходом к именованию и формированию структуры, что позволяет легко ориентироваться в проектах. Но есть и другая, не менее важная причина — автозагрузка классов.
Если определенный неймспейс в PHP содержит только функции, то для его загрузки используется специальная секция autoload/files
в файле composer.json
.
"autoload": {
"files": [
"src/Points.php",
"src/Segments.php"
]
}
Эту секцию вы могли видеть практически в каждой практике на Хекслете. Composer требует перечисления всех таких файлов, и только в этом случае он загрузит их автоматически. Причем произойдет это в любом случае, не важно, используются функции этих неймспейсов или нет.
<?php
// Эта строчка приводит к загрузке всех файлов, указанных в секции files
require __DIR__ . '/vendor/autoload.php';
С классами ситуация другая. PHP содержит специальный механизм автозагрузки классов. Этот механизм работает так: если интерпретатор наталкивается на использование класса, то он проверяет, определили ли вы автозагрузчик классов, и, если определили, то вызывает его (пример). Composer определяет такой загрузчик автоматически. Его можно конфигурировать с помощью файла composer.json
. Если структура классов в вашем приложении соответствует PSR4, то конфигурация минимальна. Стандарт PSR4 задает стиль именования, позволяющий однозначно определять полное имя класса (включая неймспейсы) на основании пути до файла (относительно корня проекта) и наоборот.
FULLY QUALIFIED CLASS NAME | NAMESPACE PREFIX | BASE DIRECTORY | RESULTING FILE PATH |
---|---|---|---|
\Acme\Log\Writer\File_Writer | Acme\Log\Writer | ./acme-log-writer/lib/ | ./acme-log-writer/lib/File_Writer.php |
\Zend\Acl | Zend | /usr/includes/Zend/ | /usr/includes/Zend/Acl.php |
\Symfony\Core\Request | Symfony\Core | ./vendor/Symfony/Core/ | ./vendor/Symfony/Core/Request.php |
{
"autoload": {
"psr-4": {"App\\": "src/"}
}
}
В данном примере указано, что в папке src
относительно расположения файла composer.json
находится неймспейс App
, соответствующий стандарту PSR4. В этом случае Composer только регистрирует автозагрузчик, который подключает файлы с классами по необходимости, только во время их использования.
Все примеры на Хекслете используют автозагрузку классов. Рекомендую подглядывать в файл composer.json
каждой практики и анализировать содержимое. Кроме того, понимать автозагрузку лучше всего не через внимательное чтение документации, а через эксперименты. Попробуйте самостоятельно собрать простой php пакет и создайте внутри него классы. Не забывайте, что всегда можно подглядывать в наш шаблон.
Дополнительные материалы
PHP: Введение в ООП → Свойства
Свойства класса иногда называют аттрибутами или полями класса, но общепринятым все же является термин “свойства”. Объявление свойств по умолчанию устанавливает в них значение null
.
<?php
class Point
{
public $x;
public $y;
}
$p = new Point();
var_dump($p);
// class Point#1 (2) {
// public $x =>
// NULL
// public $y =>
// NULL
// }
Это объявление может содержать инициализацию значением, которое устанавливается в момент загрузки класса в память. Инициализация не накладывает никаких ограничений на изменение свойств в дальнейшем.
<?php
class User
{
public $children = [];
public $status = 'approved';
}
$user = new User();
print_r($user->children); // => []
По историческим причинам свойства в PHP можно определять с помощью ключевого слова “var”, но делать этого не нужно. По своему действию “var” аналогичен “public” и существует в языке только для обратной совместимости (с версиями языка < 5).
Свойства классов хоть и описываются в классе, но не принадлежат ему. То есть каждый объект при создании получает свою собственную копию свойств. В этом смысле поведение абсолютно аналогично структурам.
<?php
$user1 = new User();
$user1->status = 'declined';
$user2 = new User();
print_r($user2->status); // approved
Динамическое обращение к свойствам
В некоторых ситуациях имя свойства, к которому нужно обратиться, задается динамически и хранится в переменной. В такой ситауции можно использовать специальный синтаксис обращения к свойству:
<?php
$propertyName = 'key';
$obj->$propertyName = 'value';
$obj->$propertyName; // value
$obj->key; // value
Дополнительные материалы
PHP: Введение в ООП → Указатели
Мы привыкли к тому, что данные в PHP всегда передаются по значению. Так происходит и при присваивании, и при передаче данных в функции.
<?php
$a = 5;
$b = $a;
$a = 4;
print_r($b); // => 5
Это правило применимо ко всем данным без исключения. Если мы хотим сделать передачу по ссылке, то нужно использовать &
.
<?php
$a = 5;
$b =& $a;
$a = 4;
print_r($b); // => 4
Но объекты ведут себя подобно передаче по ссылке даже без &
.
По этой причине многие считают, что передача объектов всегда происходит по ссылке. Это не верно. Тот механизм, который используется для передачи объектов, внешне ведет себя точно так же, как и передача по ссылке, но это другой механизм.
<?php
$p1 = new Point();
$p1->x = 3;
$p1->y = 5;
$p2 = $p1;
$p2->x = 100;
print_r($p1->x); // => 100
Когда создается объект, в переменную записывается не он сам, а указатель (pointer) на него. Указатель можно воспринимать как идентификатор (номер) объекта, находящегося где-то в памяти. Когда мы присваиваем переменной объект $p2 = $p1
, то происходит копирование этого идентификатора, но сам он не меняется. Другими словами, идентификатор всегда указывает на тот же самый объект. Поэтому создается впечатление, что объекты передаются по ссылке как при присваивании, так и при передаче объектов в функции.
<?php
function setX($point, $x)
{
$point->x = $x;
}
$point = new Point();
$point->x = 3;
$point->y = 4;
setX($point, 8);
print_r($point->x); // => 8
По этой причине, работа с объектами резко отличается от того, что мы изучали раньше, например, функции, меняющие объекты, редко что-то возвращают наружу. И эти же функции почти никогда не бывают чистыми, ведь, меняя объекты, они влияют на внешнее окружение. Популярные языки программирования строят работу с объектами на основе императивной парадигмы, что, в общем случае, не обязательно. С другой стороны, есть ряд задач, в которых удобнее работать с объектами в декларативном стиле, например, при обработке коллекций. Чуть позже мы познакомимся с этим подходом.
Дополнительные материалы
PHP: Введение в ООП → Сравнение объектов
Сравнение объектов обладает некоторыми особенностями, о которых надо знать. Главное правило сравнения состоит в том, что объекты разных типов никогда не равны. Здесь никаких сюрпризов.
<?php
$p = new Point();
$s = new Segment();
$s == $p; // => false
$s === $p; // => false
Если же тип один и тот же, то возникает две ситуации: одна для оператора нестрогого сравнения и другая — для строгого.
Нестрогое сравнение (==)
Два объекта считаются равными, если они имеют одинаковые свойства и их значения совпадают.
<?php
$p1 = new Point(3, 9);
$p2 = new Point(3, 9);
$p1 == $p2; // => true
Но что будет, если значением свойства объекта является другой объект?
<?php
$s1 = new Segment(new Point(1, 3), new Point(8, 5));
$s2 = new Segment(new Point(1, 3), new Point(8, 5));
$s1 == $s2; // => true
Если вложенные объекты совпадают по правилу описанному выше, то исходные объекты также считаются равными. Другими словами, правило — рекурсивно, и проверка идет по всем вложенным объектам.
Строгое сравнение (===)
Строгое сравнение, напротив, проверяет только совпадение значения указателей. Объекты равны строго, только если это один и тот же объект.
<?php
$p1 = new Point(3, 9);
$p2 = new Point(3, 9);
$p1 === $p2; // => false
$p3 = $p1;
$p3 === $p1; // => true
Свое сравнение
На практике, все же, объекты устроены сложнее и сравнивать их стандартными средствами не получается. Например, сравнение может происходить на основании идентификаторов, взятых из базы данных. В таких случаях остается только один способ — написать свою собственную функцию (или метод) сравнения. Подробнее о методах далее в курсе.
PHP: Введение в ООП → Конструктор
Структура в СИ может быть инициализирована значениями прямо при создании.
int main()
{
// Создание переменной p1 типа point.
point p1 = { .x = 0, .y = 1 };
}
Объекты в PHP тоже могут быть инициализированы при создании, но для этого придется внести изменения в класс — написать функцию-конструктор. Конструктор класса похож на тот конструктор, который мы реализовывали, знакомясь с абстракциями. Разница только в том, что в случае классов конструктор находится внутри класса, а не описывается как обычная функция снаружи.
<?php
class Point
{
public $x;
public $y;
public function __construct($x, $y)
{
$this->x = $x;
$this->y = $y;
}
}
$point = new Point(1, 10);
$point->x; // => 1
$point->y; // => 10
Конструктор класса — функция с именем __construct
. Эту функцию невозможно вызвать напрямую. Она вызывается автоматически во время создания объекта new Point(5, 3)
, а параметры, переданные в этот вызов, сразу попадают в конструктор. Это значит, что, если в классе определен конструктор с двумя обязательными параметрами, то создание объекта всегда потребует два обязательных параметра. Сам по себе конструктор подчиняется тем же правилам, что и обычные функции, например, при необходимости можно указать значения по умолчанию. PHP, в силу динамической природы, допускает создание ровно одного конструктора на класс.
Конструктор не может и не должен ничего возвращать (технически вы можете написать
return
, но этот возврат никем не используется).
Самое интересное происходит внутри конструктора. Во-первых, конструктор вызывается тогда, когда объект уже создан. Этот объект доступен внутри конструктора как переменная $this
. Такое поведение может показаться странным, ведь, глядя на эту конструкцию $point = new Point(1, 10)
, видно, что правая часть выполняется раньше присваивания, а значит и конструктор вызывается раньше. Ответ кроется в том, какая логика скрывается за оператором new
. Процесс создания объекта внутри PHP выглядит следующим образом:
- Создается объект без вызова чего-либо. Технически, внутри интерпретатора создается та самая структура из языка СИ (PHP написан на СИ).
- Вызывается конструктор, в который передается объект. Синтаксически этой передачи не видно, но она есть. Можно сказать, что объект попадает внутрь конструктора как нулевой параметр, то есть внутри PHP для нашего класса, определенного выше, сигнатура конструктора выглядит так:
__construct($this, $x, $y)
. - Объект
$this
наполняется в соответствии с кодом, находящимся в теле функции__construct
. Так как объект всегда передается по указателю, то возврат из конструктора не нужен. - Объект возвращается наружу. В этот момент отрабатывает присваивание.
Основная задача конструктора — заполнить свойства объекта переданными параметрами. Этот способ сложнее, чем тот, который мы рассматривали в структурах (где ничего не надо определять, а можно заполнять при конструировании), и вот почему:
- Свойства могут быть защищены от прямого изменения или чтения. Этот вопрос рассматривается позже.
- Свойства могут требовать дополнительных расчетов, как, например, в ситуации с рациональными числами, которую мы рассматривали ранее, где происходило преобразование входных координат в другой способ представления.
- В конструкторе можно выполнять различные побочные эффекты, например, читать файлы или выполнять сетевые вызовы (открыть соединение с базой данных). С этой возможностью нужно быть очень осторожным. Побочные эффекты резко усложняют код и затрудняют тестирование.
Дополнительные материалы
PHP: Введение в ООП → Инкапсуляция
В общепринятом ООП есть один термин, которым любят пугать новичков. Имя ему — инкапсуляция.
В первой части этого курса мы строили абстракции, используя обычные функции с применением подхода data hiding.
<?php
$point = makeDecartPoint(3, 4);
getX($point); // => 3
getY($point); // => 4
В Объектно-ориентированном подходе, функции объединяются с данными и описываются вместе внутри класса (в классово-ориентированных языках). Инкапсуляция - механизм, позволяющий описывать данные и функции, оперирующие ими, в рамках одной языковой конструкции. В случае PHP, такой конструкцией является класс.
Мы уже начали так делать, когда знакомились с конструктором. Такие функции принято именовать методами, так как они связаны с объектом, на котором вызываются. Визуально вызов метода выглядит как обращение к свойству и его вызов.
<?php
$point = new Point(3, 4);
$point->getX(); // => 3
$point->getY(); // => 4
Вызов метода не требует передачи объекта в аргументах, потому что метод вызывается на объекте и имеет к нему доступ через переменную $this
.
<?php
class Point
{
public $x;
public $y;
public function __construct($x, $y)
{
$this->x = $x;
$this->y = $y;
}
public function getX()
{
return $this->x;
}
public function getY()
{
return $this->y;
}
}
Методы, которые извлекают составные части объекта, принято называть геттерами (getters), а методы, изменяющие составные части — сеттерами (setters). Как правило, геттеры и сеттеры один в один отображаются на конкретные свойства внутри объекта. Технически, методы — обычные функции, за исключением доступа к $this
и способа вызова.
<?php
class Point
{
public $x;
public $y;
public function __construct($x, $y)
{
$this->x = $x;
$this->y = $y;
}
public function getX()
{
return $this->x;
}
public function getY()
{
return $this->y;
}
public function setX($x)
{
$this->x = $x;
}
public function setY($y)
{
$this->y = $y;
}
}
$point = new Point(5, 10);
$point->setX(100);
print_r($point->getX()); // => 100
Сеттеры в примере выше показаны только для демонстрации. В реальном коде точка почти наверняка будет неизменяемым объектом.
Но геттеры и сеттеры - не единственные типы функций, которые позволяет описывать класс. В принципе, все, что мы описывали работая без классов, с таким же успехом описывается и с классами.
Реализация без классов:
<?php
function distanceTo($point1, $point2)
{
$squareOfX = (getX($point1) - getX($point2)) ** 2;
$squareOfY = (getY($point1) - getY($point2)) ** 2;
return sqrt($squareOfY + $squareOfX);
}
Реализация в классе:
<?php
class Point
{
...
/*
* Рассчет по теореме пифагора связи между
* сторонами прямоугольного треугольника с^2 = a^2 + b^2
*/
public function distanceTo($point)
{
$squareOfX = ($this->getX() - $point->getX()) ** 2;
$squareOfY = ($this->getY() - $point->getY()) ** 2;
return sqrt($squareOfY + $squareOfX);
}
}
$point1 = new Point(0, 0);
$point2 = new Point(3, 4);
print_r($point1->distanceTo($point2)); // => 5
print_r($point2->distanceTo($point1)); // => 5
Данная операция обладает свойством коммутативности. Результат вычисления не зависит от того, в каком порядке идут аргументы. Соответственно, при использовании методов, можно вызывать distanceTo
как на одном объекте, так и на другом.
Нередко методы выполняют не только вычисления, но и возвращают новые объекты. Например, так произойдет при вычислении симметричной точки.
<?php
class Point
{
...
public function getSymmetricalPoint()
{
// Можно выполнять промежуточное создание переменной, а можно возвращать сразу
return new Point(-$this->getX(), -$this->getY());
}
}
$point = new Point(3, 8);
$point->getSymmetricalPoint(); // => (-3, -8)
Выше мы рассмотрели техническую сторону вопроса, оставив за кадром описание преимуществ и недостатков такого похода, а также связанные темы, например, data hiding или полиморфизм. Их описание довольно обширно и практически бесполезно без хотя бы минимального опыта использования. О том, что дает или забирает инкапсуляция, я расскажу на протяжении ближайших уроков. Отдельного обсуждения заслуживает вопрос о способе хранения методов - где они находятся физически (внутри объекта или нет?). С ним мы разберемся в уроках, посвященных полиморфизму и динамической диспетчеризации.
Дополнительные материалы
PHP: Введение в ООП → Data Hiding (Data Protection)
Как я уже упоминал, в терминологии ООП творится довольно серьезная путаница. Она возникает, в первую очередь, из-за того, что многие программируют либо на одном языке, либо если и на разных, то часто схожих по структуре языках. Соответственно, происходит профессиональная деформация, когда программист видит мир сквозь призму одного языка. Одна из таких историй происходит вокруг инкапсуляции и data hiding. Напомню, что data hiding - подход, при котором нельзя изменить данные напрямую, в обход интерфейса, тем самым нарушив инварианты (такое происходит не всегда). Есть языки, в которых присутствует data hiding, например, haskell, но нет инкапсуляции. В ООП data hiding появляется благодаря двум возможностям:
- инкапсуляции
- области видимости свойств
Однако учтите что в литературе часто отождествляют термины инкапсуляция и защита данных. Поэтому не пугайтесь если многие вокруг вас будут утверждать что инкапсуляция это про защиту данных, но даже не вспомнят про объединение функций и данных в рамках одной структуры
Достаточно изменить слово public
на private
у любого свойства, как пропадет возможность обращаться к нему напрямую снаружи объекта.
<?php
class Point
{
private $x;
private $y;
public function __construct($x, $y)
{
// Внутри по прежнему доступ есть
$this->x = $x;
$this->y = $y;
}
public function getX()
{
return $this->x;
}
public function getY()
{
return $this->y;
}
}
$point = new Point(10, 8);
print_r($point->getX()); // => 10
$point->x; // PHP Fatal error: Uncaught Error: Cannot access private property Point::$x<Paste>
Подчеркну, что речь идет именно о доступе снаружи. Внутри он должен остаться, иначе как мы сможем оперировать этим свойством?
Data hiding считается важным аттрибутом любой абстракции, независимо от того, работаем мы в ООП стиле, или нет. Именно по этой причине существуют геттеры. В ООП, построенном на классах, вообще не принято обращаться к свойствам напрямую. Геттеры - первое, что реализуется при описании любого нового класса. Кстати, в языке Ruby нельзя (один способ есть, но он выходит за рамки обсуждаемой темы) обратиться к свойству объекта без геттера, но описываются они там значительно проще и компактнее, чем в таких языках, как PHP или Java, и выглядят как обращения к свойствам (в ruby можно не ставить скобки при вызове функций). Тоже самое касается и сеттеров. Свойства напрямую не редактируют, так как потенциально можно нарушить инварианты.
PHP: Введение в ООП → Изменяемость
Сеттеры (setters) служат для изменения внутреннего состояния объекта. Как и геттеры, они именуются особым образом. К сеттерам обычно добавляется префикс set
, если этот сеттер что-то устанавливает и add
- если добавляет.
<?php
$point1 = new Point(10, 11);
$point2 = new Point(-3, 3);
$segment = new Segment($point1, $point2);
$segment->getStartPoint(); // (10, 11)
$segment->setStartPoint(new Point(3, 8)); // Допустимо, потому что new Point(3, 8) - выражение
$segment->getStartPoint(); // (3, 8)
На практике изменения объектов происходят почти всегда с помощью сеттеров, и крайне редко - через изменение свойства напрямую. Причем объекты (впрочем, как и любая абстракция) иногда хранят внутри себя свойства, которые нельзя изменять снаружи (например, соединение с базой данных), и для них не делают сеттеров.
Вообще говоря, с сеттерами связано много головной боли. Несмотря на data hiding, встроенный в объекты, как я уже говорил ранее, можно легко создать ситуацию, в которой из одного объекта извлекается другой объект и меняется. Естественно, исходный объект об этих изменениях ничего не знает. Проблема обостряется тогда, когда один объект используется по всему приложению. В такой ситуации он ведет себя как глобальная переменная (в худшем ее проявлении). Изменения, сделанные в одном месте, коснутся всего.
Например, ранее вы создали класс для работы с рациональными числами. Если бы методы add
и sub
изменяли объект, на котором вызываются, то получить неверные рассчеты стало бы крайне просто. Достаточно использовать одно рациональное число в нескольких местах, и любое его изменение повлияет на всех, кто использует это число. Абсолютно такая же ситуация и с графическими примитивами на плоскости.
<?php
$point1 = new Point(10, 11);
$point2 = new Point(-3, 3);
$segment1 = new Segment($point1, $point2);
$segment2 = new Segment($point1, $point2);
// Функция moveUp перемещает весь отрезок на три значения вверх. Она не возвращает новый сегмент, а мутирует сам объект.
$segment1->moveUp(3);
print_r($segment1); // => [(10, 14), (-3, 6)]
Если внутри moveUp
происходит изменение точек (вместо создания новых), то такое изменение повлияет и на segment2
, хотя мы и не собирались его перемещать.
Другой яркий пример - Маркдаун.
Для генерации маркдауна, вообще говоря, достаточно обычной функции:
<?php
$html = generateHtml($markdown);
Иногда поведение генератора надо менять, а у него довольно много разных опций, например, нужно ли вырезать опасные теги и вообще - разрешать ли использовать теги:
<?php
$html = generateHtml($markdown, ['sanitaize' => 'true']);
По-прежнему используется функция, и с кодом все хорошо. Но, если опций много и они одинаковые по всему приложению (во всех местах, где происходит генерация), то появляется много дублирования с передачей этих опций на каждый вызов. Эту задачу можно решить двумя способами. Один основан на генерации функции, которая замыкает внутри себя опции (такое решение популярно в js). Второй - можно использовать объект как способ хранить состояние.
<?php
$markdown = new Markdown($options);
$html = $markdown->render($text);
Теперь достаточно прокинуть в нужную часть приложения объект $markdown
, вызвать метод render
- и больше не беспокоиться об опциях. Но обязательно найдется место, в котором понадобится особое поведение. И некоторые создатели подобных библиотек пытаются решить возникшую проблему, добавив сеттер на изменение опций, заложенных в объект.
<?php
$markdown->setOptions(['sanitize' => false]);
$html = $markdown->render($text);
Такой код создает потенциальную огромную дыру, которую очень сложно отловить. Так как сеттер меняет состояние объекта, а объект - общий для разных частей программы, то все вызовы метода render
после вызова setOptions
будут основываться на новых опциях, хотя изначально мы хотели поменять поведение только для одного места.
Существует ли способ сделать все красиво? Нет, фундаментальная проблема “изменяемое состояние” может быть убрана только отказом от изменения и созданием нового на основе старого вместо мутаций. Последний прием подходит не всегда, но мы уже использовали его на практике, например, в рациональных числах.
PHP: Введение в ООП → Магические методы (__toString)
Некоторые методы в классах имеют специальное предназначение и часто называются “магическими”. Их легко отличить от других методов наличием двух подчеркиваний в начале имени метода. К таким методам относится и конструктор. Другим полезным и часто используемым методом является __toString
. Его “магичность” заключается в том, что он вызывается автоматически, в тех ситуациях, когда объект используется как строка. К таким ситуациям относится интерполяция или конкатенация. Результат вызова этого метода используется как строковое представление объекта.
<?php
class Point
{
public $x;
public $y;
public function __construct($x, $y)
{
$this->x = $x;
$this->y = $y;
}
public function getX()
{
return $this->x;
}
public function getY()
{
return $this->y;
}
public function __toString()
{
return "({$this->x}, {$this->y})";
}
}
$point = new Point(1, 10);
// Автоматически вызывается __toString
echo $point; // => (1, 10)
// и тут
$message = 'hello, ' + $point;
// и тут
$message2 = "hi, {$point}";
Но этот же метод не вызовется если передать объект в print_r
. Эта функция всегда пытается отобразить внутреннее представление того, что ей передали.
__toString
должен вернуть строку, иначе произойдет ошибка. Ошибка возникнет и в том случае, когда у объекта нет метода toString
.
<?php
echo $o;
// PHP Catchable fatal error: Object of class stdClass could not be converted to string
Дополнительные материалы
PHP: Введение в ООП → Константы классов
Напомню, что в PHP есть такая конструкция как константа. Она используется для хранения каких-то постоянных данных, которые, как правило, глобальные. Например, константа PHP_VERSION
содержит версию PHP, в которой был запущен код (а его можно запустить на разных версиях интерпретатора). Эта константа относится к предопределенным (предоставляется интерпретатором). PHP позволяет создавать свои собственные константы, используя конструкцию const SEC_PER_DAY = 86400;
. Эти константы принадлежат неймспейсу и могут быть импортированы из него, используя конструкцию use const App\Times\SEC_PER_DAY;
.
<?php
// Класс встроен в язык http://php.net/manual/ru/class.datetime.php
class DateTime
{
const RSS = 'D, d M Y H:i:s O';
const RFC822 = 'D, d M y H:i:s O';
}
Внутри классов константы определяются точно таким же способом, как и снаружи. Основное отличие проявляется в способе доступа.
<?php
print_r(DateTime::RSS); // => D, d M Y H:i:s O
Синтаксически обращение происходит с использованием двух двоеточий после имени класса, за которыми, в свою очередь, идет имя константы. Обратите внимание на отсутствие знака $
. Для констант классов нет особого синтаксиса импорта. Он не нужен по очень простой причине - импортируется в другие неймспейсы всегда класс, а константа извлекается уже из него.
Внутри класса к константе можно обратиться ровно таким же образом, но есть и другой способ:
<?php
class DateTime
{
/* ... */
public function getRssFormat()
{
return self::RSS;
}
}
В этом способе вместо имени класса слева стоит ключевое слово self
. Его удобство заключается в отсутствии дублирования имени класса. Эта проблема особенно актуальна при активном использовании констант внутри самого класса.
Предопределенные константы
Внутри класса определено несколько магических констант:
-
__CLASS__
- текущее имя класса -
__METHOD__
- текущее имя метода
В отличии от обычных констант, магические не требуют префикса self::
и доступны только внутри класса.
<?php
class Example
{
public function printMethodName()
{
print_r(__METHOD__);
}
}
Кроме магических, в классах есть одна специальная константа class
. Она возвращает полное имя класса и может вызываться только через класс.
<?php
namespace App;
class User
{
}
User::class // => App\User
Эта константа возвращает полное имя класса относительно неймспейса, в котором происходит ее вызов.
Модификаторы доступа
С версии PHP 7.1 константы обрели возможность иметь модификатор доступа. По умолчанию константы публичные, но если очень хочется, можно их сделать приватными.
<?php
class DateTime
{
private const RSS = 'D, d M Y H:i:s O';
public const RFC822 = 'D, d M y H:i:s O';
}
Итого
Даже в такой простой штуке, как константы, зарыто много разных возможностей (и не факт, что это хорошо). Не пытайтесь их запомнить. В реальности все это используется не так часто, и, когда вам понадобятся константы, то вы легко найдете всю необходимую информацию в официальной документации. Основная цель урока - показать, как бывает.
В каких случаях нужно использовать константы? В ситуациях, когда с данным типом (классом) связана некоторая важная и статическая информация (то есть не меняющаяся). Для класса DateTime
такими константами являются различные форматы даты, закрепленные разными стандартами. В целом константы не влияют на архитектуру приложения и вообще не относятся к ООП. Это просто удобное (в рамках ООП модели PHP) добавление, полезное в некоторых ситуациях.
Дополнительные материалы
PHP: Введение в ООП → Статические свойства
В классовых языках, таких как PHP, часто встречается понятие “статика”. Статикой называют статические свойства либо статические методы. Что это и зачем нужно — далее в уроке.
Хочу сразу расставить точки над i. Статические свойства и методы не являются фундаментальной фичей объектно-ориентированного программирования, скорее наоборот - они появляются вследствие ограничений, накладываемых классами. При этом они довольно часто используются и поэтому включены в базовый курс по ООП.
Статическое свойство, в отличие от обычного свойства, принадлежит классу, а не инстансу. С точки зрения синтаксиса отличие только лишь в дополнительном ключевом слове static
.
<?php
namespace App;
class User
{
public static $table = 'users';
}
Если попробовать обратиться к нему через объект, используя ->
, то возникнет ошибка.
<?php
$user = new User();
$user->table; // PHP Notice: Accessing static property App\User::$table as non static
Статическое свойство не часть объекта, то есть не часть его состояния. Основной способ обращения к статическому свойству похож на то, как мы обращались к константам. Статические свойства, подобно обычным свойствам, имеют область видимости. Их всегда можно сделать приватными.
<?php
print_r(User::$table); // => users
В отличии от констант, свойства требуют наличия знака $
. Только не перепутайте: $table
— это не имя переменной, а имя статического свойства.
К свойствам все же можно обращаться, используя объекты, но это лишь синтаксический сахар . Объект в таком вызове используется только как способ понять, что это за тип.
<?php
$user = new User();
$user::$table; // в реальности вместо $user подставляется класс
Такая возможность открывает доступ к полиморфизму , о котором мы еще будем разговаривать.
К статическим свойствам можно обращаться не только снаружи, но и внутри объектов этого же типа. Как и в случае констант, есть два способа. Первый показан выше, когда мы указываем полное имя класса, второй способ использует уже знакомый нам self
:
<?php
class User
{
public static $table = 'users';
public function getTable()
{
return self::$table;
}
}
$user = new User();
$user->getTable(); // users
Зачем?
Основная цель статических свойств — хранить информацию о типе в целом, безотносительно его экземпляров. Таким приемом часто пользуются для связи сущностей предметной области с базой данных. Например, в статическом свойстве удобно (но не всегда правильно) хранить имя соответствующей таблицы в базе данных. В случае с User
выше, таблица называется users
. Подобная возможность активно используется в ORM (фреймворк для отображения сущностей предметной области из базы в код и обратно).
В идеале статические свойства класса должны инициализироваться при старте программы и никогда не меняться. Ведь статическое свойство по своим характеристикам является глобальной переменной. Если сохранять туда временные данные и менять их, то очень просто создать трудноподдерживаемый код с большим числом ошибок.
По этой причине статические свойства часто делают приватными, а доступ снаружи оставляют через статические методы , о которых мы поговорим в следующем уроке.
Дополнительные материалы
PHP: Введение в ООП → Статические методы
Статические методы — это почти то же самое, что и статические свойства, только методы.
<?php
class User
{
private static $table = 'users';
public static function getTable()
{
return self::$table;
}
}
User::getTable(); // users
Статические методы, как и свойства, не принадлежат экземплярам, они — часть типа. Следовательно, из статического метода невозможно получить доступ к объекту (ведь нет никакого объекта) через $this
. $this
внутри него просто не существует. Статические методы могут обращаться к другим статическим методам или статическим свойствам, используя self
.
Как я уже упоминал в предыдущем уроке, статические методы часто используют для доступа к приватным статическим свойствам. Причем, как геттеры, так и сеттеры, которые нужны редко, но все же бывают нужны.
Но есть еще один способ использования статических методов, не связанный со статическими свойствами. Их используют как способ создать объект вместо прямого вызова конструктора через оператор new
.
Как вы помните, PHP (как, впрочем, и любой динамический язык) позволяет иметь ровно один конструктор для класса. В случае таких данных, как время, это - серьезное ограничение, потому, что нельзя одним конструктором описать все возможные способы создания дат, которые используются в коде.
<?php
$date = new DateTime('2000-01-01');
В стандартной библиотеке PHP есть класс DateTime
, который принимает на вход строчку определенного формата и возвращает соответствующий объект. А что, если в нашей программе формат времени другой? А если у нас вообще нет строчки, а есть отдельные числа? Естественным желанием было бы иметь разные конструкторы под разные задачи. Их у нас нет, но зато есть статические методы, которых можно создать столько, сколько нужно.
<?php
// Специальная библиотека для работы с датами будет рассматриваться в следующем курсе
$vancouverTimeRightNow = Carbon::now('America/Vancouver'); //implicit __toString()
$noonTodayLondonTime = Carbon::createFromTime(12, 0, 0, 'Europe/London');
$internetWillBlowUpOn = Carbon::create(2038, 01, 19, 3, 14, 7, 'GMT');
Как видно из кода выше, статические методы имеют разные сигнатуры, но внутри они, так или иначе, вызывают конструктор, передавая туда уже подготовленные параметры. Конструктор можно вызывать двумя способами: первый — использовать полное имя класса, второй — через self
. Второй способ предпочтительнее просто потому, что позволяет не дублировать имя класса.
<?php
class Carbon
{
public static function now($timezone = '')
{
return new self(date("Y-m-d H:i:s"), $timezone);
}
}
Подводя итог, можно сказать, что статические методы используют как фабрику объектов в случаях, когда создание объекта достаточно сложное и требует некоторых манипуляций.
Еще есть третий способ использования статических методов — глобальные функции неймспейса . Такой способ особенно популярен в языках типа Java, где физически невозможно создать функцию вне класса. В PHP очень похожая модель, и, хотя создавать функции можно в обычных неймспейсах, по факту делают так редко. Одна из причин связана с наличием автозагрузки классов, такой способ банально удобнее с точки зрения лени. С точки зрения “правильности” такой код скорее “неправильный”. Если статическая функция не порождает объектов данного типа, или хотя бы не использует статические свойства, то непонятно, почему она вообще оказалась в этом классе. Но это в теории. На практике есть устоявшиеся нормы и традиции. В своей практике, работая в проектах, построенных на классовой модели (не все проекты в PHP разрабатываются именно так), вы будете встречать код, который почти всегда принадлежит тому или иному классу.
Дополнительные материалы
PHP: Введение в ООП → Интерфейсы (Конструкция языка interface
)
Вместе с классами в PHP широко используется языковая конструкция “интерфейс”. В этом уроке мы рассмотрим техническую сторону вопроса, а потом поговорим о смысле. Про последнее я сейчас могу сказать немного, потому что полноценный разговор про суть интерфейсов у нас пойдет во время изучения полиморфизма в последующих курсах. А пока достаточно иметь общее представление об интерфейсах, так как без них не получится окунуться во фреймворки.
Интерфейс в PHP — конструкция языка, описывающая абстрактный тип данных (АТД). Напомню, что АТД определяет набор операций (функций), независимых от конкретной реализации типа (в нашем случае класса) для манипулирования его значениями. На практике интерфейсы содержат определения функций (то есть описание их сигнатур) без их реализации.
Хотя данная конструкция для нас в новинку, само понятие интерфейса используется на протяжении всего курса. В первую очередь это касается рассуждений о рассматриваемом типе. Для оперирования точками на плоскости нам не нужна “реализация” точек. Достаточно того, что мы представляем их визуально и знаем операции, выполняемые над ними. То же самое касается и более базовых концепций, например, чисел и любых арифметических операций. Задумывались ли вы над тем, как на самом деле выполняются арифметические операции? Ответ на этот вопрос гораздо сложнее, чем может показаться на первый взгляд, и он зависит не только от языка, но и от конкретного аппаратного обеспечения (железа). Однако незнание ответа не мешает нам пользоваться числами, строками и массивами, не зная их устройства.
<?php
// file: DecartPointInterface.php
namespace App;
// Интерфейсы, по аналогии с классами, хранятся в своих собственных файлах
// и загружаются автоматически при следовании стандарту PSR4.
// Имя интерфейса может быть любым, главное - соответствие PSR4.
interface DecartPointInterface
{
public function __construct($x, $y);
public function getX();
public function getY();
}
То, что раньше мы описывали словами и держали в голове, теперь явно записано в виде кода. Декартова точка — это АТД с тремя операциями:
- Создание точки из двух значений
- Извлечение координаты X
- Извлечение координаты Y
По сути, прикладному коду больше ничего знать о точках и не нужно. Естественно, если нам понадобятся новые операции, то мы всегда можем их добавить, тем самым расширив интерфейс. Свои собственные АТД можно менять как угодно и когда угодно, только учтите, что изменение интерфейса влечет за собой исправления кода, использующего этот интерфейс.
Сама по себе конструкция Interface
никак не влияет на остальной код. Недостаточно просто создать интерфейс, в этом нет смысла. Интерфейс должен быть реализован , и тогда он начнет приносить пользу.
<?php
namespace AnotherApp;
// Импорт интерфейса
use App\DecartPointInterface;
class DecartPoint implements DecartPointInterface
{
private $x;
private $y;
// Интерфейсные функции
public function __construct($x, $y)
{
$this->x = $x;
$this->y = $y;
}
public function getX()
{
return $this->x;
}
public function getY()
{
return $this->y;
}
// Не интерфейсные функции
public function __toString()
{
return "({$this->getX()}, {$this->getY()})";
}
}
Реализация интерфейса происходит за счет ключевого слова implements
, за которым идет название интерфейса. Интерпретатор проверяет, чтобы в классе были описаны все функции интерфейса и их сигнатуры совпадали, а если это не так, то возникает ошибка. Реализация интерфейса никак не ограничивает возможности по наполнению класса, другими словами, вы можете определять и добавлять в класс все, что хотите, помимо интерфейсных функций.
Сами по себе интерфейсы мало полезны. Например, можно в любой программе открыть все файлы с классами и удалить часть определения класса, которая описывает реализацию интерфейсов (слово implements
и то что идет за ним). После этого не изменится ровным счетом ничего - программа продолжит выполняться так же, как и выполнялась. Но ситуация меняется, если использовать интерфейс в сигнатурах функций и методов вместо классов.
<?php
function compare(DecartPointInterface $point1, DecartPointInterface $point2)
{
// ...
}
Во время выполнения программы PHP проверяет, реализует ли класс соответствующий интерфейс, и если нет, то возникает ошибка. Причем проверка идет именно на наличие записи implements
в определении класса, а не на факт того, что методы определены (проверка реализации интерфейса гарантирует это).
Такая запись позволяет коду завязываться не на конкретную реализацию точек, а на их интерфейс. Это - ключевая мысль, которую имеет смысл обсуждать подробнее вместе с полиморфизмом.
Countable
В PHP встроен интерфейс Countable, а функция count
умеет работать с любым объектом, реализующим этот интерфейс.
Дополнительные материалы
PHP: Введение в ООП → Плюсы и минусы разных способов организации абстракций
Теперь, после того, как мы немного поработали с объектами, давайте попытаемся ответить на вопрос: “какую такую задачу они решают, которую не решают абстракции на основе обычных функций + ассоциативный массив как структура”?
Изложенные ниже тезисы могут показаться вам совсем чуждыми и непонятными в силу отсутствия опыта. В этом нет ничего страшного, главное - увидеть направления, а отработкой мы займемся в следующих курсах.
Перед тем как начать уходить глубже в тему объектов, хочу вас предостеречь. ООП в современном мире воспринимается большим числом программистов (особенно начинающими), как серебряная пуля, как средство от всех болезней. Учитывая, что в PHP это основной способ строить абстракции, у вас может сложиться такое же впечатление. Но это не так. Во-первых, под ООП понимают две абсолютно разные концепции. Та, которую мы обсуждаем, является мейнстримом, и встроена во многие языки настолько глубоко, что писать в другом стиле либо невозможно, либо очень тяжело. Но есть и другая, созданная Аланом Кеем . Что интересно, именно Алан является создателем термина ООП, но его ООП не имеет почти ничего общего с тем, что сейчас называется ООП.
ООП для меня это сообщения, локальное удержание и защита, скрытие состояния и позднее связывание всего. Это можно сделать в Smalltalk и в LISP. Alan Key
Во-вторых, существуют другие способы получить поведение, похожее на то, которое вы будете наблюдать в ООП-коде. Более того, многие из них значительно мощнее в возможностях (и некоторые появились задолго до ООП-языков). Например, мультиметоды в языке Clojure дают большую свободу (мультидиспетчеризацию) и позволяют строить полиморфизм на специализированных функциях.
В общем и целом, чем больше разных по структуре языков и парадигм вы знаете, тем лучше понимаете, что происходит. Рекомендую: clojure, haskell, elixir, kotlin и js (последний стандарт).
Преимущества
- Пожалуй, основное преимущество связано с полиморфизмом подтипов . Подробно эта тема освещается позже. Сейчас лишь скажу, что если мы вызываем функцию, то это всегда некоторая конкретная функция, импортированная из конкретного неймспейса. А вот если мы вызываем метод, то появляются варианты. Когда интерпретатор доходит до кода с вызовом метода
$obj->methodCall()
, он не может сразу сказать, где определен данный метод, потому что ответ на этот вопрос зависит от того, какой тип у$obj
. Отсюда следует, что, если разные объекты содержат методы с одинаковым именем (и сигнатурой), то их можно прозрачно (для вызывающего кода) подменять. На практике такая возможность местами упрощает код (становится меньше условных конструкций), но главное — делает его расширение проще. - Работа с абстракцией, основанной на ассоциативном массиве, таит в себе один сюрприз. Так как любая сущность представляется этим массивом, то можно по ошибке вызывать функцию, предназначенную для одной абстракции (например, точки), на другой абстракции (например, сегменте). Что при этом произойдет — непонятно, и зависит от того, насколько удачно совпали структуры. И если такое произошло, то функция внезапно может отработать без ошибок и даже что-то вернуть. В итоге программа продолжит работать некорректно, вместо того, чтобы завершиться с ошибкой. Инкапсуляция исключает подобную ситуацию. Вызываемый метод всегда принадлежит тому объекту, на котором он вызывается. Если метода нет, то будет ошибка, если есть — то он отработает так, как и должен отработать. Но это преимущество является преимуществом только при сравнении с абстракциями, построенными на общих структурах данных (и в динамических языках), такими, как ассоциативные массивы. В языках с развитой системой типов (но без ООП), например, в haskell, подобной проблемы также нет.
<?php
$segment = makeSegment(makePoint(1, 3), makePoint(10, 11));
// Функция отработает, хотя в нее передали сегмент, а не точку
getX($segment); // getX - функция, предназначенная для работы с точками
- Это преимущество немного неожиданно. Возможность вызывать методы у объектов открывает дорогу к автокомплиту в редакторах. Да-да! Именно благодаря такому способу вызова редактор может подсказать список методов, которые есть у данного объекта. Если вы сначала пишите функцию, а затем передаете туда данные, то вы должны знать про существование функции заранее. Но тут нужно оговориться. Вызов функции после данных не является прерогативой ООП. В некоторых современных языках (Nim, D, Rust) поддерживается Unified Function Call, который позволяет проделывать такой же трюк с обычными функциями. Ниже пример на языке Nim.
# Создается тип Vector, представляющий из себя кортеж из двух элементов
type Vector = tuple[x, y: int]
# Определяется функция, принимающая на вход два вектора и возвращающая новый вектор,
# полученный сложением исходных векторов
proc add(a, b: Vector): Vector =
(a.x + b.x, a.y + b.y)
let
# Создается переменная v1, содержащая вектор
v1 = (x: -1, y: 4)
# Создается переменная v2, содержащая вектор
v2 = (x: 5, y: -2)
# Обычный вызов функции
v3 = add(v1, v2)
# Вызов через точку: v1 передается в функцию add первым параметром
v4 = v1.add(v2)
# Цепочка вызовов. Результат предыдущего вычисления всегда передается первым параметром в следующий
v5 = v1.add(v2).add(v1)
- Реализация ООП в PHP содержит конструкции для обеспечения data hiding . Справедливости ради скажу, что, несмотря на это, их всегда можно обойти (например, используя Reflection API ). Причем не только в PHP, но и в других языках с похожей моделью, например, в Java. С другой стороны, практика показывает, что отсутствие таких механизмов не создает больших проблем при работе. К таким языкам относится JavaScript.
Недостатки
- Инкапсуляция. Как и всегда в инженерной деятельности, за возможности нужно платить. Инкапсуляция, при всех своих плюсах, создает огромную проблему. Расширяемость поведения объекта падает до нуля. Если мы работаем с обычными функциями, то достаточно написать новую функцию, чтобы можно было продолжать работать. Когда речь заходит про инкапсуляцию, то все не так. Дело в том, что методы описываются в классах. В PHP класс можно описать ровно один раз. И большая часть этих классов приходит в проекты из сторонних библиотек. Как только понадобится расширить поведение любого стороннего класса, мы сразу сталкиваемся с проблемами. Любой код из библиотек поставляется как есть, и мы не можем открыть исходный код любой библиотеки и внести необходимые нам правки. По этой причине расширяемость поведения объектов в ООП языках — головная боль. Как правило, создатели класса пытаются заботиться об этом сами, давая возможность расширять свое поведение снаружи (если это возможно). Существуют языки, в которых эту проблему пытаются решать, позволяя “дописывать” определение класса по ходу работы программы, - например, в Ruby. В js то же самое достигается за счет механизма прототипов. Языки, в которых функции и данные разделены не имеют подобного недостатка, и код на них пишется, как ни странно, легче и проще (хотя местами многословнее). Сюда же можно отнести проблему, называемую антипаттерном (плохой реализацией) god object. Если вы уже немного знакомы с ООП, то можете подумать, что наследование спасает от этой проблемы. Так вот, наследование не просто не спасает, но и само по себе является проблемой. Об этом я расскажу в соответствующем курсе, когда мы разберем суть наследования как отношения между типами, и ограничениями, без которых наследование невозможно.
- Представление любой мало-мальской сущности в языке с помощью пользовательского типа сильно раздувает программу. А сущности часто создают только по той причине, что не нашлось подходящего под нее типа (в котором логично было бы описать ее). Существует миф о том, что программы, написанные в ООП-стиле (на самом деле имеется ввиду та самая модель ООП, которая используется в языках типа PHP или Java), при больших размерах оказываются относительно компактными по объему кода. В реальности все с точностью до наоборот. Программы на языке Clojure компактнее аналогов на PHP во много раз. И чем больше кода, тем больше разрыв. Эта тема настолько животрепещущая, что кто-то не поленился и создал проект-шутку FizzBuzzEnterpriseEdition. К тому же, появляются проблемы с ответственностями. Собака должна есть еду (
$dog->eat($food)
), или же еда съедается собакой ($food->eatBy($dog)
)? Несмотря на кажущуюся абсурдность, подобная проблема реальна и проявляется очень часто. - Думаю, что влияние этого пункта вы уже ощутили на себе, хотя мы только начали. Слишком много языковых сущностей (Бритва Оккама). В PHP постоянно добавляют новые возможности по реализации ООП. Вот лишь некоторые из них: абстрактные классы, анонимные классы, интерфейсы, статические методы, видимость методов, видимость свойств, видимость констант, трейты, магические методы, наследование. И это только ключевые слова. А все эти механизмы могут взаимодействовать друг с другом, порождая неведомые комбинации, у каждой из которых есть свое особенное поведение и ограничения. В итоге одно и то же поведение можно реализовать десятком разных способов. Приходится знать тысячи нюансов и постоянно решать споры о том, какой подход лучше.