воскресенье, 10 августа 2014 г.

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


Объектно-ориентированное программирование в PHP. Общие положения, синтаксис.

Философское отступление.

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

Т.к. мы рассматриваем программирование на языке PHP, то изложение будем строить на классическом процедурном подходе. Перед основной темой - немного "философии" :)
Итак, данные. Данные - это основа и ядро нашего алгоритма. Данные являются целью работы и определяют устройство логики программы. Мы можем проследить некоторые связи между формой данных и наличием определенных структур и даже синтаксических конструкций в языке.

  • Первый и главный аспект - само наличие данных. И связанных с ними переменных. Когда появляются данные - возникает необходимость в переменных (и их специфичной разновидности - констант). Различие типов данных определяет идею типизации в языках программирования. Это не очень бросается в глаза в скриптовых языках из-за их нестрогой типизации, но тем не менее, каждая переменная содержит значение определенного типа - строку, число, массив и т.д.
  • Второй момент - повторяемость данных, или множественность. Из изучения базового синтаксиса мы знаем, что множественные значения реализуются использованием массивов. Массив, как правило, содержит некоторое количество более-менее однотипных элементов-значений. Расширяя таким образом концепцию одиночной переменной до хранилища последовательного множества. 
  • Как только мы заговорили про массивы (и множества), сразу возникает мысль о способе обработки этих самых множественных значений. Само собой, именно для этого в языке программирования существуют различные вариации циклов.
  • Третий момент, на который стоит обратить внимание - неоднородность данных. Любое реально имеющее ценность множество данных (или состояний) будет состоять из различных значений. Что наталкивает нас на необходимость управления логикой алгоритма исходя из некоторых изменяющихся условий, создаваемых упомянутой неоднородностью. Синтаксически такое управление реализовано конструкциями "ветвления": if и switch. Зачастую, обработка массивов данных строится как сочетание циклов и конструкций ветвления.
  • Следующий аспект можно описать как "повторяемость логики". Алгоритмические решения часто бывают достаточно универсальны, что дает нам повод использовать один и тот же участок кода в разных местах нашей программы. Вычленив его из общего "потока" в отдельную процедуру, мы можем вызвать этот код в нужном нам месте, или местах. Что во-первых, делает основной код компактнее. Во-вторых, делает меньше и весь совокупный код. В-третьих, позволяет работать над кодом процедуры в одном месте, не заботясь о множестве повторений. И в-четвертых, единство выполнения некоторого вынесенного алгоритма делает его реализацию максимально универсальной и проработанной. В PHP (как и в большинстве языков) подобная концепция реализуется с помощью функций.
  • Рассмотрев уже описанные в предыдущих статьях элементы языка программирования в их взаимосвязи с обрабатываемыми данными и алгоритмическими решениями, пора перейти к следующему уровню абстракции - обособленном пространстве некоторого набора данных и связанных с ним функций. Необходимость подобного обособления проистекает из существования более сложных структур данных, чем одиночные переменные, или массивы. Такие структуры, как правило, описывают некоторые умозрительные, или реальные сущности и создаются при написании программы, когда минимальный функционал простых единичных значений, или однородных множеств, становится недостаточным.
1. Предпосылки.

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

<?php
    function printUserInfo($user){
        print " {$user['name']} - {$user['age']} years  \n";
    }
    
    function checkUserGender($user, $gender){
        return $user["gender"] == gender;
    }
    
    function checkAgeInterval($user, $minAge, $maxAge){
        return $user["age"] >= $minAge && $user["age"] <= $maxAge;
    }
    
    // testing
    
    $users = array(
        array("name"=>"Vasya", "age"=>16, "gender"=>"male" ),
        array("name"=>"Petya", "age"=>22, "gender"=>"male" ),
        array("name"=>"Kolya", "age"=>45, "gender"=>"male" ),
        array("name"=>"Olya", "age"=>21, "gender"=>"female" ) 
    );
    
    foreach($users as $user){
        if (checkAgeInterval($user, 20, 30)){
            printUserInfo($user);
        }
    }

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

2. Классы и объекты.

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

<?php
    class User{
    
    }
    
    $user = new User;


3. Поля класса.

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

<?php
    class User {
        var $name = "Vasya";
        var $age = "16";
        var $gender = "male";
    }
    
    $user = new User;
    
    print $user->name;

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

4. Модификаторы доступа.

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

