Эпизод 7. D – DIP: The Dependency Inversion Principle

Привет всем в 7-ом эпизоде сериала “ООП здорового человека”. В этой серии: D – последняя буква аббревиатуры 5-ти принципов SOLID – что он значит? Идемте разбираться.

Теория

Принцип имеет 2 формулировки:

  • Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Представим, что есть класс по работе с БД и класс, делающий расчеты на основании того, что прилетело с БД. Таким образом, расчетный модуль ставит в зависимость модуль по работе с БД и , первый- модуль высокого уровня; второй, соответственно, более низкого уровня. Собственно, данный принцип – это следование принципу LSP и OSP.

Примеры

1: рассмотрим все тот же пример с БД и расчетами: однажды, по какой-нибудь причине может понадобиться переключится на иную БД.

№2: ближе к жизни. Человек передвигался на велосипеде, нафармил денег – купил машину. Здесь уже DIP сохранен, постольку, и велик, и тачка, созданы для управление руками и ногами.

Практика

Рассмотрим пример с велосипедом и машиной. В самом простом случаи(без всяких принципов), начальная позиция выглядит так

<?php
class Human
{
    function move()
    {
        $transport = new Bicycle();
        
        $transport->getMaxSpeed();
    }
}

class Bicycle
{
    function getMaxSpeed()
    {}
}

затем мы класс Bicycle заменяем классом Car. В моем коде это одноо место, в боевом коде же- их будет много; лазить по коду и везде переписывать – как минимум – долго(не говоря о возникших багах). Следующей ступенью развития может быть использование Внедрение зависимостей (Dependency injection)

<?php
class Human
{
    public function __construct(private Bicycle $transport)
    {
    }

    function move()
    {
        $this->transport->getMaxSpeed();
    }
}

это уже лучше – один раз на класс это делаем в конструкторе; но если классов где нужно переключить много?

В этом случаи на помощь придет вместо обращения к классу, обращение к интерфейсу(или абстрактному классу) и, по средству позднего связывания, дергать нужный класс

<?php
class Human
{
    public function __construct(private TransportInterface $transport)
    {
    }

    function move()
    {
        $this->transport->getMaxSpeed();
    }
}

class Bicycle implements TransportInterface
{
    function getMaxSpeed()
    {}
}

interface TransportInterface
{
    function getMaxSpeed();
}

таким образом, остается лишь указать класс-реализатор нужного интерфейса.

Эпизод 6. I – ISP: The Interface Segregation Principle

Приветствую вас в 6-ом эпизоде сериала “ООП здорового человека”. Здесь речь пойдет о 4-ем принципе SOLID – разделение интерфейсов. По истории этого принципа мне сказать нечего, потому перейдем к сути.

Теория

Принцип разделения интерфейса. Книга: Создавайте мелкозернистые интерфейсы, ориентированные на клиента – это дословный перевод; если же смысловой – будет звучать так: клиент не должен зависеть от методов, которые он не использует. Из принципа следует, что интерфейс это не принадлежность системы, а должен быть ассоциирован с клиентом и его потребностью.

Примеры

№1 Допустим мы в магазине выбираем товары; у них всех есть общие характеристики(цена, размеры, вес и т.д.);два товара – свитер(дополнительная х-ка материал) и машина(доп. мощность двигла). Принцип о том, что не нужно делать интерфейс со всеми свойствами(и существующими и нет в конкретном классе); нужно сделать 3 интерфейса: интерфейс продукта (с общими свойствами)и специфические к каждому отдельно.

№2 Это уже из моей “программистской” практики. Часть сайта представлял собой анкету, в которой, в зависимости от семейного положения клиента, открывались и закрывались различные блоки полей к заполнению. Было это сделано так: был один фасад(класс) со всеми полями; при выборе положения происходила проверка и по ее результатам некоторые поля в классе устанавливались в NULL. Каюсь, я этот момент не переделывал(времени не было + не мой фронт работ, я всего-лишь глянул что там). Если б я переделывал это в соответствии с ISP, было б где-то так: от фасада отпочковывается 1 интерфейс с общими для всех полями, и еще интерфейсы с индивидуальными полями для каждого состояния.

Практика

Раз уж я вспомнил о примере с работы(на деле это фриланс, без обязательств молчать), рассмотрю его в коде; иными словами – реализую это так, как я вижу(с применением ISP)

Для начала мы имеем общий класс, описывающий все свойства(это его примерный интерфейс)

<?php
interface PersonalDataInterface
{
    public function setFamilyState($value);
    public function setIndividualData($value);
    public function setParentsData($value);
    public function setFamilyData($value);
}

