Пролог

Здравствуйте всем, это пролог к серии статей сериала “ООП здорового человека(Сезон 1)”. Данная серия статеек об объектно-ориентированном программировании “здорового человека”(в предыдущей записи я привел пример ООП курящего). Современное ООП это не только работа из сущностями как с объектами, но и следование некоторым механизмам, принципам и шаблонам проектирования кода.

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

Сериальчик стоит глянуть как новичкам, так и прожжённым программистам. Первым – для того чтоб легко “въехать в тему”(рассказываю максимально доступно); вторые больше мне нужны – для исправления моих ошибок(если будут). В целом, не стесняйтесь – комментируйте(только прошу – коль критикуете – объясняйте за что и где)!

Содержание:

Эпизод 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()), где , по возможности, выбирается нужный метод. В целом, смысл следующий: класс-наследник должен уметь работать со всеми методами родителя.

Эпизод 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
{
    // Приказ об устанавлении соединения с хранилищем:
    // -- получим инфу об используемой БД
    // -- инициализируем новый экземпляр для работы с БД
    // выполняем запрос
}

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

Эпизод 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 через него – пишите в комменты…

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

Эпизод 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 также) имеет в себе реализацию полиморфизма, т.к. принимает в себя параметр различных типов и приводит его к нужному.