diff --git a/Makefile b/Makefile index 972a86f..1e2f759 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ build: yarn export deploy: - scp -r ./out/* rinsvent@188.225.77.88:/home/rinsvent/dev/gateway_data/static/tbd \ No newline at end of file + scp -r ./out/* rinsvent@188.225.77.88:/home/rinsvent/dev/gateway_data/static/messenger \ No newline at end of file diff --git a/next.config.js b/next.config.js index 8202a6a..3cbcc60 100644 --- a/next.config.js +++ b/next.config.js @@ -4,7 +4,7 @@ const nextConfig = { swcMinify: true, images: { loader: "imgix", - path: "https://tbd.rinsvent.ru/", + path: "https://messenger.rinsvent.ru/", }, webpack(config) { config.module.rules.push({ diff --git a/package.json b/package.json index 854ef5a..f5edebe 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "tbd", + "name": "messenger", "version": "0.1.0", "private": true, "scripts": { @@ -27,4 +27,4 @@ "eslint-config-next": "12.3.1", "typescript": "4.8.4" } -} +} \ No newline at end of file diff --git a/pages/index.tsx b/pages/index.tsx index eedd8b3..02e13d3 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -19,252 +19,459 @@ const Home: NextPage = () => {
-

Trunk-Based Development

+

Symfony messenger

- Модель ветвления в системе контроля версий, в которой разработчики работают над кодом в единственной - ветке под названием ‘trunk’. Эта модель позволяет не создавать другие долгоживущие ветки и описывает - технику как именно это делать. Разработчики избегают merge конфликтов при слиянии кода, не ломают - сборку, и живут долго и счастливо -
-
-

Базовые принципы

- +

Компонент Messenger позволяет управлять асинхронным кодом в Symfony.

+

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

+

С некоторыми из них мы скоро познакомимся.

-

GIT flow VS TBD

-

Конфликты

- - - - - GIT - TBD - - - Auto merge - Yes - Yes - - - Manual merge - Yes - Yes - - - Manual merge (интеграция задачи в измененный код) - Yes - No - - -
-
-
-

Долгоживущие ветки

- Проблемы: - -

- При GIT flow долгоживущие ветки - это норма. У нас в проекте были ветки возрастом более года. - Актуализация таких веток занимала несколько дней. А ведь эти ресурсы можно было использовать на разработку новых фич! -

-

- При TBD если разработка идет непосредственно в master/trunk таких проблем нет - код актуализируется после первого pull запроса. - Разница с local state будет минимальна. В большинстве случаев получим fast forward. - При работе в feature ветке могут возникать проблемы описанные выше. Чтобы минимизировать проблемы, TBD - рекомендует ограничение жизнь ветки в 2 дня. - Это условный интервал, за который не должно накопиться много конфликтов. - Дополнительным положительным фактором является то, что основная часть команды продолжает писать в master, и этот код можно подливать в feature ветку - и тем самым уменьшать количество конфликтов, которые могут появиться в день X. Когда ветка вольется в master. - Важно соблюдать правило 2 дней! -

+

Пример, конфигурации с транспортом amqp может выглядеть так:

+
+ +
-

Разный state БД

- Это порождает ошибки разработки и деплоя. - -

При GIT flow различное состояние бд - это норма

+

Отладка

+
+ +

- При следовании TBD эти проблемы исключаются. Изменение структуры бд всегда происходит в мастер. - Разработчикам остается актуализировать ветку и проблема решена. + С помощью команды можно посмотреть как сконфигурирован транспорт. Посмотреть всю цепочку обработки сообщения перед отправкой и обработкой.

+
+ + ---------------- --------------------------------------------------------------------------- + Option Value + ---------------- --------------------------------------------------------------------------- + Service ID debug.traced.messenger.bus.default.inner + Class Symfony\\Component\\Messenger\\MessageBus + Tags - + Public no + Synthetic no + Lazy no + Shared yes + Abstract no + Autowired no + Autoconfigured no + Arguments Iterator (14 element(s)) + - Service(messenger.bus.default.middleware.traceable) + - Service(messenger.bus.default.middleware.add_bus_name_stamp_middleware) + - Service(messenger.middleware.reject_redelivered_message_middleware) + - Service(messenger.middleware.dispatch_after_current_bus) + - Service(messenger.middleware.failed_message_processing_middleware) + - Service(SVEAK\\MessengerBundle\\Middleware\\ChainMiddleware) + - Service(SVEAK\\MessengerBundle\\Middleware\\ParallelMiddleware) + - Service(SVEAK\\MessengerBundle\\Middleware\\AutoRoutingMiddleware) + - Service(SVEAK\\MessengerBundle\\Middleware\\TtlMiddleware) + - Service(SVEAK\\MessengerBundle\\Middleware\\LockMiddleware) + - Service(SVEAK\\MessengerBundle\\Middleware\\SendTimestampMiddleware) + - Service(SVEAK\\MessengerBundle\\Middleware\\LockRoutingMiddleware) + - Service(messenger.middleware.send_message) + - Service(messenger.bus.default.middleware.handle_message) + ---------------- --------------------------------------------------------------------------- -
-

Разработка

-

GIT flow

- При GIT flow нужно работать над задачей в отдельной ветке - Задач много, следовательно веток много. - Все ветки различаются. Различается набор сервисов. Различается структура бд. - Для начала разработки нужно переключиться на ветку - Обычно приходится почистить кеш. Применить миграции. Для тестов переинициализировать бд. - Без регламента на мелкие commit`ы, провоцируются ситуации когда вся задача складывается в один commit, что делает историю менее читаемой. - Так же при таком подходе могут накапливаться изменения файлов при разработке. - Когда возникает необходимость переключиться на другую задачу, то нужно спрятать незакомиченные правки (при возвращении к задаче нужно будет снова накатить патч изменений). Этот процесс занимает время и отвлекает от задачи, уменьшает желание уходить с ветки. - -

TBD

- При TBD нужно работать мелкими правками. Большую часть времени работаем в мастер. - У всех приблизительно одинаковый набор сервисов, состояние БД. - Небольшие различия в коде, вероятно, даже не повлияют на работоспособность окружения. - Если возникает необходимость переключиться на другую задачу. Нет необходимости уходить в другую ветку и пересобирать окружение. Задачу можно сделать здесь на месте. Закомитить правку и продолжить основную задачу. -
-
-

Release ready

-

GIT

- Состояние Release ready достигается путем слияния feature ветки в staging и проверке на dev стенде. - По факту мы тестируем измененный код. Отличающийся от master и production. - Что может спровоцировать ряд ошибок. -

TBD

- Состояние Release ready достигается за счет создания release ветки и проверке на dev стенде. - Как и в случае с GIT flow. Мы создаем отдельную ветку и тестируем. - Различия в том что в случае GIT на дев стенде одновременно находится ряд задач в финальной стадии реализации, нуждающиеся в мелких правках. - При TBD на дев стенде будут находиться сразу все задачи принятые в работу в разных состояниях готовности. - В данном случае тестируется ровно тот же код что окажется в production, с теми же feature flags. Это позволяет избежать ряда ошибок и гарантировать что эту ошибку возможно воспроизвести локально, а не искать ветку в рамках которой появилась ошибка. - - Есть ряд правил которые минимизируют количество ошибок на дев стенде -
    -
  • - Маленькие атомарные комиты.  - Позволяют легче воспринимать изменения и снижают риск ошибки. -
  • -
  • - Continuous review  - Теперь код не спрятан в отдельной ветке, а доступен всем. - Правки прилетают небольшие и каждый может их изучить. - Тем самым будет происходить слежение за развитием продукта и понимание что меняется -
  • -
  • - Автотесты -
  • -
  • - Парное программирование  - Позволяет снизить риск, того что будет допущена ошибка и спроектировать качественное архитектурное решение по задаче -
  • -
  • - Feature flags  - Весь не готовый код должен быть скрыт за флагом -
  • -
  • - Сложные задачи по прежнему можно делать в отдельных ветках - Важно декомпозировать  задачу так, чтобы работы по ней длились не более 2 дней. -
  • -
  • - Частые деплои.  - Уменьшает количество правок которое должно уйти в production. Следовательно и риски что-то поломать. -
  • -
+ ! [NOTE] The "debug.traced.messenger.bus.default.inner" service or alias has been removed or inlined when the container + ! was compiled. +`} + language={`txt`} + showLineNumbers={false} + startingLineNumber={1} + theme={github} + /> +
+

Messenger использует паттерн - "Цепочка обязанностей" для обработки сообщений.

+

Здесь мы видим, что наши кастомные middleware`ы были добавлены в середину цепочки между служебными и core обработчиками.

+

Служебные отвечают за перенаправление сообщения обработанного с ошибкой в другую fallback очередь, за механизм повтора обработки, за выполнение определенных сообщений после того как обработается текущее и т.д.

+

Core обработчики отвечают за непосредственно отправку сообщения и конечную обработку

-
-

Схема работы по TBD

-
- +

Использование

+

Чтобы начать использование messenger нужно создать DTO:

+
+ id; + } + + public function getContext(): array + { + return $this->context; + } +} +`} + language={`php`} + showLineNumbers={false} + startingLineNumber={1} + theme={github} + />
-
+

И затем handler:

+
+ -

TBD

- - - - Приемущества - Недостатки - - - - - Частые интеграции - Сложность. Нужно понимать SOLID. Увеличивается порог входа в проект - - - Постепенное изменение кода - Нужно делать абстракции вокруг кода который меняется - - - Возможность быстро переключиться на другую задачу - Нужно написать сервисный слой для работы по TBD. Например: как использовать feature flags - - - Всегда актуальный код - Нужно закрывать незаконченный код через флаги, что увеличивает количество кода. - - - Нет конфликтов - Более качественная итоговая архитектура механизма - - - Всегда одна структура бд - Есть вероятность, что в проекте будут оставаться “мертвые” ветки кода. - - - Уменьшение тех. долга, за счет работы с актуальной версией кода. - Нужно убирать использование флагов когда код ушел в прод - - - Нужно покрывать код тестами, легче вносить изменения. - Нужно покрывать код тестами, чтобы гарантировать работоспособность. - - - - Чаще будем встречать push reject - - -
- +use App\\Message\\CommentMessage; +use App\\Repository\\CommentRepository; +use App\\SpamChecker; +use Doctrine\\ORM\\EntityManagerInterface; +use Symfony\\Component\\Messenger\\Attribute\\AsMessageHandler; -
-

Branch by Abstraction

- Можно выделить следующие этапы: -
    -
  • Выделить интерфейс для заменяемой функциональности.
  • -
  • Заменить прямой вызов реализации в клиенте на обращение к интерфейсу.
  • -
  • Создать новую реализацию, которая реализует интерфейс.
  • -
  • Подменить старую реализацию на новую.
  • -
  • Удалить старую реализацию.
  • -
-
+#[AsMessageHandler] +class CommentMessageHandler +{ + public function __construct( + private EntityManagerInterface $entityManager, + private SpamChecker $spamChecker, + private CommentRepository $commentRepository, + ) { + } + public function __invoke(CommentMessage $message) + { + $comment = $this->commentRepository->find($message->getId()); + if (!$comment) { + return; + } + + if (2 === $this->spamChecker->getSpamScore($comment, $message->getContext())) { + $comment->setState('spam'); + } else { + $comment->setState('published'); + } + + $this->entityManager->flush(); + } +} +`} + language={`php`} + showLineNumbers={false} + startingLineNumber={1} + theme={github} + /> +
+

Допустимо создавать множество handler`ов на одну DTO. В этом случае вызовутся все обработчики.

+
-

Пример, реализации задачи

-

Имеем механизм отправки писем

+

Отправка сообщения

+

В простом виде для того чтобы отправить сообщение нужно "заинжектить" транспорт в конструкторе и затем отправить через него DTO.

send($data); + public function __construct( + private CommentRepository $commentRepository, + private MessageBusInterface $bus, + ) { + } + + public function exampleAction( + Request $request, + ): Response { + $comment = $this->commentRepository->find(1); + $this->bus->dispatch(new CommentMessage($comment->getId(), [])); + return $this->redirectToRoute('conference', ['slug' => $conference->getSlug()]); + } } -...`} +`} language={`php`} showLineNumbers={false} startingLineNumber={1} theme={github} />
-

Выделяем интерфейс

+

В этом случае сообщение пройдет всю цепочку обработки через все middleware`ы.

+

В messenger.middleware.send_message обработчике сообщение будет сериализовано и отправлено в очередь.

+

В более сложных вариантах использования может потребоваться сделать какую-то обработку сообщения.

+

Например:

+

+

+

+

+ В этом случае нужно обернуть DTO в спец объект Envelope, который помимо самого сообщения может содержать вспомогательную информацию - объекты имплементирующие интерфейс Symfony\Component\Messenger\Stamp\StampInterface. Например: +

key; + } + + public function setKey(?string $key): self + { + $this->key = $key; + return $this; + } + + public function getTtl(): ?int + { + return $this->ttl; + } + + public function setTtl(?int $ttl): self + { + $this->ttl = $ttl; + return $this; + } + + public function getUseLock(): ?bool + { + return $this->useLock; + } + + public function setUseLock(?bool $useLock): self + { + $this->useLock = $useLock; + return $this; + } + + public function getParallel(): ?bool + { + return $this->parallel; + } + + public function setParallel(?bool $parallel): self + { + $this->parallel = $parallel; + return $this; + } + + public function getAfterParallelMessage(): ?object + { + return $this->afterParallelMessage; + } + + public function setAfterParallelMessage(?object $afterParallelMessage): self + { + $this->afterParallelMessage = $afterParallelMessage; + return $this; + } + + public function getTimestamp(): ?int + { + return $this->timestamp; + } + + public function setTimestamp(?int $timestamp): self + { + $this->timestamp = $timestamp; + return $this; + } + + public function getNeedResolveOrderConflict(): ?bool + { + return $this->needResolveOrderConflict; + } + + public function setNeedResolveOrderConflict(?bool $needResolveOrderConflict): self + { + $this->needResolveOrderConflict = $needResolveOrderConflict; + return $this; + } + + public function getMessages(): array + { + return $this->messages; + } + + public function addMessage(object $message): self + { + $this->messages[] = $message; + return $this; + } + + public function popMessage(): ?object + { + return array_shift($this->messages); + } + + public function getLockKey(): string + { + $this->assertHasKey(); + return $this->key . ':lock'; + } + + public function getTimestampKey(): string + { + $this->assertHasKey(); + return $this->key . ':timestamp'; + } + + public function getOrderConflictKey(): string + { + $this->assertHasKey(); + return $this->key . ':order_conflict'; + } + + public function getParallelKey(): string + { + $this->assertHasKey(); + return $this->key . ':parallel'; + } + + public function assertHasKey(): void + { + if (!$this->key) { + throw new \\InvalidArgumentException('Key not found'); + } + } } `} language={`php`} @@ -273,113 +480,306 @@ public function send(EmailSender $sender, array $data): void theme={github} />
-

Интегрируем его в старый механизм

+

Отправить штамп можно так:

key('user_1') + ->lock(); +$envelope = Envelope::wrap($data); +$envelope = $envelope->with($context); + +$this->bus->dispatch($envelope); +`} language={`php`} showLineNumbers={false} startingLineNumber={1} theme={github} />
-

Заменяем прямой вызов на обращение через интерфейс

+

Эта информация будет доступна в middleware и ее можно использовать, например так:

send($data); -}`} + public function __construct( + private RedisClient $client, + ) { + } + + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + /** @var ?ContextStamp $stamp */ + $stamp = $envelope->last(ContextStamp::class); + if (!$stamp || !$stamp->getUseLock()) { + return $stack->next()->handle($envelope, $stack); + } + + $receivedStamp = $envelope->last(ReceivedStamp::class); + // Если воркер обработал сообщение, то снимаем lock. Даже если код упал. + if ($receivedStamp) { + try { + return $stack->next()->handle($envelope, $stack); + } finally { + $this->client->unlock($stamp->getLockKey()); + } + } + + // Если отправка залочена, то прекращаем обработку + if (!$this->client->lock($stamp->getLockKey())) { + return $envelope; + } + + return $stack->next()->handle($envelope, $stack); + } +} +`} language={`php`} showLineNumbers={false} startingLineNumber={1} theme={github} />
-

Делаем новую реализацию

+

В начале middeware есть проверка на наличие нашего штампа. Если его нет, то обработчик пропускает и запускается следующий элемент цепочки.

+

+ Далее мы проверяем наличие ReceivedStamp. + Если его нет, значит мы находимся на этапе отправки. + Мы пробуем поставить блокировку в редисе и если нам удалось, то идем дальше, иначе останавливаем обработку сообщения. + Если же ReceivedStamp есть, то значит мы в консамере. Здесь нам нужно прокинуть сообщение дальше по цепочке и после обработки снимаем блокировку. Даже в случае ошибки. Чтобы не заблочить выполнение других сообщений. +

+
+
+

Получаем сообщение

+

Для того чтобы получить сообщение из очереди и обработать его, нужно выполнить команду

-

Заменяем вызов старого механизма на новый

+

Команда будет работать в режиме демона. То есть работать в бесконечном цикле, засыпая на каждой итерации.

+

На каждой итерации messenger будет проходиться по всем сконфигурированным транспортам, которые должны обработаться командой.

+

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

+

Для локальной разработки чтобы при переходе с ветки на ветку код обновлялся в демоне было принято решение добавить опцию --time-limit=10 в команду консамера, чтобы каждые 10 секунд демон перезагружался

+
+
+

Наша реализация

+

Для более удобного использования было принято решения сделать сервис для работы с messenger.

emailSender = $emailSender; - $this->smsSender = $smsSender; +grabContextStamp(); + $data = $context->popMessage(); + if (!$data) { + throw new \\InvalidArgumentException('Message not found!!!'); + } + $envelope = Envelope::wrap($data); + $envelope = $envelope->with($context); + + $this->bus->dispatch($envelope); + $this->context = null; + } + + public function key(string $key): self + { + $context = $this->grabContextStamp(); + $context->setKey($key); + return $this; + } + + public function ttl(int $ttl): self + { + $context = $this->grabContextStamp(); + $context->setTtl($ttl); + return $this; + } + + public function lock(): self + { + $context = $this->grabContextStamp(); + $context->setUseLock(true); + return $this; + } + + public function parallel(): self + { + $context = $this->grabContextStamp(); + $context->setParallel(true); + return $this; + } + + public function ifNewer(int $timestamp): self + { + $context = $this->grabContextStamp(); + $context->setTimestamp($timestamp); + return $this; + } + + public function resolveOrderConflict(): self + { + $context = $this->grabContextStamp(); + $context->setNeedResolveOrderConflict(true); + return $this; + } + + public function addMessage(object $message): self + { + $context = $this->grabContextStamp(); + $context->addMessage($message); + return $this; + } + + public function setAfterParallelMessage(object $message): self + { + $context = $this->grabContextStamp(); + $context->setAfterParallelMessage($message); + return $this; + } + + private function grabContextStamp(): ContextStamp + { + return $this->context = ($this->context ?? new ContextStamp()); + } } -public function execute() { - feature(FeatureEnum::UseSmsSender) - ? $this->send($this->smsSender, []) - : $this->send($this->emailSender, []); -}`} +`} language={`php`} showLineNumbers={false} startingLineNumber={1} theme={github} />
-

либо сразу через DI

+

Он реализован по паттерну builder, где все состояние хранится в объекте контексте, который всегда очищается после вызова метода send

+

В сервисе заложен набор фич с реализацией определенных кейсов работы с очередями, которые неоднократно применялись на проектах.

+

Кейсы:

+

Задача должна выполнить 1 раз. Следующая может начаться только если 1 закончилась.

+

В данном случае ставится лок в редис. Если лок есть, то новое сообщение игнорируется

superBus + ->key('user_1') // Ключ запроса + ->lock() // Флаг блокировки. Если стоит, то применится механизм. Ключ будет построен на основе key + ->addMessage(new Test('asdf')) + ->send(); `} - language={`yaml`} + language={`php`} showLineNumbers={false} startingLineNumber={1} theme={github} />
-

и в коде будет так

+

Нужно игнорировать сообщения дата отправки которых меньше, чем те которые уже получили

+

В данном случае в редис пишется метка времени последнего отправленного сообщения + Следующее сообщение будет сравниваться с установленным значением. Если значение меньше, сообщение игнорится. + Если больше, то отправляем и ставим новое значение в редис.

send($this->sender, []) -}`} +$this->superBus + ->key('user_1') + ->ifNewer(121) + ->addMessage(new Test('asdf')) + ->send(); // Отправится, так как первое +$this->superBus + ->key('user_1') + ->ifNewer(120) + ->addMessage(new Test('asdf')) + ->send(); // Будет проигнорировано +$this->superBus + ->key('user_1') + ->ifNewer(122) + ->addMessage(new Test('asdf')) + ->send(); // Отправится, так как оно более свежее +`} language={`php`} showLineNumbers={false} startingLineNumber={1} theme={github} />
-
- -
-

Feature Flags

-

Суть подхода в том чтобы обернуть код в if с проверкой пред. установленной опции. - Например,

+

Отправка сообщения, которое должно обработаться через 23 секунды

+

В данном случае добавляется stamp с delay меткой. Messenger сам поймет что сообщение ttl. Создаст новую ttl ветку и отправит туда сообщение.

superBus + ->ttl(23) + ->addMessage(new Test('asdf')) + ->send(); +`} language={`php`} showLineNumbers={false} startingLineNumber={1} theme={github} />
-

или

+

Гарантирование порядка выполнения сообщений

+

+ 1. 1 очередь и много воркеров. 2 сообщение может обработаться быстрее более быстрым воркером чем первое +

+

+ 2. Сообщения в разных очередях и разные воркеры. Когда есть 2 зависимые задачи. Например, в одну очередь отправляем сообщение о добавлении продукта, а в другую о добавлении настроек продуктов. +

+

+ В данном случае в редисе ставится лок на отправку сообщений. + Если ключ уже залочен, то бедет создана лок очередь и сообщения будут отпарвлены туда. + Как только базовое сообщение обработается, воркер заберет остальные сообщения из лок очереди и выполнит их в этом же потоке. + Если базовое сообщение не успеет обработаться в течение часа, сообщения будет перенаправлено в sync очередь, где обработаются синхронно 1 воркером

superBus + ->key('product_1') + ->resolveOrderConflict() + ->addMessage(new Product('111')) + ->send(); +$this->superBus + ->key('product_1') + ->resolveOrderConflict() + ->addMessage(new ProductSettings('111')) + ->send(); `} language={`php`} showLineNumbers={false} @@ -387,42 +787,65 @@ public function execute() { theme={github} />
-

- Все флаги описываются в enum, что позволяет быстро отследить все места в проекте где флаг используется. - Для применения флагов на проекте, было реализовано следующее решение: -

- -

Для удобного управления флагами и мониторинга состояния активности флага, на коленке была собрана служебная страница

-
- +

Цепочка задач. Когда нужно гарантировать порядок выполнения задач и все задачи заранее известны.

+

В данном случае в rabbit улетят все сообщения разом и обработаются в такой же последовательности

+
+ superBus + ->addMessage(new Product('111')) + ->addMessage(new Product('222')) + ->addMessage(new Product('333')) + ->addMessage(new Product('444')) + ->send(); +`} + language={`php`} + showLineNumbers={false} + startingLineNumber={1} + theme={github} + /> +
+

Запустить задачи параллельно

+

В данном случае в rabbit улетят все сообщения отдельно. Последовательность обработки не гарантирована

+
+ superBus + ->parallel() + ->addMessage(new Product('111')) + ->addMessage(new Product('222')) + ->addMessage(new Product('333')) + ->addMessage(new Product('444')) + ->send(); +`} + language={`php`} + showLineNumbers={false} + startingLineNumber={1} + theme={github} + /> +
+

Выполнить задачу после выполнения ряда параллельных задач

+

В данном случае в rabbit улетят все параллельные сообщения отдельно. Последовательность обработки не гарантирована. + После того как все сообщения выполнятся будет снята блокировка в редисе и выполнится последнее сообщение

+
+ superBus + ->key('user_2') + ->parallel() + ->addMessage(new Test('111')) + ->addMessage(new Test('222')) + ->addMessage(new Test('333')) + ->addMessage(new Test('444')) + ->setAfterParallelMessage(new Test('555')) + ->send(); +`} + language={`php`} + showLineNumbers={false} + startingLineNumber={1} + theme={github} + />
-

Правил с флагами будет два:

-
    -
  • После того, как функциональность полностью протестирована и стабильно работает, флаг нужно удалить.
  • -
  • Мест в коде, где идет ветвление по одному и тому же feature флагу, должно быть минимальное количество.
  • -
-
-
-

Декомпозиция задач и Short-lived ветки

- Все ветки кода, кроме главной, должны иметь короткий срок жизни, максимум – несколько дней. - Этого можно добиться за счет мелкой декомпозиции: ветка будет небольшой, если она решает небольшую задачу. - Правильная постановка задач на этапе планирования играет очень важную роль. - Можно, например, придерживаемся принципа декомпозиции задач INVEST. - - Декомпозиция по INVEST определяет, каким должен быть пользовательский сценарий (user story): - - Каждую из планируемых задач нужно «прогнать» по всем этим пунктам. Если по результатам выпадает хотя бы один из них, задачу нужно декомпозировать заново.