class PersonalData implements PersonalDataInterface
{
    protected int $familyState;
    protected array $individualData;
    protected array $parentsData;
    protected array $familyData;

    public function setFamilyState($value)
    {
        $this->familyData = $value;
    }

    public function setIndividualData($value)
    {
        $this->individualData = $value;
    }

    public function setParentsData($value)
    {
        if ($this->familyState === 0) {
            $this->parentsData = $value;
        }
    }

    public function setFamilyData($value)
    {
        if ($this->familyState === 1) {
            $this->familyData = $value;
        }
    }
}

если очистить код от плевел – было так. После переписи получил это

<?php
interface IndividualDataInterface
{
    public function setIndividualData($value);
}

interface ParentsDataInterface
{
    public function setParentsData($value);
}

interface FamilyDataInterface
{
    public function setFamilyData($value);
}

class MarriedPersonalData implements IndividualDataInterface, FamilyDataInterface
{
    protected array $individualData;
    protected array $familyData;

    public function setIndividualData($value)
    {
        $this->individualData = $value;
    }

    public function setFamilyData($value)
    {
        $this->familyData = $value;
    }
}

class TeenagerPersonalData implements IndividualDataInterface, ParentsDataInterface
{
    protected array $individualData;
    protected array $parentsData;

    public function setIndividualData($value)
    {
        $this->individualData = $value;
    }

    public function setParentsData($value)
    {
        $this->parentsData = $value;
    }

при этом поле familyState(из того как было) становится определителем используемого класса для построения объекта клиента.

На деле – примеров телега! Роли пользователей сайта, типы постов… В целом, это простой совет построения кода для обеспечения его гибкости(как и любой другой принцип). А еще этот принцип соблюдает SRP для интерфейсов.

Эпизод 5. LSP: The Liskov Substitution Principle

Приветствую вас в 5-ом эпизоде сериала “ООП здорового человека”. Здесь речь пойдет о 3-ем принципе SOLID – принцип подстановки Лисков. С названия просто ничего не понятно… поскольку Лисков – это фамилия ученого-информатика по имени Барбара. В общем, этот принцип(как и многие) придуман не дядей Бобом, а всего-лишь пере озвучен и переосмыслен – за что, в конкретно этом случаи, огромный респект ему…

Теория

Респект потому, что в оригинале формулировка имеет сложное математическое определение. Для сравнения, оригинал:

Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

Книга Мартина:

Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.

Простыми словами(вывод, которому я пришёл со второй формулировки, если подумать – первая о том же): проектируй код так, чтоб при замене объекта-родителя объектом-наследником система не падала. Вообще, этот принцип мне видится как частный случай The Open Closed Principle(проясните мне ваше виденье в комментах! только без токсика..).

Пример

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

Практика

Коль собственных примеров нет(та и, в принципе – он будет аналогичен этим) – опишу фигуры и птиц в виде классов. Начнем с прямоугольника и квадрата. В прямоугольнике есть высота h, ширина w и добавим метод по вычислению площади area()

<?php

class Rectangle
{
    public int $h;
    public int $w;

    public function setH(int $h): void
    {
        $this->h = $h;
    }

    public function setW(int $w): void
    {
        $this->w = $w;
    }
    