<?php
    class User {
        public $name = "Vasya";
        private $age = "16";
        var $gender = "male";
    }
    
    $user = new User;
    
    print $user->name;
    print $user->age;

 При попытке выполнить данный код интерпретатор PHP выдаст ошибку.

<b>Fatal error</b>:  Cannot access private property User::$age on line <b>11</b>

Возникает вопрос, а зачем нам члены класса, к которым нельзя доступиться? Ответ на этот вопрос нам дает использование методов. О чем и пойдет речь дальше.


5. Методы класса.

Вторым существенным элементом класса являются методы. Методы - это те же функции, но объявленные как внутренние компоненты класса. Все методы класса имеют доступ к переменным класса, в отличие от внешнего кода, когда поле объявлено как private. Доступ к членам класса внутри методов осуществляется посредством использования внутреннего контекста объекта, ссылку на который хранит специальная переменная  $this  . Контекст $this доступен после создания экземпляра класса. Через него мы обращаемся как к полям, так и к другим методам текущего класса.

<?php
    class User {
    
        private $name;
        
        public function setName($name){
            $this->name = $name;
        }
        
        public function getName(){
            return $this->name;
        }
    }
    
    $user = new User;
    $user->setName("Vasya");
    
    print $user->getName();

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

6. Конструктор класса.

Настало время поговорить о такой удобной возможности, как инициализация объекта в момент создания. Синтаксис PHP предоставляет нам специальный вид методов, которые называются конструкторами. Конструктор класса вызывается в момент создания объекта-экземпляра класса. Кроме того, конструктор может принимать аргументы, которые передаются в круглых скобках, после имени класса, как при вызове функции. Имя метода-конструктора, начиная с 5-й версии PHP - "__construct" (Перед словом стоит два символа подчеркивания. Подобное мы увидим и в других специальных методах в PHP). Ранее конструктор назывался по имени класса.

<?php
    class User {
        private $name;
        private $age;
        private $gender;
        
        public function __construct($name, $age, $gender){
            $this->name = $name;
            $this->age = $age;
            $this->gender = $gender;
        }
    }
    
    $user = new User("Vasya", 16, "male");

После объявления параметров конструктора мы можем присвоить значения параметров в переменные-члены класса. Так же, мы можем вызывать в конструкторе необходимые методы класса. Стоит иметь в виду, что конструктор - это не функция, в том смысле, что он не возвращает никаких значений.

7. Деструктор класса.

Раз уж существует метод класса, который выполняется в момент создания экземпляра, почему бы не быть методу, который выполняется в момент уничтожения объекта? Такой метод называется деструктором и описывается сходным с конструктором способом. Т.к. у деструктора нет точки вызова в коде (это происходит автоматически), то и параметров у него тоже не может быть. Деструкторы, как правило, используются для освобождения системных ресурсов, вроде подключения в базе данных, или дескриптора файла. Либо для автоматизации выполнения некоторых итоговых операций. Деструктор объявляется словом "__destruct", с двумя подчеркиваниями впереди.

<?php
    class Unit {
        function __construct(){
            print "constructor doing...\n";
        }
        
        function test(){
            print "method test \n";
        }
        
        function __destruct(){
            print "destructor doing...\n";
        }
        
    }
    
    function testUnit(){
        print "test in testUnit \n";
        $unit = new Unit();
        $unit->test();
    }
    
    print "before test \n";
    testUnit();
    print "after test \n";

Результат работы примера:

before test 
test in testUnit 
constructor doing...
method test 
destructor doing...
after test 

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

Сравнительное использование.

Таковы основы синтаксиса классов и объектов в PHP.
Теперь самое время переписать первый пример из этой статьи в ООП стиле.

<?php
    class User {
        private $name;
        private $age;
        private $gender;
        
        public function __construct($name, $age, $gender){
            $this->name = $name;
            $this->age = $age;
            $this->gender = $gender;
        }
        
        public function printUserInfo(){
            print " {$this->name} - {$this->age} years  \n";
        }
        
        public function checkUserGender($gender){
            return $this->gender == gender;
        }
        
        function checkAgeInterval($minAge, $maxAge){
            return ($this->age >= $minAge && $this->age <= $maxAge);
        }

    }
    
    // testing
    
    $users = array(
        new User("Vasya", 16, "male"),
        new User("Petya", 22, "male"),
        new User("Kolya", 45, "male"),
        new User("Olya", 21, "female")
    );
    
    foreach($users as $user){
        if ($user->checkAgeInterval(20, 30)){
            $user->printUserInfo();
        }
    }


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

Комментариев нет:

Отправить комментарий