[PHP] Улучшаем архитектуру: Инверсия и внедрение зависимостей, наследование и композиция
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет. Очень часто, работая со старым (а иногда и не очень) кодом, или пытаясь применить какую-то библиотеку, сталкиваешься с ограничениями в расширении. Зачастую проблемы бы не было, будь код архитектурно грамотен. Есть множество архитектурных правил и паттернов, которые в конечном счете облегчают расширение кода, рефакторинг и переиспользование. В статье хочу затронуть некоторые из них в примерах.Давным давно в далеком далеком проекте появился сервис, отправляющий письмо с новым паролем пользователям. Примерно вот такой:
<?php
class ReminderPasswordService
{
protected function sendToUser($user, $message)
{
$this->getMailer()->send([
'from' => 'admin@example.com',
'to' => $user['email'],
'message' => $message
]);
}
public function sendReminderPassword($user, $password)
{
$message = $this->prepareMessage($user, $password);
$this->sendToUser($user, $message);
}
protected function prepareMessage($user, $password)
{
$userName = $this->escapeHtml($user['first_name']);
$password = $this->escapeHtml($password);
$message = "Привет {$userName}!
Твой новый пароль {$password}";
$message = $this->format($message);
$message = $this->addHeaderAndFooter($message);
return $message;
}
protected function format($message)
{
return nl2br($message);
}
protected function escapeHtml($string)
{
return htmlentities($string);
}
protected function addHeaderAndFooter($message)
{
$message = "<html><body>{$message}<br>С уважением, Админ!</body>";
return $message;
}
protected function getMailer()
{
return new Mailer('user', 'password', 'smtp.example.com');
}
}
В то время разработчик считал его очень гибким, т.к. можно спокойно расширить любую часть класса, поменять текст, заголовки, или что-то еще. И вот, приходит менеджер, и просит отправлять копию письма, но без пароля на адрес менеджера, а также с корпоративного почтового сервиса. И еще - только основной текст. Ну и в формате plainText, а не HTML. Программист обрадовался своей дальновидности и гибкому классу и написал вот такого наследника (он был слишком ленив, или у него было слишком мало времени, чтобы задуматься о некоторых вещах).
<?php
class ReminderPasswordCopyToManagerService extends ReminderPasswordService
{
protected function send($user, $message)
{
$this->getMailer()->send([
'from' => 'admin@example.com',
'to' => 'manager@example.com',
'message' => $message
]);
}
protected function prepareMessage($user, $password)
{
$userName = $this->escapeHtml($user['first_name']);
$message = "Привет {$userName}!
Твой новый пароль ****";
return $message;
}
protected function getMailer()
{
return new Mailer('user2', 'password2', 'smtp.corp.example.com');
}
}
Со временем сервис обрастал наследниками, использующими частично его методы, частично новые. В один прекрасный солнечный день пришел менеджер с задачей переключиться с smtp на API популярного сервиса. Класс Mailer уже не подходит, а в коде уже целый зоопарк его упоминаний. Давайте посмотрим на этом этапе, что можно было сделать вначале, чтобы эта задача не превратилась в головную боль?Dependency Injection (Внедрение зависимостей, DI)
DI - это паттерн, позволяющий не задумываться над созданием объектов, делегируя их куда-то наружу, и просто получать готовые сконфигурированные объекты внутри.
В первую очередь давайте рассмотрим, что плохого в создании большинства объектов внутри класса. Начнем с того, что это мешает нам использовать расширения, например - наследников. Для того, чтобы использовать другой класс - нам приходится вмешиваться в код самого сервиса, либо переписывать это в его потомке. Такой код гораздо сложнее поддерживать в дальнейшем. Немаловажным фактом также оказывается осложнение или вообще отсутствие возможности написать Unit тесты к сервису. Давайте представим, что мы использовали какую-то реализацию DI, которая конфигурирует и передает объекты прямо в конструктор. Наш сервис тогда будет выглядеть таким образом:
<?php
class ReminderPasswordService
{
/**
* @var Mailer
*/
protected $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
// удалили метод getMailer, заменив его protected свойством $mailer
// ...
}
Также большинство популярных реализаций позволяет подменить объект на другой для определенного сервиса, и нашему потомку не требуется уже реализовывать свой getMailer():
<?php
class ReminderPasswordCopyToManagerService extends ReminderPasswordService
{
protected function send($to, $message)
{
$this->mailer->send([
'from' => 'admin@example.com',
'to' => 'manager@example.com',
'message' => $message
]);
}
protected function prepareMessage($user, $password)
{
$userName = $this->escapeHtml($user['first_name']);
$message = "Привет {$userName}!
Твой новый пароль ****";
return $message;
}
}
Казалось бы, мы решили проблему, и добавили больше гибкости. Но чтобы решить задачу с переключением, нам придется написать наследника Mailer, берущего от родителя только собственно тип (ну или, возможно, пару методов) и полностью переписать отправку. Это не есть хорошо, и вот, что мы будем делать дальше.Принцип инверсии зависимостей (Dependency Inversion Principle, DIP)
Согласно принципу - мы не должны зависеть от конкретных реализаций, а максимально абстрагироваться от кода. Говоря простым языком - мы должны зависеть не от классов а от интерфейсов.
Давайте попробуем переписать код с использованием интерфейсов. Сразу хочу отметить, что написание интерфейсов налагает на нас больше ответственности, и мы должны хорошенько подумать, куда код будет двигаться дальше. С отправкой писем это довольно очевидно: у нас могут появится копии, скрытые копии и другие параметры электронного письма. Поэтому мы не можем просто объявить в интерфейсе мейлера метод
<?php
interface MailerInterface
{
public function send($emailFrom, $emailTo, $message);
}
Т.к. заранее список параметров сложно предугадать - мы создадим еще один интерфейс - MailMessageInterface с необходимыми сейчас геттерами и сеттерами, в дальнейшем будет проще расширять его новыми данными.
<?php
interface MailMessageInterface
{
public function setFrom($from);
public function getFrom();
public function setTo($to);
public function getTo();
public function setMessage($message);
public function getMessage();
}
и наш MailSenderInterface, соответственно, обретает вид
<?php
interface MailerInterface
{
public function send(MailMessageInterface $message);
}
Но в этом случае нам придется как-то создавать объект MailMessageInterface, и в этом нам поможет фабрика
<?php
interface MailMessageFactoryInterface
{
public function create(): MailMessageInterface;
}
Наш сервис, соответственно, обретает такой вид
<?php
class ReminderPasswordService
{
/**
* @var MailerInterface
*/
protected $mailer;
/**
* @var MailMessageFactoryInterface
*/
protected $messageFactory;
public function __construct(MailerInterface $mailer, MailMessageFactoryInterface $messageFactory)
{
$this->mailer = $mailer;
$this->messageFactory = $messageFactory;
}
protected function send($user, $messageText)
{
$message = $this->messageFactory->create();
$message->setFrom('admin@example.com');
$message->setTo($user['email']);
$message->setMessage($messageText);
$this->mailer->send($message);
}
// далее ничего не менялось
public function sendReminderPassword($user, $password)
{
$message = $this->prepareMessage($user, $password);
$this->sendToUser($user, $message);
}
protected function prepareMessage($user, $password)
{
$userName = $this->escapeHtml($user['first_name']);
$password = $this->escapeHtml($password);
$message = "Привет {$userName}!
Твой новый пароль {$password}";
$message = $this->format($message);
$message = $this->addHeaderAndFooter($message);
return $message;
}
protected function format($message)
{
return nl2br($message);
}
protected function escapeHtml($string)
{
return htmlentities($string);
}
protected function addHeaderAndFooter($message)
{
$message = "<html><body>{$message}<br>С уважением, Админ!</body>";
return $message;
}
}
Теперь этот код, хоть и очень далек от идеала, но все же может быть расширен вместо переписывания. Давайте теперь полностью взглянем на код сервиса и его наследника.
<?php
class ReminderPasswordCopyToManagerService extends ReminderPasswordService
{
protected function send($to, $messageText)
{
$message = $this->messageFactory->create();
$message->setFrom('admin@example.com');
$message->setTo('manager@example.com');
$message->setMessage($messageText);
$this->mailer->send($message);
}
protected function prepareMessage($user, $password)
{
$userName = $this->escapeHtml($user['first_name']);
$message = "Привет {$userName}!
Твой новый пароль ****";
return $message;
}
}
Наследование VS композиция.
Композиция - это по сути разбиение класса на подмножество других классов для более удобного переиспользования кода. Говоря простым языком - мы не наследуем, а выносим нужный в обоих местах код в отдельный класс.
Плюсы: 1. Мы можем спокойно использовать этот кусок кода где угодно там, где он нам потребуется еще. 2. Легко покрыть тестами маленький кусок логики, а не большой класс с вызовом кучи protected/private методов 3. Легко подменить этот класс другим, если вдруг нам где-то потребуется делать что-то иначе.Я давно для себя решил, что есть очень тонкая грань между местами, где наследование все-таки нужно, и местами, где все же лучше использовать композицию. В 90% случаев лучше использовать второе (я сейчас не говорю про ограничения вашей экосистемы, про места, где без наследования не обойтись), поэтому принимая решение в пользу композиции ошибиться сложно.Для начала хочу привести более понятный и очевидный пример, где композиция выигрывает. Мы имеем сервис, который дергает внешнее API, забирая какие-то данные
<?php
class SomeAPIService implements SomeAPIServiceInterface
{
public function getSomeData($someParam)
{
$someData = [];
// ...
return $someData;
}
}
И в последнее время мы стали недовольны производительностью сервиса, и решили кешировать данные, чтобы иметь более быстрый доступ к ним. Неискушенный разработчик может написать:
<?php
class SomeApiServiceCached extends SomeAPIService
{
public function getSomeData($someParam)
{
$cachedData = $this->getCachedData($someParam);
if ($cachedData === null) {
$cachedData = parent::getSomeData($someParam);
$this->saveToCache($someParam, $cachedData);
}
return $cachedData;
}
// ...
}
однако такая реализация не позволяет подменить API сервис другим, имплементирующим такой же интерфейс, нарушая DIP, а также сильно усложняет написание тестов. В варианте композиции кеширующий класс будет выглядеть так
<?php
class SomeApiServiceCached implements SomeAPIServiceInterface
{
private $someApiService;
public function __construct(SomeApiServiceInterface $someApiService)
{
$this->someApiService = $someApiService;
}
public function getSomeData($someParam)
{
$cachedData = $this->getCachedData($someParam);
if ($cachedData === null) {
$cachedData = $this->someApiService->getSomeData($someParam);
$this->saveToCache($someParam, $cachedData);
}
return $cachedData;
}
// ...
}
Согласитесь, тут гораздо больше гибкости, да и тесты написать гораздо проще.Вернемся к нашему старому коду и взглянем на ReminderPasswordCopyToManagerService и посмотрим, что можно вынести "за скобки". Первое, что бросается в глаза - класс наследует ненужные методы addHeaderAndFooter и format, а также метод prepareMessage сильно отличается от родителя (нарушая также принцип открытости-закрытости (Open-Closed Principe), модифицируя, а не расширяя родительский класс), и ему не нужен второй параметр Общее - тело сообщения, метод escapeHtml.Давайте попробуем вынести общее в отдельные классы.
<?php
class ReminderPasswordMessageTextBuilder
{
public function buildMessageText($userName, $password)
{
return "Привет {$userName}!
Твой новый пароль {$password}";
}
}
class Escaper
{
public function escapeHtml($string)
{
return htmlentities($string);
}
}
Если посмотрим на отличия, то в целом оба сервиса отличаются только текстом сообщения, а также получателями. Перепишем оба сервиса так, чтобы они были независимы друг от друга, и содержали в себе только отличия.
<?php
class ReminderPasswordService
{
// Обратите внимание, что свойства стали приватными
private $mailer;
private $messageFactory;
private $escaper;
private $messageTextBuilder;
public function __construct(
MailerInterface $mailer,
MailMessageFactoryInterface $messageFactory,
Escaper $escaper,
ReminderPasswordMessageTextBuilder $messageTextBuilder
) {
$this->mailer = $mailer;
$this->messageFactory = $messageFactory;
$this->escaper = $escaper;
$this->messageTextBuilder = $messageTextBuilder;
}
public function sendReminderPassword($user, $password)
{
$messageText = $this->prepareMessage($user, $password);
$message = $this->messageFactory->create();
$message->setFrom('admin@example.com');
$message->setTo($user['email']);
$message->setMessage($messageText);
$this->mailer->send($message);
}
private function prepareMessage($user, $password)
{
$userName = $this->escaper->escapeHtml($user['first_name']);
$password = $this->escaper->escapeHtml($password);
$message = $this->messageTextBuilder->buildMessageText($userName, $password);
$message = $this->format($message);
$message = $this->addHeaderAndFooter($message);
return $message;
}
// методы ниже тоже будут вынесены в отдельные классы.
private function addHeaderAndFooter($message)
{
$message = "<html><body>{$message}<br>С уважением, Админ!</body>";
return $message;
}
private function format($message)
{
return nl2br($message);
}
}
и бывший наследник
<?php
class ReminderPasswordCopyToManagerService
{
private $mailer;
private $messageFactory;
private $escaper;
private $messageTextBuilder;
public function __construct(
MailerInterface $mailer,
MailMessageFactoryInterface $messageFactory,
Escaper $escaper,
ReminderPasswordMessageTextBuilder $messageTextBuilder
) {
$this->mailer = $mailer;
$this->messageFactory = $messageFactory;
$this->escaper = $escaper;
$this->messageTextBuilder = $messageTextBuilder;
}
public function sendReminderPasswordCopyToManager($user)
{
$messageText = $this->prepareMessage($user);
$message = $this->messageFactory->create();
$message->setFrom('admin@example.com');
$message->setTo($user['email']);
$message->setMessage($messageText);
$this->mailer->send($message);
}
private function prepareMessage($user)
{
$userName = $this->escaper->escapeHtml($user['first_name']);
$message = $this->messageTextBuilder->buildMessageText($userName, '****');
return $message;
}
}
Таким образом, хоть классы и обрели ряд зависимостей, но покрыть тестами или переиспользовать отдельные участки кода стало заметно удобнее. Мы избавились от связи между ними, и спокойно можем развивать каждый отдельный класс независимо от другого.P.S. конечно же, данные классы еще далеки от идеала, но об этом в другой раз.
===========
Источник:
habr.com
===========
Похожие новости:
- [PHP, Проектирование и рефакторинг, Yii] SOLID на практике. Принцип открытости-закрытости и ActiveQuery Yii2
- [Анализ и проектирование систем, Управление разработкой, Управление проектами, Микросервисы] Скрытые расходы при переходе на микросервисы
- [Разработка под iOS, Разработка мобильных приложений, Swift] Как мы стартовали Vivid Money для iOS
- [Разработка веб-сайтов, JavaScript, Node.JS, Конференции] Fwdays'20: Node.js Middleware – никогда больше
- [Анализ и проектирование систем, Проектирование и рефакторинг, Распределённые системы, Микросервисы] Макропроблема микросервисов (перевод)
- [Программирование, Проектирование и рефакторинг, Профессиональная литература] Как событийно-ориентированная архитектура решает проблемы современных веб-приложений (перевод)
- [PHP] Приглашаю все на вебинар по вопросам безопасности PHP
- [Программирование, Проектирование и рефакторинг] Рефакторинг без особой боли
- [Анализ и проектирование систем, SQL, Проектирование и рефакторинг] Опрос. Денормализация или нет?
- [Анализ и проектирование систем, Управление разработкой, Agile] Как скрам помогает стать более сильным разработчиком?
Теги для поиска: #_php, #_php, #_solid, #_arhitektura (архитектура), #_refaktoring (рефакторинг), #_php
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:13
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет. Очень часто, работая со старым (а иногда и не очень) кодом, или пытаясь применить какую-то библиотеку, сталкиваешься с ограничениями в расширении. Зачастую проблемы бы не было, будь код архитектурно грамотен. Есть множество архитектурных правил и паттернов, которые в конечном счете облегчают расширение кода, рефакторинг и переиспользование. В статье хочу затронуть некоторые из них в примерах.Давным давно в далеком далеком проекте появился сервис, отправляющий письмо с новым паролем пользователям. Примерно вот такой: <?php
class ReminderPasswordService { protected function sendToUser($user, $message) { $this->getMailer()->send([ 'from' => 'admin@example.com', 'to' => $user['email'], 'message' => $message ]); } public function sendReminderPassword($user, $password) { $message = $this->prepareMessage($user, $password); $this->sendToUser($user, $message); } protected function prepareMessage($user, $password) { $userName = $this->escapeHtml($user['first_name']); $password = $this->escapeHtml($password); $message = "Привет {$userName}! Твой новый пароль {$password}"; $message = $this->format($message); $message = $this->addHeaderAndFooter($message); return $message; } protected function format($message) { return nl2br($message); } protected function escapeHtml($string) { return htmlentities($string); } protected function addHeaderAndFooter($message) { $message = "<html><body>{$message}<br>С уважением, Админ!</body>"; return $message; } protected function getMailer() { return new Mailer('user', 'password', 'smtp.example.com'); } } <?php
class ReminderPasswordCopyToManagerService extends ReminderPasswordService { protected function send($user, $message) { $this->getMailer()->send([ 'from' => 'admin@example.com', 'to' => 'manager@example.com', 'message' => $message ]); } protected function prepareMessage($user, $password) { $userName = $this->escapeHtml($user['first_name']); $message = "Привет {$userName}! Твой новый пароль ****"; return $message; } protected function getMailer() { return new Mailer('user2', 'password2', 'smtp.corp.example.com'); } } DI - это паттерн, позволяющий не задумываться над созданием объектов, делегируя их куда-то наружу, и просто получать готовые сконфигурированные объекты внутри.
<?php
class ReminderPasswordService { /** * @var Mailer */ protected $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } // удалили метод getMailer, заменив его protected свойством $mailer // ... } <?php
class ReminderPasswordCopyToManagerService extends ReminderPasswordService { protected function send($to, $message) { $this->mailer->send([ 'from' => 'admin@example.com', 'to' => 'manager@example.com', 'message' => $message ]); } protected function prepareMessage($user, $password) { $userName = $this->escapeHtml($user['first_name']); $message = "Привет {$userName}! Твой новый пароль ****"; return $message; } } Согласно принципу - мы не должны зависеть от конкретных реализаций, а максимально абстрагироваться от кода. Говоря простым языком - мы должны зависеть не от классов а от интерфейсов.
<?php
interface MailerInterface { public function send($emailFrom, $emailTo, $message); } <?php
interface MailMessageInterface { public function setFrom($from); public function getFrom(); public function setTo($to); public function getTo(); public function setMessage($message); public function getMessage(); } <?php
interface MailerInterface { public function send(MailMessageInterface $message); } <?php
interface MailMessageFactoryInterface { public function create(): MailMessageInterface; } <?php
class ReminderPasswordService { /** * @var MailerInterface */ protected $mailer; /** * @var MailMessageFactoryInterface */ protected $messageFactory; public function __construct(MailerInterface $mailer, MailMessageFactoryInterface $messageFactory) { $this->mailer = $mailer; $this->messageFactory = $messageFactory; } protected function send($user, $messageText) { $message = $this->messageFactory->create(); $message->setFrom('admin@example.com'); $message->setTo($user['email']); $message->setMessage($messageText); $this->mailer->send($message); } // далее ничего не менялось public function sendReminderPassword($user, $password) { $message = $this->prepareMessage($user, $password); $this->sendToUser($user, $message); } protected function prepareMessage($user, $password) { $userName = $this->escapeHtml($user['first_name']); $password = $this->escapeHtml($password); $message = "Привет {$userName}! Твой новый пароль {$password}"; $message = $this->format($message); $message = $this->addHeaderAndFooter($message); return $message; } protected function format($message) { return nl2br($message); } protected function escapeHtml($string) { return htmlentities($string); } protected function addHeaderAndFooter($message) { $message = "<html><body>{$message}<br>С уважением, Админ!</body>"; return $message; } } <?php
class ReminderPasswordCopyToManagerService extends ReminderPasswordService { protected function send($to, $messageText) { $message = $this->messageFactory->create(); $message->setFrom('admin@example.com'); $message->setTo('manager@example.com'); $message->setMessage($messageText); $this->mailer->send($message); } protected function prepareMessage($user, $password) { $userName = $this->escapeHtml($user['first_name']); $message = "Привет {$userName}! Твой новый пароль ****"; return $message; } } Композиция - это по сути разбиение класса на подмножество других классов для более удобного переиспользования кода. Говоря простым языком - мы не наследуем, а выносим нужный в обоих местах код в отдельный класс.
<?php
class SomeAPIService implements SomeAPIServiceInterface { public function getSomeData($someParam) { $someData = []; // ... return $someData; } } <?php
class SomeApiServiceCached extends SomeAPIService { public function getSomeData($someParam) { $cachedData = $this->getCachedData($someParam); if ($cachedData === null) { $cachedData = parent::getSomeData($someParam); $this->saveToCache($someParam, $cachedData); } return $cachedData; } // ... } <?php
class SomeApiServiceCached implements SomeAPIServiceInterface { private $someApiService; public function __construct(SomeApiServiceInterface $someApiService) { $this->someApiService = $someApiService; } public function getSomeData($someParam) { $cachedData = $this->getCachedData($someParam); if ($cachedData === null) { $cachedData = $this->someApiService->getSomeData($someParam); $this->saveToCache($someParam, $cachedData); } return $cachedData; } // ... } <?php
class ReminderPasswordMessageTextBuilder { public function buildMessageText($userName, $password) { return "Привет {$userName}! Твой новый пароль {$password}"; } } class Escaper { public function escapeHtml($string) { return htmlentities($string); } } <?php
class ReminderPasswordService { // Обратите внимание, что свойства стали приватными private $mailer; private $messageFactory; private $escaper; private $messageTextBuilder; public function __construct( MailerInterface $mailer, MailMessageFactoryInterface $messageFactory, Escaper $escaper, ReminderPasswordMessageTextBuilder $messageTextBuilder ) { $this->mailer = $mailer; $this->messageFactory = $messageFactory; $this->escaper = $escaper; $this->messageTextBuilder = $messageTextBuilder; } public function sendReminderPassword($user, $password) { $messageText = $this->prepareMessage($user, $password); $message = $this->messageFactory->create(); $message->setFrom('admin@example.com'); $message->setTo($user['email']); $message->setMessage($messageText); $this->mailer->send($message); } private function prepareMessage($user, $password) { $userName = $this->escaper->escapeHtml($user['first_name']); $password = $this->escaper->escapeHtml($password); $message = $this->messageTextBuilder->buildMessageText($userName, $password); $message = $this->format($message); $message = $this->addHeaderAndFooter($message); return $message; } // методы ниже тоже будут вынесены в отдельные классы. private function addHeaderAndFooter($message) { $message = "<html><body>{$message}<br>С уважением, Админ!</body>"; return $message; } private function format($message) { return nl2br($message); } } <?php
class ReminderPasswordCopyToManagerService { private $mailer; private $messageFactory; private $escaper; private $messageTextBuilder; public function __construct( MailerInterface $mailer, MailMessageFactoryInterface $messageFactory, Escaper $escaper, ReminderPasswordMessageTextBuilder $messageTextBuilder ) { $this->mailer = $mailer; $this->messageFactory = $messageFactory; $this->escaper = $escaper; $this->messageTextBuilder = $messageTextBuilder; } public function sendReminderPasswordCopyToManager($user) { $messageText = $this->prepareMessage($user); $message = $this->messageFactory->create(); $message->setFrom('admin@example.com'); $message->setTo($user['email']); $message->setMessage($messageText); $this->mailer->send($message); } private function prepareMessage($user) { $userName = $this->escaper->escapeHtml($user['first_name']); $message = $this->messageTextBuilder->buildMessageText($userName, '****'); return $message; } } =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:13
Часовой пояс: UTC + 5