Опыт разработки мини-приложения для Telegram: создание NeVerRLandPoker

Привет, друзья! 🙌

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

О приложении NeVerRLandPoker

Идея моего мини-приложения зародилась довольно давно. Как пет-проект я решил создать сервер для покера на Node.js (хотя сам я больше PHP-шник, но в этот раз Node.js оказался предпочтительнее) и клиентскую часть на Vue.js (мой любимый фреймворк). Так и появилась NeVerRLandPoker — рейтинговая игра в Техасский Холдем, в которой каждый ход может стать решающим.

Почему я выбрал разработку мини-приложения для Telegram?

Мини-приложения Telegram — это удобная платформа для создания небольших интерактивных приложений, и вот почему:

  1. Простота доступа. Пользователю не нужно ничего скачивать — достаточно открыть бота, и сервис или игра уже на экране.
  2. Широкая аудитория. Telegram обладает огромной пользовательской базой, что делает его отличной площадкой для запуска игровых и развлекательных приложений.

Чему я научился в процессе разработки мини-приложения?

  1. Интуитивный интерфейс. Одной из главных задач стало упрощение интерфейса для игры через Telegram. Поскольку экранные элементы в Telegram мини-приложениях ограничены, я убрал лишние функции, оставив только то, что важно для геймплея в покерной игре с рейтингами, такой как NeVerRLandPoker.
  2. API Telegram. Важной частью разработки стало понимание возможностей Telegram API, особенно аутентификации пользователей. Раньше я использовал стандартное окно регистрации для получения логина и пароля. Теперь же Telegram сам передает информацию о пользователе, и я использую её для создания токена аутентификации в сокетах.
  3. Обратная связь пользователей. Telegram позволяет мгновенно получать фидбек от пользователей. Это очень помогло в улучшении приложения: игроки могут прямо через бота оставить свои комментарии, и я могу оперативно вносить изменения.

Как адаптировать веб-приложение для Telegram?

Процесс адаптации моего покерного приложения для Telegram оказался проще, чем я думал:

  1. Зарегистрировать бота через BotFather — это стандартный шаг для создания любого Telegram-бота.
  2. В Bot Settings > Menu Button указать ссылку на веб-страницу мини-приложения.
  3. Подкорректировать код для интеграции с Telegram API.

Изменения в коде

Раньше у меня было окно регистрации, которое отправляло логин и пароль для получения токена, используемого для аутентификации в сокетах. Сейчас Telegram API предоставляет информацию о пользователе, и я использую её для создания токена.

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

Что получилось в итоге?

Адаптация моей игры к Telegram оказалась не только простой, но и интересной. Моё мини-приложение — NeVerRLandPoker — это рейтинговая игра в Техасский Холдем с использованием анте, что делает игру динамичной и захватывающей. Важной частью игры стала рейтинговая система, которая мотивирует игроков соревноваться за место в топе.

Само приложение доступно в Telegram через бота. Всё, что вам нужно, — это просто открыть бота и начать играть. Присоединяйтесь и попробуйте свои силы в покере!

Совет разработчикам мини-приложений для Telegram

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

Структуры во Vue

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

Инициализация проекта

Первое, что нужно – это установить node js, затем – непосредственно стартовую сборку вью. Перейдем в директорию, где хотим писать проект, и с нее запустим

npm init vue@latest

Лично я этим путем ни разу не шел… я всегда использовал IDE, в ней ввесь проект собирается без командной строки. Моя среда разработки почему-то(еще не вьехал почему) собирает проект на Vue2, но мы договорились о 3-ей версии. Потому в pacage.json сменил версию и запустил

npm i

после обновления фреймворка, запустим сервер для разработки

npm run serve

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

Если у вас тоже были проблемы с 2 и 3 версиями, скорее всего вы увидите пустой экран и ошибки в консоле браузера. Перейдите в файл src/main.js и разместите там этот код

import {createApp} from "vue";
import App from './App.vue'

createApp(App).mount('#app')

Он импортирует нужную функцию с вью 3 и монтирует приложение на нужный элемент.

Короче, после всех действий я получил вот-такую структуру проекта

структура проекта  vue

но вся писанина, которую мы будем делать, будет в папке src.

Структура компонента

Как и все(известные мне доселе) js-фронтенд-фреймворки, данный экземпляр состоит из компонентов. Откроем src/App.vue(первый компонент) и увидим следующее

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