    public function getArea(): int
    {
        return $this->w * $this->h;
    }
}

$rectangle  = new Rectangle();
$rectangle->setH(8);
$rectangle->setW(5);
echo $rectangle->getArea();

/*
Вывод:
40

и, поскольку квадрат это частный случай прямоугольника, кажется логичным квадрат наследовать от прямоугольника – но: задав высоту – как быть с шириной и наоборот?

Вот мой вариант решения этой задачи(уверен, не лучший, ты то точно знаешь лучший)

<?php

abstract class Figure
{
    protected $params;

    public function setParams(array|int $params): void
    {
        $this->params = $params;
    }

    abstract public function getArea(): int;
}

class Rectangle extends Figure
{
    public function getArea(): int
    {
        return (count($this->params) > 1) ?
            $this->params[0] * $this->params[1] :
            throw new Exception('Set all params');
    }
}

class Square extends Figure
{
    public function getArea(): int
    {
        return pow($this->params,2);
    }
}

$rectangle  = new Rectangle();
$rectangle->setParams([5,8]);
echo $rectangle->getArea().PHP_EOL;

$square = new Square();
$square->setParams(6);
echo $square->getArea();

/*
Вывод:
40
36

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

С птицами аналогична: одни летают, вторые плавают, третьи – ножками перемещаются. При работе с такими объектами нужно дергать за какой-то абстрактный метод(к примеру, move()), где , по возможности, выбирается нужный метод. В целом, смысл следующий: класс-наследник должен уметь работать со всеми методами родителя.

Эпизод 3. Интерфейсы и трейты

Приветствую всех в третей, заключительной, части минисериала ” Сущности ООП”. Здесь я буду повествовать об интерфейсах и трейтах(что следует с названия) – что это? зачем? как использовать?

Интерфейсы

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

Свойства интерфейсов

Как сказал ранее – он определяет публичные свойства(исключая тело метода, только имя и параметры) и методы класса. Также, он определяет константы класса и, в отличии от полей, они могут принимать значения в самом интерфейсе; затем эти константы будут доступны в классе-реализаторе.

<?php
interface B
{
    const A = 1;
    function rr();
}

interface C
{
    function drr();
}

interface D extends B,C
{
}

class Page implements D
{
    function __construct()
    {
        echo self::A;
    }

    function rr()
    {
    }

    function drr()
    {
    }
}

new Page();

/*
Вывод:
1
Process finished with exit code 0

В коде выше(PHP) приведено еще одно свойство интерфейсов – множественное наследование. Несмотря на то, что это карта класса, конечный класс может содержать иные, как публичные, так и скрытые методы и поля.

Разница между абстрактным классом и интерфейсом

Первое – это разные сущности – соответственно, срои сущностные особенности – это и очевидного… Следующее – это то, что в абстрактном классе могут быть реализованные методы. В целом, это все отличия.

Цель интерфейсов – задание определенных свойств для классов реализаторов, используеться для обеспечения гибкости кода путем следования некоторым принципам SOLID.

Трейты

Трейт – это механизм обеспечения повторного использования кода в языках с поддержкой только одиночного наследования определение от разработчиков PHP. По сути, это решение-костыль для языков с одиночным наследованием, но где хочется/нужно наследовать больше кода; т.е. трейт не может быть инициализирован в объект(как и интерфейс, и абстрактный класс), что, также, означает, что трейт без класса ничто(как и интерфейс, в общем-то…).

Свойства

Здесь речь будет не о том, что можно сделать в трейте, а о том как клас вращает трейты. Почему же о классах? – потому как в трейте можно только объявлять методы, свойства и константы. Нет, соврал… трейт может использовать трейт(строка 12), также содержать абстрактные и статические методы и свойства.

Итак, о классах… начнем с того, что можно подмешивать в класс более одного трейта(строка 23). При этом методы и свойства одного метода доступны в другом(если они публичны, строка 6). Об области видимости – она такая же как у классов, с теми же модификаторами.

<?php
trait  A
{
    public function ff()
    {
        echo $this->b;
    }
}

trait B
{
    use C;
    public $b;
    protected function  ff()
    {}
}

trait C
{}

class Page
{
    use A, B {
        A::ff insteadof B;
        b as private;
    }
}

Теперь пару строк о структуре после перечисления используемых трейтов(строки 23 – 26). Поскольку в двух примесях есть метод с одинаковым названием – возникает конфликт и код выкинет фатальную ошибку. Строка 24 говорит о том, что в классе нужно отдать предпочтение методу с трейта А(A::ff вместо B); аналогично работает и для свойств. Строка 25 – изменение видимости свойства/метода.

Как уже отмечалось ранее, цель – это способ осуществления множественного наследования там, где оно недоступно на прямую.

Эпизод 4. О – OCP: The Open Closed Principle

Приветствую вас в 4-ом эпизоде сериала “ООП здорового человека”. Здесь речь пойдет о 2-ом принципе SOLID – открытость-закрытость кода…

Теория

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

Здесь я немного припух: как так – дядя Боб(на деле – первая формулировка сиего принципа принадлежит Бертрану Мейеру) запрещает использовать одну из возможностей ООП? Ну-да… полазив в интернете, посмотрев иные трактовки(а также пример в книге Роба) – нет! Речь идет не о наследовании, а о порождении объекта класса для дальнейшего использования(что, в принципе, не перечит моей трактовке… однако, я не представляю – как не переопределять методы родителя в некоторых ситуациях…).

Речь, как я понял(если откинуть мою трактовку) об объектном полиморфизме. Смысл в том, чтоб при создании метода (вызывающего класс в процессе работы) была предусмотрена возможность изменить вызываемый класс, не изменяя, при этом, сам метод. Кста, мы уже знаем о SRP, поэтому в его контексте: при соблюдении OCP также будет соблюден SRP, поскольку точка зависимости от чужого класса останется одной.

Примеры

№1: печатная машинка и компьютер с принтером; и там и там пользуясь клавиатурой происходит набор текста, затем краска на бумаге отображает набранное. Интерфейс одинаков, результат тоже – различные только механизмы между точками.

Практика

Для иллюстрации этого принципа рассмотрим класс Product(из этой статьи); а именно – метод получения данных о продукте. Дело в том ,что эти данные где-то лежат; соответственно, нужно установить связь с хранилищем, составить запрос и исполнить его – и всем этим занимается отдельный(-е) класс. Представим это так

<?php
class Product
{
    // достать инфо  о продукте:
    // -- дергаем Storage и строим запрос
    // -- приказываем выполнить его
    // дергаем Currency и пересчитываем цену продукта
}

class Storage
{
    // Устанавливаем соединение с хранилищем
    // выполняем запрос
}

Теперь, хранилища могут быть разные(MySQL, MongoDB, обычный файл…); у каждого свой способ соединения и свой способ задать вопрос – в общем свой класс. Этот принцип о том, что класс Storage должен проектироваться так, чтоб он мог работать с классом любой БД:

<?php
class Storage
{
    // Приказ об устанавлении соединения с хранилищем:
    // -- получим инфу об используемой БД
    // -- инициализируем новый экземпляр для работы с БД
    // выполняем запрос
}

ну и, как все понимают, классы работы с хранилищами должны иметь одинаковые методы. Для реализации данного принципа построения кода придуманы дизайн-паттерны кода – Стратегия и  Шаблонный метод; разберу эти паттерны позже – в этой же статейке я хотел показать, что в подобных ситуациях(когда логика говорит о возможности появлении вариантов развития системы) – используй порождение объекта, а не наследование его класса. Можно конечно и начальный способ оставить – но это чревато ошибками в будущем… и это уже не про мой сериал.

Эпизод 2. Классы – часть 2. Магические методы

Привет всем во втором эпизоде сериала “Сущности ООП”. Здесь я расскажу о магических методах классов. Несмотря на то, что приводимые здесь названия методов(и код) будет о пхп, данные методы есть и в других ЯП; конечно, если он поддерживает ООП, ну и, вероятно, под другим именем…

Магические методы

Это методы класса, которые по умолчанию создаются(и, по умолчанию, делают ничего!) для каждого класса. Их отличие от обычных методов в том, что они могут быть переписаны в этом же классе(как и в наследниках). Теперь главное – почему они магические? Связано это с тем, каким образом они вызываются: они работают не потому, что их явно вызвали в коде, а по срабатыванию определенного события. Эти методы нужно просто знать, ибо фреймворки часто их используют. Да, вероятно, для написания более понятного кода стоит использовать явные вызовы, но все-же – они есть и используются…

Касательно конкретно PHP – название этих методов начинается с двойного нижнего подчеркивания. Также, они могут иметь все три(описаны в предыдущей серии) модификатора области видимости; при этом: public ведет открыто и наследуется при событиях с наследником, protected – закрыто, но наследование сохраняется; private – события с наследниками их не дергают.

Создание и разрушение объекта

Магический метод , отслеживающий создание объекта с класса – наиболее часто используемый метод из всех – __construct. Немного о передаче переменных в конструктор: до PHP 8.0 было так

<?php
class Page
{
    public $name;
    public $id;
    
    public function __construct($name,$id)
    {
        $this->id = $id;
        $this->name  = $name;
    }
}

$page  = new Page('Главная', 1);

после(включая ее) cтал возможен такой синтаксис(помимо первого)

<?php
class Page
{
    public function __construct(public $name, public $id)
    {
    }
}

$page  = new Page('Главная', 1);

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

Антипод данного метода – __destruct – срабатывает каждый раз когда объект разрушается(в том числе и при завершении работы скрипта)

<?php
class Page
{
    public function __construct($name)
    {
        print_r('Объект создан'.PHP_EOL);
        print_r('Название страницы: '.$name.PHP_EOL);
    }

    public function __destruct()
    {
        print_r('Объект уничтожен'.PHP_EOL);
    }
}

$page  = new Page('Главная');
unset($page);
echo 'Страница';

/*
Вывод:
Объект создан
Название страницы: Главная
Объект уничтожен
Страница

Перечитывал написанное – вспомнил… вспомнил, что не уточнил – в конструктор может быть передан любой тип переменных кроме callable(груб говоря – функции).

Перегрузка(обращение к недоступным активам класса)

Под “недоступными активами класса” я подразумеваю несуществующие или недоступные в текущей области видимости свойства/методы.

Свойства

Начну с работы с недоступными свойствами. Для манипуляции с ними есть методы __set, __get, __unset и __isset. Первые два – это динамические сеттеры и геттеры:

<?php
class Page
{
    private $fields;

    public function __set($name, $value)
    {
        $this->fields[$name] = $value;
        print_r("Свойство [$name] установлено".PHP_EOL);
    }

    public function __get($name)
    {
        $value = $this->fields[$name];
        print_r("Свойство [$name] равно $value".PHP_EOL);
    }
}

$page  = new Page();
$page->name = 'Главная';
$page->name;

/*
Вывод:
Свойство [name] установлено
Свойство [name] равно Главная

Вторые два метода срабатывают когда происходим проверка на существование/пустоту недоступного свойства, а также его удаления

<?php
class Page
{
    private $fields;

    public function __set($name, $value)
    {
        $this->fields[$name] = $value;
    }

    public function __isset($name)
    {
        print_r('Вызван метод '.__METHOD__.PHP_EOL);
    }

    public function __unset($name)
    {
        print_r('Вызван метод '.__METHOD__.PHP_EOL);
    }
}

$page = new Page();
$page->name = 'Главная';
isset($page->name); // существует ли
empty($page->name); // пуст ли
unset($page->name); // уничтожить свойство

/*
Вывод:
Вызван метод Page::__isset
Вызван метод Page::__isset
Вызван метод Page::__unset

Методы

У методов все попроще, всего два один на обычные методы(__call) и один на статические(__callStatic)

<?php
class Page
{
    public function __call($name, $args)
    {
        print_r('Вызван метод '.__METHOD__.PHP_EOL);
        print_r($name.PHP_EOL);
        print_r($args);
    }

    public static function __callStatic($name, $args)
    {
        print_r('Вызван метод '.__METHOD__.PHP_EOL);
        print_r($name.PHP_EOL);
        print_r($args);
    }
}

$page = new Page();
$page->setTitle('Главная', 1);
Page::setTitle('Главная', 1);

/*
Вывод:
Вызван метод Page::__call
setTitle
Array
(
    [0] => Главная
    [1] => 1
)
Вызван метод Page::__callStatic
setTitle
Array
(
    [0] => Главная
    [1] => 1
)

Сериализация объектов

Сериализация — это процесс преобразования объекта в поток байтов для сохранения или передачи в память, базу данных или файл; вот такое определение дает Google этому процессу…

Прямой процесс – сериализация

В моем ЯП есть функция сериализации в поток байтов – serialize. На выходе будет строка

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function doSomeThink()
    {}
}

$page = serialize(new Page());
echo $page;

/*
Вывод:
O:4:"Page":2:{s:2:"id";i:1;s:4:"name";s:14:"Главная";}

Здесь целых два магических метода будут проверены – __sleep и __serialize; Если первый метод в объекте присутствует – он должен вернуть массив с именами свойств для сериализованного объекта

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __sleep()
    {
        return ['id'];
    }
}

$page = serialize(new Page());
echo $page;

/*
Вывод:
O:4:"Page":1:{s:2:"id";i:1;}

Вторая функция, доступная с PHP 7.4, будет перезаписывать первую(если присутствуют обе) и должна возвращать массив “ключ – значение”, который и будет сериализован

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __serialize()
    {
        return [
            'id' => 2,
            'name' => 'Карточка'
        ];
    }
}

$page = serialize(new Page());
echo $page;

/*
Вывод:
O:4:"Page":2:{s:2:"id";i:2;s:4:"name";s:14:"Карточка";}

Зачем это нужно? Классический пример – одно из полей объекта – ссылка на соединение с БД. Поскольку сериализованный объект не может хранить его, это поле не нужно.

Десериализация

Это процесс восстановления последовательности байт(полученной при сериализации) в объект. Как и при сериализации, при десериализации срабатывают два метода – __wakeup и __unserialiize. Их отличие в том, что они получают восстановленный обьект и ничего не должны возвращать.

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __sleep()
    {
        return ['id'];
    }

    public function __wakeup()
    {
        $this->id = 5;
    }
}

print_r(unserialize('O:4:"Page":1:{s:2:"id";i:2;}'));

/*
Вывод:
Page Object
(
    [id] => 5
    [name] => Главная
)

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

Иная магия

К рассмотрению осталось пять методов: __toString отрабатывает, когда объект пытаются представить в виде строки

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __toString()
    {
        return "это обьект Главная Страница";
    }
}

echo new Page();

/*
Вывод:
это обьект Главная Страница

При экспорте свойств объекта – работает статический __set_state. Я не понял(и, пока что, не понимаю), что она делает и к чему это – поэтому пропустим ее. Третья – __invoke – срабатывает когда объект пытаются запустить как функцию

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __invoke()
    {
        echo 'Главная страница';
    }
}

call_user_func(new Page());

/*
Вывод:
Главная страница
Process finished with exit code 0

Cледующий метод – метод __clone – срабатывает после того, как объект клонируют. Этот код увеличивает идентификатор объекта

<?php
class Page
{
    public $id = 1;
    public $name = 'Главная';

    public function __clone()
    {
        $this->id ++;
    }
}

print_r(clone new Page());

/*
Вывод:
Page Object
(
    [id] => 2
    [name] => Главная
)

Process finished with exit code 0

и последний – __debugInfo – создан для вывода объекта через var_dump. Эта функция показывает все поля объекта(публичные, скрытые…), но если объект имеет метод __debugInfo – можно задать свой вывод.

Эпизод 1. Классы – часть 1. Ключевые слова и модификаторы

Привет всем читателям! Это первый эпизод сериала “Сущности ООП”. Здесь я расскажу о классах: что это и какие модификаторы они имеют – но поскольку о них можно поговорить побольше, то это всего лишь первая часть.

Классы

Как сообщает нам Википедия: класс – это шаблон для создания объекта; но мне более нравится: Класс – описание структуры объекта и методов работы с ним – оно более емкое. Если просто говорить – это набор свойств и методов, которые при использовании определенных модификаторов могут быть скрыты в объекте, а также вызваны без создания объекта; также есть вариант, что класс не может порождать объект – здесь мы все это и обсудим…

Класс, не умеющий рождать объект

Обычный класс делать это умеет(“Process finished with exit code 0” – говорит об отсутствии ошибок)

<?php
class Page
{}

$page = new Page();

// OUTPUT:
// Process finished with exit code 0

но если перед class словом добавить abstract – он потеряет данную возможность. Класс не умеющий рождать объекты? зачем? как с ними работать?

<?php
abstract class Page
{}

$page = new Page();

// OUTPUT:
// Fatal error: Uncaught Error: Cannot instantiate abstract class Page
// Process finished with exit code 255

Начнем по порядку – зачем? Полазив на разных сайтах(блогах, форумах…), посмотрев разные мнения и учев свои опыт и взгляд – пришел к выводу – обеспечение полиморфизма классов(принципы OCP, LSP – реализация в виде паттернов); и, мало не забыл сказать, что важно – абстрактные классы могут содержать абстрактные свойства и методы(читай далее). Теперь как с ними играть: наследовать, наследовать и только наследовать!

<?php
abstract class Page
{}

class MainPage extends Page
{}

$page = new MainPage();

// OUTPUT:
// Process finished with exit code 0

Модификаторы доступности свойств и методов

И методы, и свойства имеют одинаковый набор модификаторов открывающих им определенную область видимости – public, рrotected и private. Первый модификатор(public)

<?php
class Page
{
    public $id;

    public function getId()
    {
        return $this->id;
    }
}

class MainPage extends Page
{
    public function index()
    {
        $id = $this->id;
        $id = $this->getId();
    }
}

$page = new MainPage();
$page->index();
$id = $page->getId();
$id = $page->id;

// OUTPUT:
// Process finished with exit code 0

открывает полную область – как внутри объекта класса и классов-наследников, так и при обращении к объекту вне класса; второй – только внутри себя

<?php
class Page
{
    protected $id;

    protected function getId()
    {
        return $this->id;
    }
}

class MainPage extends Page
{
    public function index()
    {
        $id = $this->id;
        $id = $this->getId();
    }
}

$page = new Page();
$page->index();
$id = $page->getId();// error
$id = $page->id;// error

// OUTPUT:
// Fatal error: Uncaught Error: Call to protected method Page::getId() from context ''
// line 23
// Process finished with exit code 255

и классов-наследников; и третий – только внутри себя.

<?php
class Page
{
    private $id;

    private function getId()
    {
        return $this->id;
    }
}

class MainPage extends Page
{
    public function index()
    {
        $id = $this->id;// error
        $id = $this->getId();// error
    }
}

$page = new MainPage();
$page->index();
$id = $page->getId();// error
$id = $page->id;// error

// OUTPUT:
// Fatal error: Uncaught Error: Call to private method Page::getId() from context 'MainPage'
// line 17
// Process finished with exit code 255

Кстати, у функции(метода) класса слово отвечающее за область видимости может отсутствовать – тогда ее область видимости равна public.

Ключевые слова в свойствах и методах

С одним ключевым словом(abstract) уже разобрались. Эти слова являются дополнительными(необязательными) инструкциями к тому, как работают свойства и методы. Начнем их рассмотрение с директивы static – она позволяет доступ к активу не создавая объект; иначе говоря – прикрепляет функцию внутрь класса.

<?php
class Page
{
    public static $id;
    
    public static function getId()
    {}
}

Page::$id;
Page::class;
Page::getId();

$page = new Page();
$page::$id;
$page::class;
$page::getId();

// OUTPUT:
// Process finished with exit code 0

Кста, внутри класса автоматически создаётся статическая публичная переменная class, содержащая имя класса с его неймспейсом.

И последний модификатор(для пхп) это final – говорит о том, что при наследовании класса данный метод не возможно переопределить.

Эпизод 3. S – SRP: The Single Responsibility Principle

Привет всем в 3-ем эпизоде сериала, где мы будем разбирать 1-ый из 5-ти принципов SOLID. Прежде чем прочесть эту часть, прочтите предыдущую(если не читали).

Теория

“Принцип единственной ответственности”, – в книге Мартина говориться: “Модуль ответственен только перед одним актером”. В современном прочтении же, звучит так: каждый метод должен иметь одну ответственность(делать одно действие или иметь причину для изменения – кому как угодно). Сам принцип был “разработан” Мейлиром Пейдж-Джонсом и  Томом ДеМарко – инженер-программистами.

Примеры

№1: на проекте сидят трое: менеджер по клиентам, системный администратор и кодер. Сис.админ и менеджер не могут заглядывать в код, навалянный кодером. Кодер не должен общаться с клиентом и администрировать сервер(и это моя позиция, но на всех галерах, где я работал – это нарушалось); ну и т.д.

№2: первобытный общественный строй – все занимаются всем дабы выжить. Пришла идея, чтобы каждый делал то, что у него получается лучше, и делился излишками своего дела; так поступают все – и вуаля! появляются искусства и прочие не необходимые для выживания виды деятельности. А все почему? – потому, что появляются излишки производства, т.к. каждый делает то, что умеет и могет.

Практика

В прошлой статье приведен класс, делающий все, от парсинга УРЛ до отрисовки страницы. Произведем “разделение труда” в классе “первобытного строя”.

Вначале мы парсим УРЛ и извлекаем идентификатор продукта с него – пусть этим будет занят класс Parser

<?php
class Product
{
    // достать инфо  о продукте
    // дергаем Currency и пересчитываем цену продукта
}

class Parser
{
    // парсить URL
    // достать ID продукта
}

class Currency
{
    // достать информацию о символе валюты
    // достать информацию о цене валюты
}

class View
{
    // написовать страницу
}

class Controller
{
    // дергаем Parser и получаем ID продукта
    // дергаем Product получаем инфо о продукте
    // дергаем View и отдает ему все на рендеринг
}

Затем, как Parser обработал УРЛ, дернем Product и получим данные о нем. Product, в свою очередь, обратится к Currency и получить инфу о валюте клиента; пересчитает цену товара в клиентских попугаях. Итак, информация собрана – отдадим все это на рендеринг(классу View).

Да, при этом появляется класс-дирижёр(Controller). Вероятно, правильнее было б работать с Currency через него – пишите в комменты…

В общем – это мое виденье данного, я б сказал главного, принципа кодирования.

Эпизод 2. СОЛИДный код

Рад приветствовать вас во втором эпизоде сериала “ООП здорового человека”. В этой серии: мы узнаем(если кто-то еще не знает) о главных принципах ООПешного кодирования, которые ложатся в аббревиатуру с аббревиатур SOLID. Но, поскольку я не согласен, что эти 5 принципов основные – рассмотрим еще принципы.

SOLID

Вот, что нам говорит страница Википедии: “SOLID — мнемонический акроним, введённый Майклом Фэзерсом для первых пяти принципов, названных Робертом Мартином в начале 2000-х, которые означали 5 основных принципов объектно-ориентированного программирования и проектирования”. Я не филолог и не историк – потому приму это утверждение на веру(как по мне – здесь важно только то, что это набор принципов…). Посмотрев много источников, хочу сказать, что авторство принцыпов принадлежит не Робу Мартину; он всего лишь пересказал их – некоторые перевел на более доступный язык, некоторые переосмыслил(и скажу, что в его интерпретации они стали лучше) ну и т.д… ах-да, еще и популяризировал! – что немаловажно…

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

Теперь, сами принципах:

  • S – SRP: The Single Responsibility Principle
  • О – OCP: The Open Closed Principle
  • L – LSP: The Liskov Substitution Principle
  • I – ISP: The Interface Segregation Principle
  • D – DIP: The Dependency Inversion Principle

Практическая часть

Кроме теории, я буду в коде(на моем любимом PHP) разбирать как воплотить эти принципы. Для этого я подготовил следующий god-объект(объект-бог):

<?php
class Page
{
    // достаем ID продукта из URL
    // достаем данные о продукте
    // достаем данные о валюте
    // пересчитываем цену продукта
    // рисуем страницу продукта
}

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

  • получить идентификатор продукта;
  • информацию о нем;
  • информацию о валюте покупателя;
  • расчет цены продукта в валюте покупателя;
  • рендер продуктовых данных в шаблон.

Сами методы реализовывать я не буду(не это цель, но посмотрим…) – всего-лишь продемонстрирую “расчесывание” класса согласно этим 5-ти принципам.

Смысл

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

Эпизод 1. Механизмы

Здравствуйте всем, это первая серия сериала “ООП здорового человека”. Есть 3 механизма, без которых ООП не ООП – это наследование, инкапсуляция и полиморфизм. Также, есть механизм “сокрытие”, который приписываю к части инкапсуляции.

Наследование 

Наследование — концепция объектно-ориентированного программирования, согласно которой абстрактный тип данных может наследовать данные и функциональность некоторого существующего типа, способствуя повторному использованию компонентов программного обеспечения. Эт-то о че нам говорит статья Википедии… я же предлагаю пока не рассматривать абстракцию(поскольку это не необходимое условие для данного механизма), а рассмотреть само явление наследования. Как и в жизни, наследник получает в свое распоряжение активы наследуемого; на примере класса Person из этой статьи:

<?php
class Person
{
    public int $id;
    public string $name;
    public string $secondName;
    public string $sex;
    public string $birthday;

    public function toArray()
    {
        return get_object_vars($this);
    }
}

допустим персона это частный случай каких-то единиц(персона наследует элемент)

<?php
class Item
{
    public int $id;
    public string $name;

    public function toArray()
    {
        return get_object_vars($this);
    }
}

class Person extends Item
{
    public string $secondName;
    public string $sex;
    public string $birthday;
}

При этом класс Person включает все методы и свойства класса Item

<?php
// классы ранее

print_r(get_class_methods('Person'));
print_r(get_class_vars('Person'));

// вывод
Array
(
    [0] => toArray
)
Array
(
    [secondName] => 
    [sex] => 
    [birthday] => 
    [id] => 
    [name] => 
)

Однако, их(активы наследуемого класса) можно скрыть от наследника – об этом в следующей статье. Смысл наследования состоит в минимальном копи-пасте кода. В некоторых языках наследоваться можно от многих, в других же – от одного класса(пример PHP – там множественное наследование спародировано трейтами – об этом в следующих сериях).

Инкапсуляция

Инкапсуляция (англ. encapsulation, от лат. in capsula) — размещение в одном компоненте данных и методов, которые с ними работают. В реализации большинства языков программирования обеспечивает механизм сокрытия, позволяющий разграничивать доступ к различным компонентам программы. Этот принцип в классе Person сохранен, т.к. все методы класса работают с данными, лежащими в нем же.

Инкапсуляция считается неполной если не реализовано сокрытие. Сокрытие – запрет на прямой доступ к полям объекта, вместо этого – доступ через модификаторы. На примере класса Item это выглядит так

<?php
class Item
{
    protected int $id;
    protected string $name;

    public function setId($id)
    {
        $this->id = (int) $id;
    }

    public function getId()
    {
        return $this->id;
    }

    public function setName($name)
    {
        $this->name = strval($name);
    }

    public function getName()
    {
        return $this->name;
    }

    public function toArray()
    {
        return get_object_vars($this);
    }
}

Для сокрытия модификатор (свойства) public заменяется на protected/private(о разнице также в следующих сериях) и доступ открывается через “сеттеры” и “геттеры”(собственно, модификаторы).

Полиморфизм

И снова Википедия: “Полиморфизм в языках программирования и теории типов — способность функции обрабатывать данные разных типов”. Если мы рассмотрим класс Item(вариант с сокрытием), то метод setId(setName также) имеет в себе реализацию полиморфизма, т.к. принимает в себя параметр различных типов и приводит его к нужному.