Коли послідовність умов стала окремим процесом
Мені було важливо, щоб:
кожне правило залишалося окремим класом;
порядок пріоритетів був видимий в одному місці;
правило могло або повернути результат, або передати обробку далі;
кожен крок можна було тестувати незалежно.
Для цього добре підійшов Laravel Pipeline.
Спільний контракт для кроків
<?php declare(strict_types=1); namespace App\Services\Commission\Steps; use App\DTO\CommissionContext;use Closure; interface CommissionStepInterface{ public function handle( CommissionContext $context, Closure $next, ): array;}Такий контракт робить усі правила однаковими з погляду Pipeline. Усередині вони можуть перевіряти зовсім різні умови, але взаємодіють із процесом однаково.
Порядок правил видно в одному місці
<?php declare(strict_types=1); namespace App\Services\Commission; use App\DTO\CommissionContext;use App\Services\Commission\Steps\CampaignCommissionStep;use App\Services\Commission\Steps\CustomerTierCommissionStep;use App\Services\Commission\Steps\DefaultCommissionStep;use App\Services\Commission\Steps\PreferredPartnerCommissionStep;use App\Services\Commission\Steps\ProductCategoryCommissionStep;use App\Services\Commission\Steps\VolumeCommissionStep;use Illuminate\Pipeline\Pipeline; final readonly class CommissionPipeline{ public function __construct( private Pipeline $pipeline, ) {} public function calculate(CommissionContext $context): array { return $this->pipeline ->send($context) ->through($this->steps()) ->thenReturn(); } private function steps(): array { return [ PreferredPartnerCommissionStep::class, ProductCategoryCommissionStep::class, CustomerTierCommissionStep::class, VolumeCommissionStep::class, CampaignCommissionStep::class, DefaultCommissionStep::class, ]; }}Ця частина рішення мені подобається найбільше.
Щоб зрозуміти пріоритет розрахунку, не потрібно відкривати великий метод і шукати вкладені if. Достатньо подивитися на список кроків зверху вниз.
Першими виконуються спеціальні правила. Якщо жодне з них не підходить, останній крок повертає стандартну комісію.
Один із кроків
<?php declare(strict_types=1); namespace App\Services\Commission\Steps; use App\DTO\CommissionContext;use App\Helpers\Percent;use App\Services\Commission\CommissionPolicy;use Closure; final readonly class DefaultCommissionStep implements CommissionStepInterface{ public function __construct( private MinimumCommissionApplier $minimumCommissionApplier, private CommissionPolicy $commissionPolicy, ) {} public function handle( CommissionContext $context, Closure $next, ): array { $percent = $this->commissionPolicy->defaultPercent( $context->subject, $context->amount, ); return [ 'commission' => $this->minimumCommissionApplier->apply( Percent::amount($context->amount, $percent), $context, ), 'original_percent' => $percent, 'percent' => $percent, 'rule' => 'default', ]; }}Що змінилося після цього?
Розрахунок не став коротшим у сенсі кількості бізнес-правил. Але його структура стала значно зрозумілішою.
Тепер:
кожне правило має окрему відповідальність;
порядок виконання видно одразу;
новий сценарій можна додати окремим кроком;
окремі правила простіше тестувати;
головний сервіс більше не росте разом із кожною новою умовою.
Для мене це був один із тих випадків, коли патерн не просто зробив код красивішим, а допоміг показати сам бізнес-процес у структурі програми.
Висновок
Laravel Pipeline часто асоціюється з middleware або послідовною обробкою даних. У цьому випадку він добре підійшов для набору пріоритетних правил розрахунку.
Найбільша користь для мене була не в тому, що зникли умовні оператори. Вони просто перемістилися у відповідні класи.
Вона була в іншому: порядок правил став явним, кожен крок отримав власну відповідальність, а розрахунок перестав виглядати як один великий метод.