В нем, как видно, есть 3 секции. Секция template обычная html-разметка, может содержать теги других компонентов и особые vue-атрибуты; единственное правило – все содержимое этого тега должно иметь один родительский узел(те обёрнуто общим div-ом, к примеру). Секция script – обычный js-обьект(о нем ниже), а также, импорты нужных компонентов и библиотек. Секция style, как понятно, это стили.

Структура javascript объекта

Этот объект содержит имя компонента, функции(т.н. хуки жизненного цикла приложения), вложенные объекты. Вот полный список

Здесь:

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  props:{
    property:Number
  },
  methods:{},
  computed:{},
  watch:{},
  data:function () {
    return {}
  },
  setup() {
  },
  beforeCreate() {
  },
  created() {
  },
  beforeMount() {
  },
  mounted() {
  },
  beforeUpdate() {
  },
  updated() {
  },
  beforeUnmount() {
  },
  unmounted() {
  }
}
  1. components это другие компоненты вью, использующиеся в этом;
  2. props – свойства с типом их данных, которые будут получены из вне(источник – родительский компонент);
  3. methods – функции, которыми будет управляться компонент;
  4. computed – обьект, содержащий вычисляемые значения;
  5. watch – отслеживание изменений в значениях;
  6. data – функция, возвращающая объект со свойствами;
  7. setup – начиная с этой функции и ниже это хуки жизненного цикла приложения, те будут вызваны при определенных событиях в приложении(не спровоцированных пользователем напрямую); конкретно это срабатывает при инициализации компонента(в котором находится); в отличии от остальных мест, здесь нет доступа к this и все, что здесь создастся не будет реактивным;
  8. beforeCreate – ровно то же, что и в 7; для чего надо – фиг знает;
  9. created – это уже после разрешения всех данных, тут доступен this и реактивность;
  10. beforeMount – срабатывает перед началом рендеринга компонента;
  11. mounted – когда компонент целиком отрендерился;
  12. beforeUpdate – срабатывает перед каждым перерисовыванием любого элемента компонента;
  13. updated – после этого действия(в пункте 12);
  14. beforeUnmount – перед тем, как компонент будет удален из DOM-дерева страницы;
  15. unmounted – после удаления.

Резюмируя

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

Знакомимся с Vue js

Привет всем в первой статье посвященной javascript. Знаю, что не всем он по душе(как и пхп), но да ладно(учитывая, что это единственный прод. язык фронтэнда). Я не буду разглагольствовать о том как он работает или его синтаксисе, начну сразу с фреймворка.

Vue что это и почему он?

Сперва, я познакомился с чистым js(разумеется), потом с jQuery и React, и в конце с Vue(ну как “в конце” – на данный момент). jQuery это первая попытка использования языка в ООП виде(в js вообще своеобразный ООП), эта библиотека долгое время была ультимативной на рынке. Потом начали появлятся полноценные фронтэнд фреймворки. React это первый с коим я познакомился(тк он использовался в WP), но именно Vue есть вершиной(на сейчас), имхо. Я не хочу обидеть React, однако , именно Vue содержит то, о чем говорит “реакт” – реактивность. Немного подробнее: первый при получении изменений перерисовывает ввесь компонент, второй – именно изменившуюся часть компонента.

И, главная, как по мне особенность, шаблонизатор этих фреймворков – JSX для React и HTML Vue. Дело в иом, что я часто верстаю компоненты, а потом переношу в фреймворк, при этом JSX требует замены гипертекстовых атрибутов на специальные(да, некоторые редакторы умеют делать это автоматом). Также, в JSX мне не удалось добиться работы эммета и аводополнения доступными классами; но шаблонизатор вью это делает без проблем.

Настройка среды разработки

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

Первое, что нужно сделать, так это(как и с любым другим js-фреймворком) установить NodeJS. Затем специальное расширение для браузера Vue Devtools, которое в панель разраба браузера добавит вкладку Vue

Vue  Devtools
Vue Devtools

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

Примечание

Я работал не в большом количестве редакторов кода, поэтому не сочтите это за объективное мнение.Из всего, где я писал код, наиболее удобным(экспорт модулей, умное автодополнение, работа с отступами …), качественно проработанным показался PhpStorm(полагаю, как и другие их продукты для фулл-стек разработки). Это бесплатная реклама продукта jetbrain-ов, но их среды этого достойны,имхо.

Эпизод 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 – говорит о том, что при наследовании класса данный метод не возможно переопределить.