Класс - описание логики поведения (через свойства и методы).
Объект - экземпляр класса, который содержит данные и может выполнять логику класса.
Интерфейс - функции, которые должен реализовать класс.
Ещё есть наследование (когда один класс расширяет другой), полиморфизм (когда класс-потомок меняет логику поведения родителя), инкапсуляция (красивое слово, которое означает "сокрытие логики" или "давай запихаем эту хрень так, чтобы её никто никогда не нашёл"). Не расстраивайтесь, если вы не знаете точного определения какого-то из модных программерских слов, в 99% случаев можете сказать, что это "хрень какая-то", не ошибётесь.
Важно помнить: в ООП данные хранятся в объектах, а логика в классах. Объекты всегда создаются из класса.
Представим себя на месте программиста, которому нужно сделать простую задачу: вывести список пользователей.
/**
* Скрипт, который выводит пользователей в виде списка
*/
// Данные пользователей
$users = [
['name' => 'Вася'],
['name' => 'Петя'],
['name' => 'Коля'],
/* ещё миллион записей */
];
// Показываем список
foreach($users as $user) {
echo $user['name'], PHP_EOL;
}
Код выше выведет список всех пользователей, даже если их миллион. Если мы хотим работать со списком в более удобном формате, то нужно сделать постраничную навигацию:
// Текущая страница и лимит записей на странице
$page = 1;
$limit = 2;
// Показываем пользователей на текущей странице
foreach (array_slice($users, ($page - 1) * $limit, $limit) as $user) {
echo $user['name'], PHP_EOL;
}
Давайте добавим в вывод ещё тэги HTML, чтобы список был более красивым:
// Показываем пользователей текущей страницы в виде HTML-списка
echo '<ol start="', $page * $limit ,'">';
foreach (array_slice($users, ($page - 1) * $limit, $limit) as $user) {
echo '<li>', htmlentities($user['name']), '</li>', PHP_EOL;
}
echo '</ol>';
Для полного счастья здесь не хватает разделения чётных и нечётных строк. Давайте добавим:
// Показываем пользователей текущей страницы в виде HTML-списка с разделением на "чётные" и "нечётные" строки
echo '<ol start="', $page * $limit ,'">';
foreach (array_slice($users, ($page - 1) * $limit, $limit) as $i => $user) {
$class = ($i + 1) % 2 ? 'odd' : 'even';
echo '<li class="', $class, '">', htmlentities($user['name']), '</li>', PHP_EOL;
}
echo '</ol>';
Как видите, код получился, мягко говоря, не очень красивый и понятный. Такой код называют "смешанный" или "грязный" (ну или "говнокод"). Тут переплетается высокоуровневая логика (показ списка пользователей, постраничная навигация) и низкоуровневое форматирование (HTML-тэги, экранирование, обращение к массиву и так далее).
А что будет, если нам скажут, что имя пользователя должно быть ссылкой? А что будет, если вместо списка потребуется таблица со множеством колонок? А что будет, если нам надо будет выгрузить список в PDF? А что будет, если ... (в этом месте можно подставить любые обстоятельства).
Если код оставить в том виде, в котором он сейчас есть, то рано или поздно он перестанет быть управляемым и начнёт диктовать разработчикам свои условия. То есть вместо того, чтобы добавлять новые плюшки, программист будет больше думать, как бы не сломать старое.
Первое, что нужно понять, ООП это лишь один из способов организации кода. С помощью объектов мы разделяем большую логику на на много маленьких, чтобы нам легче было управлять своим же кодом. Это значит, что не нужно во все места совать классы и интерфейсы. Если ООП не упрощает код, а только запутывает, то это неправильное ООП! Каждый раз спрашивайте себя: "А что я упрощу, если сделаю объект?"
Но давайте вернёмся к примеру выше и посмотрим, как можно упростить себе жизнь.
Первое, на что нужно обратить внимание, это то, что мы работаем с пользователем как с массивом:
$user['name']
А что будет, если свойств у пользователя будет много или ключи массива будут меняться? Тогда придётся где-то вести документацию, какие ключи можно использовать, а какие нет. И кто-то должен будет контролировать, чтобы программисты использовали только правильные ключи. Оказывается, эту задачу можно переложить на среду исполнения, если использовать ООП.
Давайте создадим класс пользователя, который на вход принимает данные пользователя и имеет функцию для возврата имени:
/**
* Пользовтель
*/
class User
{
private $data = [];
function __construct(array $data = []) {
$this->data = $data;
}
function name() {
return $this->data['name'] ?? null;
}
}
Исправим объявление списка:
// Данные пользователей
$users = [
new User(['name' => 'Вася']),
new User(['name' => 'Петя']),
new User(['name' => 'Коля']),
/* ещё миллион записей */
];
Теперь для доступа к имени пользователя можно использовать функцию $user->name()
. Мы "спрятали" (инкапсулировали) структуру исходного массива с данными пользователя за функциями класса User
. Если исходный массив поменяется, то изменения нужно будет внести только в функции класса.
Дальше решим проблему с постраничной навигацией. Поскольку базовый массив не поддерживает такую навигацию, "спрячем" его в объект, а уже объект "научим" выдавать страницы. Для начала опишем класс коллекции:
class Collection {
private $array = null;
function __construct(array $array = []) {
$this->array = $array;
}
// Возвращает данные на одной странице
function itemsOnPage($page, $limit) {
return array_slice($this->array, ($page - 1) * $limit, $limit);
}
}
Изменим объявление исходных данных:
// Данные пользователей
$users = new Collection([
new User(['name' => 'Вася']),
new User(['name' => 'Петя']),
new User(['name' => 'Коля']),
/* ещё миллион записей */
]);
Теперь можно обращаться к нашим данным так:
// Постраничный вывод пользователей
foreach($users->itemsOnPage($page, $limit) as $user) {
echo $user->name(), PHP_EOL;
}
Осталось решить проблему с форматом вывода пользователей. Дело в том, что echo
в этом цикле является низкоуровневой логикой, которую надо "прятать". Собственно, foreach
также является лишним звеном в этой цепочке. В идеале логику отображения нужно вынести в шаблон и передавать ему список пользователей:
// Получаем список пользователей
$users = $users->itemsOnPage($page, $limit);
// Используем шаблонизатор Twig для отображения списка
echo $twig->render('users.html', [
'start' => ($page - 1) * $limit,
'users' => $users,
]);
Шаблон представляет из себя псевдо язык разметки, в котором в теории удобно делать вёрстку:
{# Шаблон для отображения списка пользователей #}
<ol start="{{start}}">
{% for user in users %}
<li class="{{loop.index is even ? 'even' : 'odd'}}">{{user.name() | escape}}</li>
{% endfor %}
</ol>
Основная цель таких шаблонов - разделить логику отображения от логики данных. Преимущество, которое нам дают шаблоны заметны сразу: основной код стал "чище" и проще. Ходят легенды, что подобные шаблоны могут делать дизайнеры, верстальщики, папы, мамы, кошки без участия программистов. То есть налицо разделение труда (а если вспомнить историю за 5-й класс, то это уже прогресс в развитии).
Вот ситуации, когда, на мой взгляд, ООП даёт преимущества:
Я часто слышу мнение, что ООП нужно использовать везде и всегда. Нет! ООП должно помогать программисту писать код, а не превращать его жизнь в ад из "высоких стандартов опп".
Ситуации, когда ООП не даст вам преимуществ:
Не забывайте, что суть программирования - постоянный баланс между технологиями и задачами, которые надо решать. ООП лишь одна из техник написания кода, которая имеет свои достоинства и недостатки. Не нужно зацикливаться на какой-то одной технике, игнорируя преимущества других. Есть ещё и функции, и старый добрый include
. Самое главное, соблюдать баланс и писать простой и понятный код (используйте метод утёнка, чтобы объяснить логику своего кода).