Зачем нам ООП?

866
.
Предисловие
Буквально на каждом форуме WAPа я встречаю и продолжаю встречать вопросы вида - зачем нужен ООП когда можно жить без него? Вот и здесь, я нашел целых 3 темы, в которых, возможно, была предпринята попытка раскрытия данного вопроса, но совсем в другом русле. Вопрос отпадает сам собой, когда речь заходит о сложных системах, решающих целую группу задач со своими подзадачами. Стоит системе вырасти во что то, требующее декомпозиции задач, как на помощь приходит ООП. Я уже много писал об этой модели http://visavi.net/blog/blog.php?cid=12& (автор - Башка) и часто участвую в темах, связанных с ней, потому хотелось бы немного поговорить о данном инструменте и на этом форуме.

Для кого эта статья?
Данная статья рассчитана на программистов, знакомых как с PHP, так и с его объектным синтаксисом. Я не буду переписывать здесь что такое класс, свойства и методы, обо всем этом вы можете подробно прочитать в учебниках или в документации. Целью данной статьи является показ ОО модели несколько с другой стороны, нежели вы привыкли ее видеть. Настоятельно рекомендую вам внимательно изучить синтаксис ООП в PHP, а так же такие фундаментальные понятия, как инкапсуляция, полиморфизм, наследование и абстрагирование.

Немного о терминах
Чтобы не тратить символов далее в статье, многие термины ООП будут описаны в этой части статьи. Обращайтесь к ней если какой то термин вам покажется непонятным.
- Объект - конкретная совокупность данных, представляющих некоторую сущность системы;
- Класс - совокупность однотипных объектов, их общая структура;
- Свойство объекта - единица характеристики объекта;
- Значение свойства объекта - конкретное значение характеристики объекта;
- Состояние объекта - совокупность значений всех свойств объекта;
- Метод объекта - алгоритм, ответственный за изменение состояние объекта или получения его текущего состояния;
- Инстанцирование класса - создание экземпляра класса (объекта);
- Инкапсуляция - сокрытие реализации объекта;
- Семантика свойства - имя свойства, тип принимаемого значения, назначение свойства;
- Семантика метода - имя метода, тип возвращаемого значения, порядок, имена и типы аргументов, назначение метода;
- Семантика класса - совокупность семантик всех свойств и методов класса, назначение класса;
- Полиморфизм - различная реализация одной семантики. Данное свойство позволяет заменить два полиморфных класса друг на друга без необходимости изменения алгоритмов работы с ними;
- Наследование - повторное использование класса с возможностью его расширения. Иерархия наследования сохраняется;
- Абстрагирование - отвлечение от малозначимых данных предметной области;
- Шаблоны проектирования (паттерны проектирования) - наиболее удачное решение задачи в контексте;
- Архитектура - логическая структура системы;
- Архитектурные шаблоны (архитектурные паттерны) - наиболее удачные архитектурные решения.

В чем ошибка?
Важнейшим при понимании ОО модели является не ее синтаксис, а границы ее использования и ОО мышление программиста. Мое знакомство с ОО моделью проходило ни чуть не иначе вашего. Увидев класс для написания "Hello world" я прикрыл учебник и подзабыл об ОО на неделю другую. Hello world ОО примеры годятся разве что при изучении синтаксиса ОО модели языка, но не для понимания смысла данной модели. Смысл ОО модели не зависит от языка программирования и совершенно одинаков в любом языке. Именно по этому вам необходимо без скептицизма изучить синтаксис ООП языка PHP опираясь на "натянутые за уши" примеры, так как без понимания синтаксиса изучать ОО модель будет сложнее.

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

Цели и задачи ООП
Для чего же нужен этот "монстр" в программировании? Неужели нельзя обойтись при написании скриптов старыми добрыми функциями и переменными? Для ответа на эти, и другие схожие вопросы, вернемся во времена зарождения программирования и взглянем на код. Код, написанный на языке assembler сложно читать и сопровождать, с его помощью можно написать программы в 1 000 или даже 10 000 строк кода, но чем больше программа, тем сложнее ей управлять. С появление структурированных языков программирования, стало гораздо проще прослеживать сложные алгоритмы. Появление функций позволило повторно использовать код и дало толчок к декомпозиции задач (разбиению задачи на подзадачи). Сейчас сложную задачу можно разбить на функции и решить путем последовательного их вызова. Такая модель позволяет временно абстрагироваться от всего лишнего и сосредоточится только на данной подзадаче, а не на всей задаче.

