Основы объектно-ориентированного программирования в PHP

Как и большинство современных языков программирования, язык PHP является объектно-ориентированным. Что же это означает? Объектно ориентированное программирование, или ООП - это подход, при котором основными элементами программы являются классы, интерфейсы и объекты.

ООП зиждется на трёх принципах, которые зовутся Инкапсуляция, Наследование и Полиморфизм. Эти понятия будут расшифрованы ниже. А пока - расскажу о базовых терминах:

Классы, интерфейсы, объекты

Класс - это описание некоторой сущности на языке программирования. Классы заключают в себе описание нижеследующего:

  • Членов данных, или полей. По сути, поле данных - это переменная, объявленная внутри класса. В членах данных хранится состояние объектов класса.
  • Поведения. Поведение класса описывается функциями-членами класса, которые в ООП называются "методами".
  • Прав доступа к полям данных и методам.
  • Наследование. Каждый класс может быть потомком другого класса, и в свою очередь быть предком следующего класса. Наследник, простите, наследует от своего родителя методы и члены данных, то есть поведение и информация о состоянии.

Объект - это экземпляр класса. Экземпляров одного и того же класса можно насоздавать столько, сколько нужно. Теперь проще понять, почему "класс" так называется: класс описывает целый класс объектов, которые могут обладать одинаковым поведением и свойствами, присущими данному классу.

Интерфейс - это описание возможностей, предоставляемых классом, реализующим данный интерфейс. Интерфейс не описывает, в отличие от класса, конкретное поведение, но содержит описание методов без их тела. Интерфейс возлагает обязанность по реализации этих методов классу, заявившему, что он реализует данный интерфейс.

Объявление интерфейса, реализация интерфейса и пример использования - всё это демонстрирует фрагмент кода ниже. Можете скопировать его в PHP-файл и поиграть с ним. Эта программка будет выводить числа 1, 2, 3 и так далее, при нажатии в браузере на кнопку F5.

// Интерфейс DataStorage.
interface DataStorage {
  // setData - сохраняет значение $value под именем $name.
  public function setData($name, $value);

  // getData - возвращает значение, предварительно сохранённое
  // под именем $name
  public function getData($name);
}

// Класс FileDataStorage - Реализация интерфейса DataStorage,
// работающая с файлами.
class FileDataStorage implements DataStorage {
  private $directory;

  // Это - конструктор, то есть метод, который вызывается PHP
  // при создании экземпляра класса оператором new.
  // В конструкторе происходит инициализация объекта.
  public function __construct($directory) {
    $this->directory = $directory;
  }

  private function getFileName($dataName) {
    return $this->directory . "/" . $dataName . ".txt";
  }

  public function getData($name) {
    $fname = $this->getFileName($name);
    return is_file($fname)? file_get_contents($fname): null;
  }

  public function setData($name, $value) {
    if(!is_dir($this->directory)) {
      mkdir($this->directory, 0777, true);
    }
    return file_put_contents($this->getFileName($name), $value);
  }
}

// Функция getNextNumber() - обращается к элементу данных 'value',
// увеличивает его на единицу и возвращает полученное значение.
// Эта функция может работать с любым DataStorage.
function getNextNumber(DataStorage $storage) {
  $value = $storage->getData('value');
  $value ++;
  $storage->setData('value', $value);
  return $value;
}

// Создаём экземпляр класса FileDataStorage
// и передаём его в функцию.
$storage = new FileDataStorage('data');
echo getNextNumber($storage);

Контроль доступа

В приведённом примере в объявлении класса встречаются служебные слова public и private. Они задают уровень доступа к методам и членам данных класса. Возможны несколько уровней "допуска" к членам класса:

  • public - к такому члену класса может обращаться любой программный код.
  • protected - к такому члену можно обращаться только из метода класса, где объявлен этот член, а также из классов-потомков, которые унаследовали этот член от родителя.
  • private - доступен только для методов класса, где объявлен этот член. В классах-потомках private методы и поля данных будут недоступны.
  • по умолчанию, если не указан ни один из спецификаторов доступа, используется public.

Что же даёт это разделение по уровням доступа? Благодаря возможности "спрятать" внутренние члены данных от внешнего кода, достигается лучшая изоляция деталей реализации от пользователей класса. При грамотно разработанном интерфейсе класса, методы, через которые класс взаимодействует с внешним миром, будут доступны извне, а внутренние и вспомогательные методы и поля будут недоступны. Это повышает наглядность интерфейса класса, а часто и предотвращает грубые ошибки.

Благодаря возможности ограничения доступа к членам класса, класс можно считать "чёрным ящиком", предоставляющим простой интерфейс для связи с внешним миром, но имеющий, возможно, сложную реализацию внутри, но об этой реализации пользователям класса знать не надо. Этот принцип называется Инкапсуляцией.

