ООП
4 поста
4 поста
Попробуем разобраться с шаблонами проектирования программного обеспечения.
Картинка из статьи на Хабре https://habr.com/ru/companies/vk/articles/325492/
Чаще всего под шаблонами проектирования понимают некую часть программы, которая должна выполнить часто повторяющуюся задачу. Для таких частых, стандартных задач были написаны алгоритмы и, даже, специальная книга "Design Patterns".
Вот об этих шаблонах или паттернах, описанных в книге, и поговорим.
В статье на Википедии предлагают следующую классификацию шаблонов:
Основные
Порождающие шаблоны (Creational)
Структурные шаблоны (Structural)
Поведенческие шаблоны (Behavioral)
Мне кажется, что самый простой и популярный шаблон, который можно разобрать в первой статье - это Порождающий шаблон "Фабричный метод". Основная идея его в том, что наша программа будет создавать разные классы в зависимости от некоторых условий. Причем, во время разработки программы, мы не будем знать какой класс нам будет необходим. И программа должна будет определить и создать класс сама. Да, объяснить без примеров это не просто, поэтому посмотрим на код.
Для того, чтобы написать программу использующую паттерны, я решил создать небольшой проект и попробовать внедрить шаблоны так, чтобы практическая польза от них была как-то заметна. Проект - это элементарный телеграм бот. В который можно написать любое сообщение и получить ответ от бота. Для реализации бота я использовал простой php фреймворк с сайта code.mu на котором есть много полезных уроков и заданий.
Код проекта можно найти на гитхабе, ведь редактор текста на Пикабу не позволяет форматировать код для удобного чтения.
А бот можно найти в телеграме, написать ему сообщение и проверить работу нашего Фабричного метода.
Идея программы очень простая - вы пишите сообщение телеграм боту. Бот отправляет callback сообщение на бекэнд нашего приложения. И в зависимости от текста этого сообщения, код на бекэнде определяет какое сообщение отправить боту обратно, чтобы вывести его в телеграм клиенте.
Сообщение от телеграма приходит на роут, который мы указали при создании бота в BotFather. А во фреймворке создали этот роут в "\project\config\routes.php"
use \Core\Route;
return [
new Route('/telegram/:var1/', 'telegram', 'index'), // роут для telegram bot
];
Роут находит Telegram Controller и выполняет метод index. В этом методе мы выполняем ряд проверок. И если сообщение пришло от нашего телеграм бота, передаем сообщение в Фабричный метод, чтобы создать нужный объект и получить обратное сообщение для бота.
$ms = new MessageFactory($params);
$msObject = $ms->create();
В классе MessageFactory есть два метода. В конструкторе мы разбираем параметры сообщения и пытаемся получить текст сообщения от телеграма и id чата от которого сообщение пришло.
А в методе create мы проверяем существует ли класс для конкретного сообщения и если существует создаем объект этого класса и возвращаем его в контроллер. Если же класса нет, значит мы должны создать дефолтный класс.
public function __construct(array $params)
{
$this->command = preg_replace('/\//', '_', $params['message']['text']??'sdsfsdf');
$this->params['chatId'] = $params['message']['chat']['id']??0;
}
public function create()
{
$className = ucfirst($this->command);
$messageFile = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . "project/classes/messagefactory/messages/Message$className.php";
$messageClass = $this->namespace."Message$className";
if(file_exists($messageFile) && class_exists($messageClass)){
return new $messageClass($this->params);
} else {
return new MessageDefault($this->params);
}
}
Для каждого конкретного сообщения, которое мы хотим обработать на бекэнде по уникальному сценарию, мы должны создать класс реализации. Например, класс "\project\classes\messagefactory\messages\MessageTest.php"
Этот класс расширяет абстрактный класс MessageAbstract и реализует интерфейс IMessage. class MessageTest extends MessageAbstract implements IMessage {}
У него есть только конструктор, в котором мы вызываем конструктор родителя и заполняем свойство $message = 'Test Message'. Соответственно, если мы напишем в телеграм бот сообщение 'test', то наш Фактори метод создаст класс MessageTest, в свойстве $message у него будет строка 'Test Message' и контроллер отправит эту строку обратно в телеграм бот.
Telegram Bot
Что если бы мы не использовали все эти модные паттерны, классы и ооп. Конечно, такую программу можно реализовать и без ооп, возможно это даже проще. Но тогда описать обработку каждого сообщения придется в блоках if/else или switch/case. На первом этапе это будет не трудно. Просто напишем нужную строку для каждого case. Но, что если логика реакции на каждое сообщение будет сложной. Если при сообщении '/start' мы захотим зарегистрировать пользователя в системе и добавить его в базу данных. А при дефолтном сообщении создать еще несколько вариантов сценариев. Тогда наш контроллер будет огромного размера и поддерживать такую программу станет слишком сложно.
С другой стороны, если создать фабрику сообщений, то нам нужно только добавить отдельный файл сообщения в соответствующую директорию и все остальное сделает наш Фактори метод.
Вместо заключения. Это первый шаблон проектирования ПО который мы рассмотрели. Впереди еще много работы и полезных штук, поэтому... Спасибо всем, кто смог дочитать этот пост до конца несмотря на ужасное оформление, примеры на php и общую безграмотность. Отдельное спасибо всем, кто заходит и подписывается на ютуб канал. Я начинаю готовить видео на тему Шаблонов проектирования. Может быть примеры кода на видео будут более понятны.
Сначала определение из википедии - способность функции обрабатывать данные разных типов.
1 + '1' и другие приколы
PHP - язык с динамической и слабой типизацией, это значит, что в процессе работы программы тип переменной может быть изменен. Более того, операнды не обязаны быть одного типа и интерпретатор сможет обработать такие варианты операций без ошибок:
<?php
echo 1 . 1; // 11
echo PHP_EOL;
echo 1 + 1; // 2
echo PHP_EOL;
echo 1 + '1'; // 2
?>
Такое поведение уже формально подпадает под определение, но мы конечно хотим от полиморфизма совсем другого.
Практическая польза от полиморфизма в том, чтобы использовать один и тот же код для обработки данных полученных из разных функций, классов файлов и других источников.
Например, у нас есть новостной сайт. И мы получаем из базы данных информацию о новостях, чтобы вывести их пользователю. Чтобы не подключать бд к php песочнице, определим формат новости как json объект:
{
"title": "This is title",
"text": "This is text",
"time": "2024-01-13 12:00:00"
}
Напишем функцию, которая разберет этот json на отдельные поля и выведет на экран с нужным форматированием.
<?php
$jsonString = '{"title": "This is title", "text": "This is text", "time": "2024-01-13 12:00:00"}';
function publish($jsonString){
$jsonObject = json_decode($jsonString);
echo "<h1>$jsonObject->title</h1>";
echo PHP_EOL;
echo "<p>$jsonObject->text</p>";
echo PHP_EOL;
echo "<span>$jsonObject->time</span>";
}
publish($jsonString);
?>
Пока что неплохо справляемся и без ооп с полиморфизмом. Давайте добавим на наш сайт не только новости, но еще и статьи. В статьях не будет времени публикации, но будет имя автора. К сожалению, наша функция перестанет работать, если мы передадим такой json в нее. И тогда надо писать другую. И так для каждого типа контента. Конечно можно добавить несколько if - else и перерисовать дизайн блоков в зависимости от типов, но есть способ получше.
Теперь нам придется написать несколько классов.
<?php
class News
{
private $jsonString;
public function __construct()
{
$this->jsonString = '{"title": "This is title", "text": "This is text", "time": "2024-01-13 12:00:00"}';
}
public function publish()
{
$jsonObject = json_decode($this->jsonString);
echo "<div class='news'>";
echo "<h1>$jsonObject->title</h1>";
echo "<p>$jsonObject->text</p>";
echo "<span>$jsonObject->time</span>";
echo "</div>";
}
}
class Article
{
private $jsonString;
public function __construct()
{
$this->jsonString = '{"title": "This is title", "text": "This is text", "author": "Pikabu Member"}';
}
public function publish()
{
$jsonObject = json_decode($this->jsonString);
echo "<div class='article'>";
echo "<h2>$jsonObject->title</h2>";
echo "<p>$jsonObject->text</p>";
echo "<span>$jsonObject->author</span>";
echo "</div>";
}
}
$contents[] = new News();
$contents[] = new Article();
foreach($contents as $content){
$content->publish();
echo PHP_EOL."<div class='divider'></div>".PHP_EOL;
}
?>
Классы News и Article имеют одно приватное свойство jsonString. Заполняют его при создании объекта в конструкторе и выводят информацию из этого свойства в методе publish.
Таким образом мы можем использовать один метод для отрисовки данных из разных классов. И в главном потоке программы использовать простую конструкцию foreach(){$content->publish} для вывода разных отформатированных блоков.
Кто-то также говорит, что многие реализации должны принадлежать одному интерфейсу. То есть, мы должны определить общий интерфейс для классов его реализующих. На этапе вызова общего метода проверить принадлежность к интерфейсу. Таким образом мы сможем расширять программу многими классами и не задумываться об изменении главной программы.
<?php
interface IContent
{
function publish();
}
class News implements IContent
{
private $jsonString;
public function __construct()
{
$this->jsonString = '{"title": "This is title", "text": "This is text", "time": "2024-01-13 12:00:00"}';
}
public function publish()
{
$jsonObject = json_decode($this->jsonString);
echo "<div class='news'>";
echo "<h1>$jsonObject->title</h1>";
echo "<p>$jsonObject->text</p>";
echo "<span>$jsonObject->time</span>";
echo "</div>";
}
}
class Article implements IContent
{
private $jsonString;
public function __construct()
{
$this->jsonString = '{"title": "This is title", "text": "This is text", "author": "Pikabu Member"}';
}
public function publish()
{
$jsonObject = json_decode($this->jsonString);
echo "<div class='article'>";
echo "<h2>$jsonObject->title</h2>";
echo "<p>$jsonObject->text</p>";
echo "<span>$jsonObject->author</span>";
echo "</div>";
}
}
$contents[] = new News();
$contents[] = new Article();
foreach($contents as $content){
if ($content instanceof IContent) {
$content->publish();
echo PHP_EOL."<div class='divider'></div>".PHP_EOL;
}
}
?>
Вместо заключения. Хоть праздники уже закончились, но статью я дописал только сейчас, поэтому - Желаю всем счастливого нового года, денег, тепла и мира.
Спасибо всем, кто смог дочитать этот пост до конца несмотря на ужасное оформление, примеры на php и общую безграмотность. Отдельное спасибо всем, кто заходит и подписывается на ютуб канал. Я начинаю готовить видео на тему ООП. Может быть примеры кода на видео будут более понятны.
Ссылки на полезные документы:
Ну и в конце добавлю опрос
Штош, Пикабу явно хочет поговорить про ооп. Так не будем останавливаться. Тем более, что в комментариях уже предложили массу тем для будущих постов. В этот раз я постараюсь описать принципы инкапсуляции и наследования и надеюсь, что более опытные коллеги смогут дать мне дельные советы и по этому поводу.
Примеры все еще на PHP
Как совершенно справедливо заметили в комментах, я не смог объяснить что такое ооп в первом посте. Ну это и не мудрено. Попробую приоткрыть пользу этого подхода опять.
В прошлый раз я остановился на классе Текст. Сейчас я внесу в него изменения и, возможно, он станет лучше. К сожалению, у меня в редакторе нет возможности оформить код более красиво. Но, зато его можно скопировать в песочницу и попробовать выполнить.
<?php
class Text {
private $text;
public function __construct($text){
$this->text = $text;
}
public function printText() {
echo $this->text;
}
}
$hello = new Text("Hello, "); // создаем объект hello
$world = new Text("World!"); // создаем объект world
$hello->printText(); // вызываем метод print объекта hello
$world->printText(); // вызываем метод print объекта world
echo PHP_EOL.get_class($hello); // проверяем класс
echo PHP_EOL.get_class($world); // проверяем класс
?>
Здесь я добавил конструктор класса. С его помощью я передаю параметр с текстом. Этот параметр записывается в свойство text. Перед объявлением свойства text я использовал ключевое слово private, которое запрещает доступ к свойству извне класса. Функция get_class возвращает имя класса. Здесь я проверю, что класс двух объектов совпадает, почему это важно? Узнаем в следующих постах.
Определение возможности доступа к свойствам и методам класса (public, private, protected) - это сокрытие.
Таким образом получилось, что класс Text работает с параметром, который я передал через конструктор и методами, которые я описал в классе. Такое объединение данных и методов работы с ними + сокрытие - это инкапсуляция.
Итак, у нас есть класс, который принимает параметр и печатает текст. В нашем проекте нужно печатать текст в двух стилях. Заголовки, которые будут написаны капсом и обычные строки, у которых только первая буква будет большой. Мы можем написать функции для каждого случая обработки строк. Можем написать разные классы. Или можем использовать уже готовый класс и расширить его возможности. Напишу еще пару классов.
<?php
class Text {
protected $text;
public function __construct($text){
$this->text = $text;
}
public function printText() {
return $this->text;
}
}
class Header extends Text {
public function printText() {
return strtoupper($this->text);
}
}
class Sentence extends Text {
public function printText() {
return ucfirst(strtolower($this->text));
}
}
$header = new Header("hello, world!");
$sentence = new Sentence("hello, WORLD!");
echo $header->printText();
echo PHP_EOL;
echo $sentence->printText();
?>
Теперь у меня есть класс, который выводит текст без изменений и два класса, которые выводят текст нужным способом. При этом дочерние классы используют часть кода суперкласса, а это хорошо. Никто не любит повторять код.
Вместо вывода: инкапсуляция, модификаторы доступа и наследование уже сильно отличается от использования простых функций. Расширение возможностей классов намного лучше переписывания отдельных функций или методов суперклассов. Поэтому использовать их хорошо.
Вместо заключения. Я, конечно, не ожидал такого отклика на первую статью. Тем более удивительно, что рейтинг на аккаунте стал немного больше, а не наоборот. Значит, все не зря. Более того, 63 человека стали подписчиками. Это уже совсем удивительно, ведь судя по комментариям, про ооп все уже все знают. Наверное, вы подписались из-за искрометных мемчиков вначале постов. В общем, всем спасибо! И отдельное спасибо новым подписчикам на ютубе :)
Говорят, чтобы разобраться в чем-то достаточно хорошо, нужно попробовать объяснить это другому. Поэтому я решил написать несколько постов о программировании и заодно узнать / понять какие-то важные штуки получше. В этой первой статье начнем говорить об объектно-ориентированном подходе в программировании. Примеры я буду писать на языке PHP. Но, надеюсь, общие концепции будут применимы для любого языка.
Мем из интернетов
Попробую вкратце описать суть разных подходов. Однако, надеюсь, что вы и так их представляете. Потому что, описать кратко да так, чтобы было понятно будет трудно.
Итак, говорят, что существует две основные парадигмы программирования. Часто одну называют функциональной, а вторую объектно-ориентированной. Однако в статье Яндекса к этому понятию подходят более широко.
Ну, нам теоретические нюансы не важны, будем считать, что сначала программы писали как последовательность команд. Компьютер эту последовательность выполнял и программист получал какой-то результат. Позднее программы стали больше и сложнее и в них появилось много повторяющихся операций, тогда программисты придумали использовать функции.
Функция - часть программы, оформленная для многократного использования.
Такие функции можно вызывать из любого места в основной программе и вызывать их множество раз. Таким образом, вместо 10 одинаковых команд в двух разных местах, мы можем вызвать одну и тужу функцию в двух местах программы и сэкономить время, силы и размер программы.
Еще позднее функций стало недостаточно и программисты придумали классы и объекты.
Класс - часть программы, которая может содержать внутри функции и переменные, оформленная для многократного использования в более сложных конструкциях.
Предположим, мы хотим создать программу, которая выведет строку "Hello, world!" несколько раз.
На PHP это будет выглядеть, например так:
Здесь три команды вывода текста на экран. Поэтому удобно использовать функцию:
<?php
echo 'Hello, world!';
echo 'Hello, world!';
echo 'Hello, world!';
?>
Если вдруг, мы захотим внести изменения в программу и выводить другой текст, или, например, добавить перевод строки после текста, нам придется изменить только одну функцию в одном месте. А вызов функции останется неизменным.
<?php
function hello(){
echo 'Hello, world!';
}
hello();
hello();
hello();
?>
Что на счет ООП?
Создадим класс Текст. Он будет печатать текст и хранить этот текст внутри себя.
<?php
class Text{
public $text = 'Hello, world!';
function printText()
{
echo $this->text;
}
}
$hello = new Text();
$hello->printText();
?>
Здесь у нас класс Текст. У него есть общедоступное свойство текст, которое равно "Hello, world" и метод, который выводит этот текст на экран. Из такого простого примера не очень понятна выгода классов по отношению к простым функциям. Однако такой класс имеет рад преимуществ.
Во-первых, логика создания такого класса, его свойства и методы будут описывать одну логическую единицу нашей большой программы. И когда мы захотим использовать эту часть программы, все нужные функции и все нужные переменные уже будут внутри класса.
Во-вторых, объекты такого класса можно создать несколько раз. И использовать в разных частях программы. Объекты эти будут одинаковыми, но они смогут обладать разными свойствами.
В-третьих, наследование, полиморфизм и паттерны программирования. Здесь скрыта магия ооп. Но раскрыть ее в одной статье будет, пожалуй, невозможно.
Попробую сделать вывод. Чтобы стать хорошим программистом, нужно уметь писать, понимать и проектировать ооп. Ооп оперирует классами и объектами. Класс - это объединение нескольких функций и переменных, относящихся к одной логической части программы, программной сущности. Объект - это экземпляр класса. Конкретная реализация класса с нужными свойствами.
В будущих статьях я попробую описать все основные моменты работы с ооп. Попробую разобраться в ни сам и объяснить заинтересованным. Так что, присоединяйтесь.
В заключении. Если мои странные объяснения показались или могут показаться вам полезными, присоединяйтесь на пикабу и на ютуб. Ссылка в профиле. Осторожно, видео на непонятном языке.