Приход эры ООП направляет вектор мысли не на алгоритмы, а на данные. Если процедурное программирование ориентировалось на алгоритм (процедуры, функции), то ООП рассматривает в первую очередь данные и сущности. Для примера, при реализации модуля "Гостевая книга" матерый процедурник обратит будет рассматривать функции добавления, обновления, просмотра и удаления сообщений. Он создаст эти функции и определит входные параметры для них. Объектник же сначала посмотрит в сторону используемых данных, а именно ответит на вопрос - что использует модуль "Гостевая книга" в своей работе для решения поставленной задачи? - он так же глянет на реальный мир и попробует спроецировать его в модуль. На пример, "Гостевая книга" уже давно используется в отелях и ресторанах, но здесь используется бумажная книга, в которой может писать любой желающий. Почему бы не использовать этот же принцип для реализации? Что нам нужно для этого?
1. Сообщение пользователя - сама цель книги;
2. Автор - нужно ведь знать кто именно оставил то или иное сообщение о нашем отеле или ресторане;
3. Сама книга - где же писать как не в ней?!
Далее объектник просто реализует эти сущности в системе:
class Message{
...
}

class Autor{
...
}

class GuestBook{
...
}


Как видите, на первый план встают данные и сущности, а не алгоритмы, работающие с ними.

ОО модель позволяет легче абстрагироваться при разработке системы, а так же группировать алгоритмы и данные в объекты по их целевому назначению.

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

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

Синтаксис изучен, вектор есть, что дальше?
А дальше настоятельно рекомендую вам начать изучение существующих паттернов проектирования! Это позволит вам лучше понять смысл ОО модели и ее прелести.

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

А теперь о преймуществах:
- Проектировать сложные программы становится проще - благодаря абстрагированию, гораздо проще держать в голове сложные задачи;
- Масштабирование и сопровождение программ упрощается - инкапсуляция, полиморфизм и наследование позволяют изменять программу без необходимости изменения кода. Чаще всего ОО программисты стремятся именно к этому результату;
- Удобное повторное использование кода - функции хорошо подходят для повторного использования алгоритма, а объекты и классы, для повторного использования всей системы и ее модулей. Здесь можно выделить следующие уровни:
* Функция;
* Класс;
* Модуль - набор классов, решающих одну задачу;
* Пакет - набор модулей, решающих одну группу задач;
* Система - набор пакетов, решающих группы задач пользователей;
* Агрегатная система - набор систем, решающих группы мало связанных задач большого числа пользователей.

Когда же переходить на ООП?
Почему бы не прямо сейчас? Прочитайте учебник и изучите синтаксис ОО модели в PHP. Прочитайте о паттернах проектирования в соответствующих учебниках. Выберите любой существующий у вас модуль и попытайтесь переписать его в ОО стиле. Используйте IDE вместо текстового редактора, это поможет вам.

Заключение
Множество примеров использования ООП взамен процедруного стиля приведено в указанном мною в начале темы блоге. Я понимаю, что пока мои аргументы недостаточны, для того, чтобы программисты перешли на ОО стиль, но целью данной статьи была попытка уберечь вас от неверного пути в изучении данной модели!
.
(\/)____o_O____(\/)
Delphinum, можешь объяснить абстракцию , интерфейсы...
extends -> implements в часности, на живом примере
.
Пример
В качестве примера использования ООП приведу следующий. Мне он кажется наиболее простым и понятным, и в то же время лаконичным.

Задача: реализовать механизм перебора символов (брутфорса) с возможностью изменения символов без изменения кода. На пример имеются символы 123, нужно чтоб код вывел 123, 132, 213 и др.

Процедурник задумался бы скорее всего об алгоритмах перебора, проверки существования числа и разбора строки на компоненты. Объектнику достаточно было бы вспомнить принципы организации чисел в математике.

Предметная область:
Разряд - конкретная позиция числа;
Число - совокупность разрядов и символов числа на этих разрядах;
Символ числа - один из возможных символов, используемых при составлении числа.

На пример число 123ABC имеет 6 разрядов, символ на первом разряде (самый правый) имеет вид C, на втором B, на третьем A и т.д. Для данной системы счисления используются символы: 1, 2, 3, A, B, C что эквивалентно десятичной - 1, 2, 3, 4, 5, 6 - но вместо 4,5,6 используются буквы латинского алфавита.
Если к числу C добавляется единица, число увеличивается на разряд: C + 1 = 10.

Объектная реализация задачи:
/**
 * Класс представляет конкретный символ числа (конкретную цифру).
 */
class Discharge{
  /**
   * Набор символов для обозначения элемента числа. Упорядочен в порядке возрастания их весового коэффициента.
   * @var integer[]
   */
  protected $numerals = [1, 2, 3, 4, 5, 'A', 'B', 'C']; // Доступные символы
  /**
   * Текущий символ элемента числа. Не может быть больше, чем число символов.
   * @var integer
   */
  protected $point = 0;
  /**
   * Система счисления (число элементов массива $numerals)
   * @var integer
   */
  protected $numberSystem;
 
  function __construct(){
    $this->numberSystem = count($this->numerals);
  }
 
  /**
   * Возвращает текущее значение элемента числа.
   * @return string
   */
  public function getNumber(){
    return (string)$this->numerals[$this->point];
  }
 
  /**
   * Добавляет к элементу числа монаду (единицу)
   * @return boolean true - если число удачно инкрементированно, false - если инкрементация вызвала сброс элемента числа.
   */
  public function plus(){
    $this->point++;
    if($this->point == $this->numberSystem){
      $this->point = 0;
      return false;
    }
    return true;
  }
}
 
/**
 * Класс представляет собой совокупность символов числа (само число).
 */
class Number{
  /**
   * Массив элементов числа, упорядоченный по возрастанию разрядности.
   * @var Discharge[]
   */
  protected $number = [];
 
  /**
   * Длина числа.
   * @var integer
   */
  protected $length;
 
  /**
   * @param integer $length Длина числа
   */
  function __construct($length){
    $this->length = $length;
    for($i = 0; $i < $length; $i++){
      $this->number[] = new Discharge;
    }
  }
 
  /**
   * Метод увеличивает заданный разряд на монаду.
   * @param integer $discharge Номер разряда начиная с 0
   */
  protected final function incrementDischarge($discharge){
    if($discharge < $this->length){
      if(!$this->number[$discharge]->plus()){
        $this->incrementDischarge($discharge+1);
      }
    }
  }
 
  /**
   * Метод увеличивает число на монаду.
   */
  public function increment(){
    $this->incrementDischarge(0);
  }
 
  /**
   * Метод возвращает число.
   * @return string Текущее значение числа
   */
  public function showNumber(){
    $resultNumber = '';
    for($i=$this->length; $i--; ){
      $resultNumber .= $this->number[$i]->getNumber();
    }
    return $resultNumber;
  }
}
 
$n = new Number(5);
for($i=0; $i<10; $i++){
  echo $n->showNumber()."\n";
  $n->increment();
}
echo $n->showNumber()."\n";
 
/*
  Результат
11111
11112
11113
11114
11115
1111A
1111B
1111C
11121 - повышение разряда
11122
11123
*/
.
Koenig, Глянь предложенный мною блог, там есть эти темы, в частности по абстракциям: http://visavi.net/blog/blog.ph ... =448&
.
(\/)____o_O____(\/)
Delphinum (18.09.2012/09:52)
Koenig, Глянь предложенный мною блог, там есть эти темы, в частности по абстракциям: http://visavi.net/blog/blog.ph ... =448&
как раз читаю
.
(\/)____o_O____(\/)
Delphinum, код почитал понял, но связи как то не вижу между двумя классами,
12345abc [0-7] правильно?
$n = new Number(5); ???

.
Koenig, 12345abc [0-7] - да, это в десятичном будет как от 0 до 7.

$n = new Number(5); // задаем число в 5 разрядов. Оно сейчас имеет вид 11111
for($i=0; $i<10; $i++){ // Десять итераций
echo $n->showNumber()."\n"; // Выводит текущее число
$n->increment(); // Добавляем к числу 1
}
echo $n->showNumber()."\n"; // Выводим последнее число

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

На пример вот работа класса Discharge:
$d = new Discharge;
echo $d->getNumber(); // 1
$d->plus();
echo $d->getNumber(); // 2
$d->plus();
echo $d->getNumber(); // 3
$d->plus();
echo $d->getNumber(); // 4
$d->plus();
echo $d->getNumber(); // 5
$d->plus();
echo $d->getNumber(); // A
$d->plus();
echo $d->getNumber(); // B
$d->plus();
echo $d->getNumber(); // C
$d->plus(); // метод вернет false так как произойдет сброс разряда
echo $d->getNumber(); // 1


Как видно этот класс отвечает только за работу одного разряда.
А вот пример работы класса Number:
$n = new Number(5); // Пяти разрядное число - 11111
$n->increment(); // число станет 11112
$n->increment(); // 11113
$n->increment(); // 11114
$n->increment(); // 11115
$n->increment(); // 1111A
$n->increment(); // 1111B
$n->increment(); // 1111C
$n->increment(); // 11121 - за сброс первого разряда отвечает объект Discharge, а за инкремент второго разряда уже класс Number

Вот кусок кода класса Number, ответственного за инкремент разряда при сбросе:
if(!$this->number[$discharge]->plus()){ // Если метод инкрементации разряда вернет false, значит надо инкрементировать следующий разряд
  $this->incrementDischarge($discharge+1); 
}

Как видно данный класс уже отвечает за взаимодействие разрядов. Классу Discharge нет необходимости знать о том, как его будут использовать.
.
(\/)____o_O____(\/)
Delphinum, отбой, не доглядел в конструкторе вызов класса
.
(\/)____o_O____(\/)
Delphinum, ты ORM разрабатываешь?
.
Koenig, Уже разработал. А что?
Всего: 18