В примере выше, FileDataStorage предоставляет пользователям два внешне простых метода. Внутри этих методов происходят действия, о которых клиент (в нашем примере это функция getNextNumber) даже не догадывается. Для функции-потребителя "услуги" под названием DataStorage, детали реализации нашего FileDataStorage, который на самом деле передаётся в функцию, не имеют никакого значения. Функция работает с объектом $storage как с "чёрным ящиком", вызывая его методы и передавая в них параметры, как того требует интерфейс DataStorage. В этот момент мы подходим к следующему важному понятию:

Полиморфизм

Полиморфизм, как следует из самого слова - возможность сущностей одного класса принимать различные формы, то есть, обладать различным поведением. В ООП полиморфизм обеспечивается возможностью наследования классов друг от друга, а также возможностью реализации интерфейсов.

Снова обратимся к нашему примеру выше. В этом примере функция getNextNumber получает для своих внутренних нужд экземпляр DataStorage. DataStorage - это интерфейс, то есть, нашей функции подошла бы любая реализация этого интерфейса, ибо в любой реализации DataStorage обязаны быть методы getData и setData. Но какая конкретно реализация DataStorage будет передана, функция getNextNumber не имеет ни малейшего представления, да ей это и не нужно знать. В нашем примере есть только одна реализация DataStorage - FileDataStorage, но никто не мешает наделать и других реализаций, например, умеющих хранить данные в базе данных или на удалённом сервере. Функция getNextNumber будет одинаково работать с любой из этих реализаций.

Это свойство DataStorage, принимать различное поведение в зависимости от конкретной реализации, и называется полиморфизмом.

Для чего всё это нужно?

На практике, оформление различной функциональности в виде классов весьма удобно. Класс представляет собой изолированный набор функций-методов и данных, с которыми эти методы работают. Ограничение доступа обеспечивает то, что только лишь методы класса могут работать с этими данными. Всё это позволяет создавать всевозможные "кирпичики", то есть, компоненты, реализующие ту или иную функциональность: работу с базой данных, работу с изображениями, обработку ввода пользователя и так далее и т.п.. Если у каждого такого кирпичика есть простой интерфейс и минимум зависимостей от других кирпичиков, то такой компонент будет легко переносим и возможно будет использовать его в более чем одном проекте, экономя силы и время.

Зависимость, связность

В нашем примере (опять!) функция getNextNumber зависит только от интерфейса DataStorage. getNextNumber не зависит ни от какой конкретной реализации DataStorage, что позволяет использовать данную функцию в любой среде, достаточно лишь создать подходящую реализацию DataStorage. Это пример слабой зависимости.

Связность (сцепление, cohesion) - понятие, определяющее насколько тесно связаны между собой элементы одного модуля. Можно считать "модулем" класс, а "элементами" - его методы. Тогда связность определяет, по сути, насколько узкий (специфичный) функционал реализует данный класс.

ООП позволяет обеспечить слабую зависимость (связанность, coupling) компонентов программы друг от друга, при их высокой связности (cohesion). Слабая зависимость компонентов друг от друга при их высокой связности - признак хорошего программного дизайна, ведь это означает, что:

  • Каждый класс выполняет лишь свою определённую обязанность, часть функционала системы. И наоборот, за каждый элемент функционала системы отвечает только один класс. Например, если в системе понадобилось изменить политику безопасности, добавив ограничение на минимальную длину пароля, то изменения должны быть сделаны лишь в одном месте программы, отвечающем за проверку пароля на безопасность.
  • Классы слабо зависят друг от друга: можно заменять один объект другим, и при этом не надо будет вносить изменения в соседние компоненты системы.

На практике это означает простоту обслуживания, то есть, лёгкость внесения изменений в существующую программу для изменения существующих и добавления новых возможностей.

Эпилог

Вышенаписанное можно считать весьма кратким введением в ООП, причём язык программирования не имеет значения. Достаточно заменить пример в середине страницы на другой язык - и всё станет справедливо для другого языка.

Чтобы правильно составлять программы с использованием принципов ООП, надо "почувствовать" этот подход, что может получиться не сразу. Часто оказывается, что объектный дизайн программы - одно из самых сложных мест в разработке больших программных систем. А порой и дизайн этот меняется в ходе разработки! Поэтому классы-"кубики", большие и поменьше - надо видеть "издалека", чтобы оперировать ими, составляя структуру приложения. В этом сильно помогают различные фреймворки, предлагая уже отчасти готовый дизайн, на который остаётся навесить возможности, необходимые для конкретного приложения.

Существуют так называемые "паттерны проектирования", это тактические приёмы решения различных задач проектирования приложений. Каждый такой приём, или паттерн, имеет общепринятое название, что позволяет разработчикам, общаясь (часто прямо через комментарии в коде), парой слов объяснить, как в программе организовано взаимодействие тех или иных программных компонентов. Впрочем, паттерны проектирования - тема для отдельной статьи, даже нет, для целой книжки (такие книжки и на самом деле есть, и много).

В дальнейшем, по ходу изучения, будем возвращаться к различным аспектам ООП, так что базовые знания дополнятся более детальными, а также опытом использования на практике.

Пишите, если есть что дополнить/исправить.

Раздел:

Темы: