در پروژههای PHP معمولاً با موقعیتهایی روبهرو میشویم که برای انجام یک کار، چند روش یا الگوریتم مختلف وجود دارد. برای مثال، محاسبه تخفیف میتواند بر اساس درصد، مبلغ ثابت، سطح کاربر یا نوع محصول انجام شود. همچنین برای ارسال سفارش ممکن است چند روش مانند پست، پیک، تیپاکس یا ارسال رایگان داشته باشیم.
راه ساده برای پیادهسازی چنین شرایطی، استفاده از if و switch است؛ اما با بزرگتر شدن پروژه، تعداد شرطها افزایش پیدا میکند و تغییر یا تست کد دشوارتر میشود.
اینجاست که Strategy Pattern در PHP کاربرد پیدا میکند.
الگوی Strategy یکی از الگوهای طراحی رفتاری یا Behavioral Design Patterns است. هدف آن جدا کردن الگوریتمها از کدی است که از آنها استفاده میکند تا بتوانیم الگوریتم مناسب را در زمان اجرا انتخاب یا جایگزین کنیم. در منابع آموزشی الگوهای طراحی نیز Strategy با مفاهیمی مانند Context، Strategy و Concrete Strategy معرفی میشود. <Cite refs={["turn0search1","turn0search0"]} />
Strategy Pattern چیست؟
Strategy Pattern الگویی است که مجموعهای از الگوریتمها یا رفتارهای مختلف را در کلاسهای جداگانه قرار میدهد و به همه آنها یک قرارداد مشترک میدهد.
در این الگو:
- هر الگوریتم در یک کلاس مستقل قرار میگیرد.
- همه الگوریتمها یک Interface مشترک را پیادهسازی میکنند.
- کلاس اصلی یا Context بهجای شناخت جزئیات الگوریتم، فقط با Interface کار میکند.
- استراتژی مورد استفاده میتواند در زمان ساخت شیء یا در زمان اجرای برنامه تغییر کند.
به زبان ساده، Strategy به ما اجازه میدهد بهجای نوشتن یک کلاس بزرگ با شرطهای فراوان، چند رفتار کوچک و قابل تعویض ایجاد کنیم.
اجزای اصلی Strategy Pattern
Strategy Pattern معمولاً از سه بخش تشکیل میشود:
۱. Strategy Interface
این Interface قرارداد مشترک تمام الگوریتمها را مشخص میکند.
۲. Concrete Strategy
هر کلاس Concrete Strategy یک پیادهسازی واقعی از الگوریتم است.
برای مثال:
- تخفیف درصدی
- تخفیف مبلغ ثابت
- بدون تخفیف
هرکدام یک Concrete Strategy محسوب میشوند.
۳. Context
Context کلاسی است که از Strategy استفاده میکند. این کلاس نباید به جزئیات پیادهسازی استراتژیها وابسته باشد و باید فقط از Interface مشترک استفاده کند.
مشکل استفاده از if و switch
فرض کنید میخواهیم قیمت نهایی یک محصول را بر اساس نوع تخفیف محاسبه کنیم:
<?php
function calculatePrice(
float $price,
string $discountType
): float {
if ($discountType === 'percentage') {
return $price * 0.90;
}
if ($discountType === 'fixed') {
return max(0, $price - 100000);
}
if ($discountType === 'vip') {
return $price * 0.80;
}
return $price;
}
این کد در ابتدا ساده است؛ اما اگر تعداد روشهای تخفیف زیاد شود، تابع بهمرور طولانی و پیچیده خواهد شد.
فرض کنید در آینده موارد زیر نیز اضافه شوند:
- تخفیف مخصوص کاربران جدید
- تخفیف جشنواره
- تخفیف بر اساس تعداد خرید
- تخفیف بر اساس کد تخفیف
- تخفیف مخصوص محصولات خاص
- تخفیف ترکیبی
در این شرایط، هر تغییر ممکن است نیازمند ویرایش همان تابع اصلی باشد. این موضوع باعث افزایش وابستگی و کاهش قابلیت تست میشود.
پیادهسازی Strategy Pattern در PHP
برای شروع، یک Interface به نام DiscountStrategy ایجاد میکنیم:
<?php
declare(strict_types=1);
interface DiscountStrategy
{
public function calculate(float $price): float;
}
این Interface میگوید هر استراتژی تخفیف باید متدی به نام calculate داشته باشد که قیمت نهایی را برگرداند.
استراتژی بدون تخفیف
<?php
declare(strict_types=1);
final class NoDiscountStrategy implements DiscountStrategy
{
public function calculate(float $price): float
{
return $price;
}
}
این استراتژی قیمت را بدون تغییر برمیگرداند.
استراتژی تخفیف درصدی
<?php
declare(strict_types=1);
final class PercentageDiscountStrategy implements DiscountStrategy
{
public function __construct(
private readonly float $percentage
) {
}
public function calculate(float $price): float
{
$discount = $price * ($this->percentage / 100);
return max(0, $price - $discount);
}
}
در این کلاس، مقدار تخفیف بهصورت درصدی از قیمت محاسبه میشود.
برای مثال، اگر قیمت محصول ۱ میلیون تومان و درصد تخفیف ۱۰ باشد، قیمت نهایی ۹۰۰ هزار تومان خواهد بود.
استراتژی تخفیف مبلغ ثابت
<?php
declare(strict_types=1);
final class FixedDiscountStrategy implements DiscountStrategy
{
public function __construct(
private readonly float $amount
) {
}
public function calculate(float $price): float
{
return max(0, $price - $this->amount);
}
}
در این استراتژی، مقدار مشخصی از قیمت محصول کم میشود.
استفاده از max(0, ...) باعث میشود قیمت نهایی منفی نشود.
ساخت Context
حالا باید کلاسی ایجاد کنیم که از استراتژی تخفیف استفاده کند. این کلاس را PriceCalculator مینامیم:
<?php
declare(strict_types=1);
final class PriceCalculator
{
public function __construct(
private DiscountStrategy $strategy
) {
}
public function calculate(float $price): float
{
return $this->strategy->calculate($price);
}
public function changeStrategy(
DiscountStrategy $strategy
): void {
$this->strategy = $strategy;
}
}
کلاس PriceCalculator همان Context است.
این کلاس نمیداند تخفیف چگونه محاسبه میشود. فقط میداند شیء دریافتشده از نوع DiscountStrategy است و متد calculate دارد.
استفاده از Strategyهای مختلف
<?php
$calculator = new PriceCalculator(
new PercentageDiscountStrategy(10)
);
echo $calculator->calculate(1000000);
// 900000
در این مثال، قیمت یک میلیون تومان است و تخفیف ۱۰ درصدی اعمال میشود.
حالا میتوانیم بدون تغییر کد PriceCalculator، استراتژی را عوض کنیم:
<?php
$calculator->changeStrategy(
new FixedDiscountStrategy(150000)
);
echo $calculator->calculate(1000000);
// 850000
در اینجا همان Context با یک الگوریتم متفاوت کار میکند.
این قابلیت یکی از مهمترین مزیتهای Strategy Pattern است: تغییر رفتار بدون تغییر کد اصلی استفادهکننده.
انتخاب استراتژی در زمان اجرا
گاهی اوقات نوع استراتژی از اطلاعات کاربر یا درخواست HTTP مشخص میشود.
برای مثال:
<?php
$discountType = $_GET['discount'] ?? 'none';
$strategy = match ($discountType) {
'percentage' => new PercentageDiscountStrategy(10),
'fixed' => new FixedDiscountStrategy(150000),
default => new NoDiscountStrategy(),
};
$calculator = new PriceCalculator($strategy);
echo $calculator->calculate(1000000);
در این مثال، الگوریتم بر اساس مقدار ورودی انتخاب میشود.
نکته مهم این است که شرط انتخاب استراتژی با خود الگوریتمها ترکیب نشده است. الگوریتمها در کلاسهای جداگانه قرار دارند و بخش انتخاب فقط وظیفه انتخاب کلاس مناسب را انجام میدهد.
مثال کاربردی: روشهای مختلف ارسال سفارش
فرض کنید در یک فروشگاه اینترنتی چند روش ارسال داریم:
- ارسال با پست
- ارسال با پیک
- ارسال رایگان
- ارسال سریع
بهجای قرار دادن تمام محاسبات در یک کلاس، برای هر روش ارسال یک Strategy ایجاد میکنیم.
Interface مربوط به ارسال
<?php
declare(strict_types=1);
interface ShippingStrategy
{
public function calculateCost(float $weight): float;
}
ارسال با پست
<?php
declare(strict_types=1);
final class PostShippingStrategy implements ShippingStrategy
{
public function calculateCost(float $weight): float
{
return 80000 + ($weight * 10000);
}
}
ارسال با پیک
<?php
declare(strict_types=1);
final class CourierShippingStrategy implements ShippingStrategy
{
public function calculateCost(float $weight): float
{
return 120000 + ($weight * 15000);
}
}
ارسال رایگان
<?php
declare(strict_types=1);
final class FreeShippingStrategy implements ShippingStrategy
{
public function calculateCost(float $weight): float
{
return 0;
}
}
Context مربوط به ارسال
<?php
declare(strict_types=1);
final class ShippingCalculator
{
public function __construct(
private ShippingStrategy $strategy
) {
}
public function calculate(float $weight): float
{
return $this->strategy->calculateCost($weight);
}
}
استفاده از کلاس ارسال
<?php
$shipping = new ShippingCalculator(
new PostShippingStrategy()
);
echo $shipping->calculate(2);
اگر بعداً بخواهیم ارسال با پیک را فعال کنیم، فقط Strategy را تغییر میدهیم:
<?php
$shipping = new ShippingCalculator(
new CourierShippingStrategy()
);
کلاس ShippingCalculator نیازی به تغییر ندارد.
استفاده از Strategy با Dependency Injection
یکی از بهترین روشها برای استفاده از Strategy Pattern، ترکیب آن با Dependency Injection است.
در این روش، Strategy از بیرون به Context تزریق میشود:
<?php
declare(strict_types=1);
final class ReportService
{
public function __construct(
private ReportExportStrategy $exportStrategy
) {
}
public function export(array $data): string
{
return $this->exportStrategy->export($data);
}
}
حالا میتوانیم خروجی گزارش را با فرمتهای مختلف تولید کنیم:
<?php
interface ReportExportStrategy
{
public function export(array $data): string;
}
خروجی CSV
<?php
final class CsvExportStrategy implements ReportExportStrategy
{
public function export(array $data): string
{
$lines = [];
foreach ($data as $row) {
$lines[] = implode(',', $row);
}
return implode(PHP_EOL, $lines);
}
}
خروجی JSON
<?php
final class JsonExportStrategy implements ReportExportStrategy
{
public function export(array $data): string
{
return json_encode(
$data,
JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
}
}
استفاده از ReportService
<?php
$reportService = new ReportService(
new JsonExportStrategy()
);
echo $reportService->export([
['id' => 1, 'name' => 'Product A'],
['id' => 2, 'name' => 'Product B'],
]);
در این ساختار، سرویس گزارش به JSON یا CSV وابسته نیست؛ بلکه به قرارداد ReportExportStrategy وابسته است.
پیادهسازی Strategy در Laravel
در Laravel میتوان Strategy Pattern را با Service Container، Interface و Dependency Injection پیادهسازی کرد.
فرض کنید یک سیستم پرداخت داریم که باید از چند درگاه پشتیبانی کند:
- درگاه داخلی
- درگاه بینالمللی
- پرداخت آزمایشی
- پرداخت کیف پول
ساخت Interface
<?php
namespace App\Contracts;
interface PaymentStrategy
{
public function pay(
int $amount,
array $data = []
): array;
}
استراتژی پرداخت داخلی
<?php
namespace App\Services\Payments;
use App\Contracts\PaymentStrategy;
final class LocalPaymentStrategy implements PaymentStrategy
{
public function pay(
int $amount,
array $data = []
): array {
return [
'status' => 'success',
'gateway' => 'local',
'amount' => $amount,
'reference' => 'LOCAL-' . uniqid(),
];
}
}
استراتژی پرداخت آزمایشی
<?php
namespace App\Services\Payments;
use App\Contracts\PaymentStrategy;
final class FakePaymentStrategy implements PaymentStrategy
{
public function pay(
int $amount,
array $data = []
): array {
return [
'status' => 'success',
'gateway' => 'fake',
'amount' => $amount,
'reference' => 'FAKE-' . uniqid(),
];
}
}
در پروژه واقعی باید بهجای uniqid() از سازوکار مناسب تولید شناسه تراکنش استفاده شود. این مثال فقط برای نمایش ساختار Strategy است.
ساخت PaymentService
<?php
namespace App\Services;
use App\Contracts\PaymentStrategy;
final class PaymentService
{
public function __construct(
private PaymentStrategy $strategy
) {
}
public function pay(
int $amount,
array $data = []
): array {
return $this->strategy->pay($amount, $data);
}
}
انتخاب Strategy در Controller
<?php
namespace App\Http\Controllers;
use App\Services\PaymentService;
use App\Services\Payments\FakePaymentStrategy;
use App\Services\Payments\LocalPaymentStrategy;
use Illuminate\Http\Request;
use InvalidArgumentException;
final class PaymentController
{
public function pay(Request $request): array
{
$gateway = $request->string('gateway')->toString();
$strategy = match ($gateway) {
'local' => new LocalPaymentStrategy(),
'fake' => new FakePaymentStrategy(),
default => throw new InvalidArgumentException(
'درگاه پرداخت معتبر نیست.'
),
};
$paymentService = new PaymentService($strategy);
return $paymentService->pay(
amount: (int) $request->input('amount'),
data: $request->all()
);
}
}
در پروژههای بزرگتر، بهتر است انتخاب Strategy در یک Factory یا Resolver مستقل انجام شود تا Controller بیش از حد درگیر ساخت کلاسها نشود.
ساخت Strategy Resolver
برای جدا کردن منطق انتخاب، میتوانیم Resolver بسازیم:
<?php
namespace App\Services\Payments;
use App\Contracts\PaymentStrategy;
use InvalidArgumentException;
final class PaymentStrategyResolver
{
public function resolve(string $gateway): PaymentStrategy
{
return match ($gateway) {
'local' => app(LocalPaymentStrategy::class),
'fake' => app(FakePaymentStrategy::class),
default => throw new InvalidArgumentException(
'درگاه پرداخت پشتیبانی نمیشود.'
),
};
}
}
سپس در Controller:
<?php
public function pay(
Request $request,
PaymentStrategyResolver $resolver
): array {
$strategy = $resolver->resolve(
$request->string('gateway')->toString()
);
$paymentService = new PaymentService($strategy);
return $paymentService->pay(
amount: (int) $request->input('amount'),
data: $request->all()
);
}
با این کار، مسئولیتها بهتر جدا میشوند:
- Controller درخواست را دریافت میکند.
- Resolver استراتژی مناسب را انتخاب میکند.
- PaymentService عملیات پرداخت را اجرا میکند.
- Concrete Strategy جزئیات درگاه را پیادهسازی میکند.
تفاوت Strategy Pattern با if و switch
استفاده از if یا switch همیشه اشتباه نیست. برای تعداد کمی شرط ساده، همان روش میتواند خواناتر و مناسبتر باشد.
Strategy زمانی ارزشمند میشود که:
- تعداد الگوریتمها زیاد باشد.
- الگوریتمها پیچیده باشند.
- الگوریتمها مرتب تغییر کنند.
- نیاز به تست جداگانه وجود داشته باشد.
- انتخاب الگوریتم در زمان اجرا انجام شود.
- چند بخش مختلف برنامه از الگوریتمها استفاده کنند.
مقایسه کلی:
| روش شرطی | Strategy Pattern |
|---|---|
| الگوریتمها در یک تابع جمع میشوند | هر الگوریتم کلاس مستقل دارد |
| با افزایش حالتها کد شلوغ میشود | اضافه کردن Strategy جدید سادهتر است |
| تست بخشها دشوارتر است | هر Strategy جداگانه تست میشود |
| وابستگی به جزئیات بیشتر است | Context به Interface وابسته است |
| تغییر رفتار ممکن است نیازمند تغییر تابع اصلی باشد | رفتار از بیرون قابل تعویض است |
تفاوت Strategy و State
Strategy و State شباهت زیادی دارند؛ زیرا هر دو میتوانند رفتار را در کلاسهای جداگانه قرار دهند.
اما هدف آنها متفاوت است.
Strategy
در Strategy معمولاً برنامهنویس یا سیستم تصمیم میگیرد کدام الگوریتم استفاده شود.
برای مثال:
- انتخاب روش پرداخت
- انتخاب نوع تخفیف
- انتخاب روش ارسال
- انتخاب فرمت خروجی
State
در State رفتار شیء بر اساس وضعیت داخلی آن تغییر میکند.
برای مثال، یک سفارش ممکن است وضعیتهای زیر را داشته باشد:
- ایجادشده
- پرداختشده
- ارسالشده
- تحویلشده
- لغوشده
در اینجا خود سفارش بر اساس وضعیت فعلی رفتار متفاوتی خواهد داشت.
بهصورت خلاصه:
- Strategy یعنی انتخاب الگوریتم.
- State یعنی تغییر رفتار بر اساس وضعیت داخلی.
تفاوت Strategy و Factory
Factory مسئول ساخت یا ایجاد شیء است؛ اما Strategy مسئول پیادهسازی رفتار قابل تعویض است.
برای مثال:
<?php
$strategy = PaymentStrategyFactory::make('local');
در اینجا Factory استراتژی را میسازد یا انتخاب میکند، اما عملیات پرداخت توسط خود Strategy انجام میشود.
بنابراین میتوان Factory و Strategy را با هم استفاده کرد:
- Factory: کدام Strategy ساخته شود؟
- Strategy: الگوریتم انتخابشده چگونه اجرا شود؟
تفاوت Strategy و Template Method
در Template Method معمولاً یک کلاس پایه، ساختار کلی الگوریتم را مشخص میکند و کلاسهای فرزند بخشهای خاص را تغییر میدهند.
اما در Strategy، الگوریتمها معمولاً در کلاسهای مستقل و همسطح قرار میگیرند و از طریق Interface قابل تعویض هستند.
Strategy بیشتر بر ترکیب اشیا و Dependency Injection تکیه دارد، در حالی که Template Method بیشتر بر ارثبری تکیه میکند.
مزایای Strategy Pattern در PHP
۱. کاهش شرطهای پیچیده
با جدا کردن الگوریتمها، تعداد شرطهای داخل سرویسهای اصلی کمتر میشود.
۲. رعایت اصل Open/Closed
برای اضافه کردن یک الگوریتم جدید، معمولاً لازم نیست کد Context را تغییر دهیم. کافی است Strategy جدیدی ایجاد کنیم.
۳. تستپذیری بهتر
هر Strategy را میتوان بهصورت مستقل تست کرد.
۴. استفاده مجدد از الگوریتمها
یک Strategy میتواند در چند سرویس مختلف استفاده شود.
۵. تعویض رفتار در زمان اجرا
میتوان بر اساس تنظیمات، نقش کاربر، نوع سفارش یا محیط اجرا، Strategy مناسب را انتخاب کرد.
۶. کاهش وابستگی
Context به Interface وابسته است، نه به کلاسهای پیادهسازی مشخص.
معایب Strategy Pattern
Strategy همیشه بهترین انتخاب نیست و ممکن است در پروژههای کوچک باعث پیچیدگی اضافه شود.
معایب احتمالی عبارتاند از:
- افزایش تعداد کلاسها
- نیاز به طراحی Interface مناسب
- دشوار شدن ساختار برای افراد مبتدی
- نیاز به یک مکانیزم برای انتخاب Strategy
- افزایش تعداد فایلها در پروژههای ساده
اگر فقط دو شرط ساده دارید و الگوریتمها کوتاه هستند، استفاده از Strategy ممکن است بیش از حد مهندسیشده باشد.
اشتباهات رایج در استفاده از Strategy
ایجاد Strategy برای هر شرط کوچک
اگر یک شرط ساده و ثابت است، لازم نیست حتماً برای آن کلاس جداگانه بسازید.
وابسته کردن Context به کلاسهای Concrete
کد زیر طراحی مناسبی ندارد:
<?php
final class PriceCalculator
{
public function calculate(
float $price,
PercentageDiscountStrategy $strategy
): float {
return $strategy->calculate($price);
}
}
در این حالت Context مستقیماً به یک پیادهسازی خاص وابسته است.
طراحی بهتر:
<?php
final class PriceCalculator
{
public function calculate(
float $price,
DiscountStrategy $strategy
): float {
return $strategy->calculate($price);
}
}
قرار دادن منطق انتخاب داخل همه Strategyها
هر Strategy باید روی اجرای الگوریتم خودش تمرکز کند. انتخاب Strategy بهتر است در Resolver، Factory یا لایه مناسب دیگری انجام شود.
استفاده از Interface بیش از حد عمومی
Interface باید قرارداد مشخص و قابل فهمی داشته باشد. برای مثال، متد زیر بیش از حد عمومی است:
public function execute(mixed $data): mixed;
اگر مشخص باشد Strategy دقیقاً چه کاری انجام میدهد، بهتر است امضای متد نیز واضح باشد:
public function calculate(float $price): float;
نادیده گرفتن اعتبارسنجی ورودی
Strategy نباید فرض کند تمام ورودیها معتبر هستند. باید مشخص باشد چه ورودیهایی قابل قبول هستند و خطا چگونه مدیریت میشود.
تست Strategy با PHPUnit
یکی از مزیتهای مهم Strategy، تست مستقل هر الگوریتم است.
نمونه تست برای تخفیف درصدی:
<?php
use PHPUnit\Framework\TestCase;
final class PercentageDiscountStrategyTest extends TestCase
{
public function test_it_calculates_percentage_discount(): void
{
$strategy = new PercentageDiscountStrategy(10);
$result = $strategy->calculate(1000000);
self::assertSame(900000.0, $result);
}
}
نمونه تست برای تخفیف مبلغ ثابت:
<?php
use PHPUnit\Framework\TestCase;
final class FixedDiscountStrategyTest extends TestCase
{
public function test_it_does_not_return_negative_price(): void
{
$strategy = new FixedDiscountStrategy(200000);
$result = $strategy->calculate(100000);
self::assertSame(0.0, $result);
}
}
همچنین میتوان Context را با یک Strategy ساختگی تست کرد:
<?php
final class TestDiscountStrategy implements DiscountStrategy
{
public function calculate(float $price): float
{
return 123.0;
}
}
سپس:
<?php
$calculator = new PriceCalculator(
new TestDiscountStrategy()
);
assert($calculator->calculate(500000) === 123.0);
این روش باعث میشود تست Context به منطق واقعی تخفیف وابسته نباشد.
چه زمانی از Strategy Pattern استفاده کنیم؟
استفاده از Strategy انتخاب مناسبی است وقتی:
- چند الگوریتم برای یک هدف وجود دارد.
- الگوریتمها قابل تعویض هستند.
- رفتار بر اساس شرایط مختلف تغییر میکند.
- کلاس اصلی با شرطهای زیادی روبهرو شده است.
- میخواهید الگوریتمها مستقل و قابل تست باشند.
- نیاز دارید در آینده روشهای جدید اضافه کنید.
- پروژه از Dependency Injection استفاده میکند.
چه زمانی از Strategy استفاده نکنیم؟
در شرایط زیر بهتر است ابتدا راهکار سادهتری را بررسی کنید:
- فقط یک الگوریتم وجود دارد.
- تفاوت رفتارها بسیار جزئی است.
- تعداد شرطها کم و خوانا هستند.
- پروژه کوچک و کوتاهمدت است.
- ساخت کلاسهای متعدد ارزش واقعی ایجاد نمیکند.
هدف Design Patternها پیچیده کردن کد نیست؛ هدف آنها حل مسئلههای تکرارشونده با ساختاری قابل نگهداری است.
جمعبندی
Strategy Pattern در PHP راهکاری برای جدا کردن الگوریتمها و رفتارهای مختلف از Context است. در این الگو، هر الگوریتم در یک کلاس مستقل قرار میگیرد و همه الگوریتمها از یک Interface مشترک پیروی میکنند.
با استفاده از Strategy میتوان:
- الگوریتمها را از هم جدا کرد.
- رفتار برنامه را در زمان اجرا تغییر داد.
- شرطهای طولانی را کاهش داد.
- تستنویسی را سادهتر کرد.
- اصل Open/Closed را بهتر رعایت کرد.
- در Laravel از Dependency Injection و Service Container استفاده کرد.
برای سیستمهایی مانند پرداخت، تخفیف، ارسال سفارش، خروجی گزارش، احراز هویت و انتخاب روش ذخیرهسازی، Strategy Pattern میتواند ساختار کد را بسیار قابل نگهداریتر کند.





