در پروژههای PHP معمولاً با متدهایی روبهرو میشویم که ممکن است یک Object یا مقدار null برگردانند. در چنین شرایطی، کد مصرفکننده مجبور میشود قبل از هر استفاده، وجود Object را بررسی کند:
if ($user !== null) {
echo $user->getName();
}
اگر این وضعیت در بخشهای مختلف برنامه تکرار شود، تعداد شرطها افزایش پیدا میکند و خوانایی و نگهداری کد دشوارتر میشود.
Null Object Pattern راهکاری است که بهجای بازگرداندن null، یک Object واقعی با رفتار پیشفرض برمیگرداند. در نتیجه، کد مصرفکننده میتواند بدون شرطهای تکراری با Object کار کند.
این الگو در پروژههایی مانند سیستم لاگ، ارسال اعلان، مدیریت کاربران مهمان، پردازش سفارش و Chain of Responsibility کاربرد دارد. مستندات DesignPatternsPHP نیز Null Object را الگویی برای سادهکردن کد Client، کاهش بررسیهای null و کمکردن احتمال خطاهای مربوط به مقدار تهی معرفی میکند. <Cite refs={["turn0search0","turn0search12"]} />
Null Object Pattern چیست؟
Null Object Pattern یک الگوی طراحی است که بهجای مقدار null، یک Object با رفتار مشخص و پیشفرض در اختیار کد مصرفکننده قرار میدهد.
این Object معمولاً همان Interface یا Contract مربوط به Object اصلی را پیادهسازی میکند؛ اما بهجای انجام عملیات واقعی، رفتار خنثی یا پیشفرض دارد.
برای مثال، فرض کنید برنامه شما یک Logger دارد. در حالت عادی پیامها در فایل ذخیره میشوند؛ اما در بعضی محیطها مانند تست یا اجرای محلی، نمیخواهید هیچ لاگی ذخیره شود.
بهجای این کد:
if ($logger !== null) {
$logger->log('User created');
}
میتوان از یک NullLogger استفاده کرد:
$logger->log('User created');
در این حالت، اگر Logger واقعی وجود داشته باشد پیام ثبت میشود و اگر Logger از نوع Null باشد، عملیات بدون انجام کار خاصی پایان پیدا میکند.
نکته مهم این است که Null Object معمولاً بهعنوان یک الگوی مستقل در فهرست رسمی GoF قرار نمیگیرد، اما به دلیل کاربرد گسترده در طراحی شیگرا و Refactoring، بهعنوان یک الگوی رایج شناخته میشود. <Cite refs={["turn0search0","turn0search5"]} />
مشکل استفاده مستقیم از null
فرض کنید یک سیستم ارسال اعلان دارید. کاربر ممکن است اعلانگر فعال داشته باشد یا نداشته باشد:
<?php
declare(strict_types=1);
final class NotificationService
{
public function send(string $message): void
{
echo "Notification sent: {$message}";
}
}
در بخش دیگر برنامه:
<?php
$notificationService = null;
if ($notificationService !== null) {
$notificationService->send('Your order was created.');
}
این کد در ابتدا ساده به نظر میرسد؛ اما اگر در چندین کلاس و متد تکرار شود، مشکلات زیر ایجاد میشود:
- شرطهای
nullزیاد میشوند. - منطق اصلی برنامه با بررسیهای جانبی مخلوط میشود.
- احتمال فراموشکردن یک بررسی افزایش پیدا میکند.
- تعداد مسیرهای قابل تست بیشتر میشود.
- کد به مقدار
nullوابستگی پیدا میکند.
هدف Null Object این است که این شرطها را از کد مصرفکننده حذف کند.
ساختار Null Object Pattern
این الگو معمولاً از بخشهای زیر تشکیل میشود:
Interface یا Contract
Interface رفتار مشترک Object واقعی و Null Object را مشخص میکند.
Real Object
این کلاس عملیات واقعی را انجام میدهد.
Null Object
این کلاس همان Interface را پیادهسازی میکند؛ اما رفتار پیشفرض یا خنثی دارد.
Client
کدی است که با Interface کار میکند و لازم نیست بداند Object واقعی است یا Null Object.
ساختار کلی به شکل زیر است:
Client
|
v
Interface
|
+------ Real Object
|
+------ Null Object
در این ساختار Client فقط Interface را میشناسد. انتخاب Object واقعی یا Null Object معمولاً در مرحله ساخت Object یا در یک Factory انجام میشود.
مثال ساده: کاربر واقعی و کاربر مهمان
فرض کنید در سایت، بعضی کاربران وارد حساب خود شدهاند و بعضی کاربران مهمان هستند. میخواهیم نام کاربر را نمایش دهیم.
روش معمول:
<?php
$user = null;
if ($user !== null) {
echo $user->getName();
} else {
echo 'Guest';
}
با Null Object میتوان یک Interface تعریف کرد.
مرحله اول: ساخت Interface
<?php
declare(strict_types=1);
interface UserInterface
{
public function getName(): string;
public function isGuest(): bool;
}
این Interface مشخص میکند هر نوع User باید دو متد داشته باشد:
getNameisGuest
مرحله دوم: ساخت User واقعی
<?php
declare(strict_types=1);
final class User implements UserInterface
{
public function __construct(
private readonly string $name,
) {
}
public function getName(): string
{
return $this->name;
}
public function isGuest(): bool
{
return false;
}
}
کلاس User اطلاعات کاربر واقعی را نگهداری میکند.
مرحله سوم: ساخت NullUser
<?php
declare(strict_types=1);
final class NullUser implements UserInterface
{
public function getName(): string
{
return 'Guest';
}
public function isGuest(): bool
{
return true;
}
}
کلاس NullUser نیز همان Interface را پیادهسازی میکند؛ اما نام پیشفرض Guest را برمیگرداند.
مرحله چهارم: استفاده در Client
<?php
declare(strict_types=1);
function showUserName(UserInterface $user): void
{
echo $user->getName();
}
showUserName(new User('Saeed'));
// Saeed
showUserName(new NullUser());
// Guest
در این مثال، تابع showUserName دیگر نیازی ندارد بررسی کند که کاربر واقعی است یا خیر.
این همان مزیت اصلی Null Object Pattern است: Client با یک Interface کار میکند و از جزئیات نوع Object اطلاع ندارد.
مقایسه روش معمول با Null Object
روش معمول
<?php
$user = findUser();
if ($user !== null) {
echo $user->getName();
} else {
echo 'Guest';
}
روش استفاده از Null Object
<?php
$user = findUser() ?? new NullUser();
echo $user->getName();
در این روش فقط هنگام دریافت User تصمیم میگیریم که اگر مقدار واقعی وجود نداشت، یک NullUser ساخته شود.
بعد از آن، سایر بخشهای برنامه میتوانند بدون شرط از User استفاده کنند.
البته اگر این تصمیم در یک Repository یا Factory انجام شود، کد Client حتی نیازی ندارد از عبارت ?? استفاده کند.
ساخت Repository با Null Object
فرض کنید Repository وظیفه پیدا کردن کاربر را بر عهده دارد:
<?php
declare(strict_types=1);
final class UserRepository
{
public function findById(int $id): UserInterface
{
if ($id === 1) {
return new User('Saeed');
}
return new NullUser();
}
}
اکنون Client به شکل زیر نوشته میشود:
<?php
declare(strict_types=1);
$repository = new UserRepository();
$user = $repository->findById(10);
echo $user->getName();
// Guest
متد findById دیگر null برنمیگرداند؛ بلکه همیشه یک Object از نوع UserInterface تحویل میدهد.
این روش باعث میشود قرارداد متد واضحتر شود. مصرفکننده میداند که همیشه یک User دریافت میکند، حتی اگر کاربر واقعی در سیستم وجود نداشته باشد.
استفاده از Null Object برای Logger
یکی از کاربردهای رایج Null Object، ساخت Logger خنثی است.
Interface مربوط به Logger
<?php
declare(strict_types=1);
interface LoggerInterface
{
public function info(string $message): void;
public function error(string $message): void;
}
Logger واقعی
<?php
declare(strict_types=1);
final class FileLogger implements LoggerInterface
{
public function __construct(
private readonly string $filePath,
) {
}
public function info(string $message): void
{
$this->write('INFO', $message);
}
public function error(string $message): void
{
$this->write('ERROR', $message);
}
private function write(string $level, string $message): void
{
$line = sprintf(
"[%s] %s: %s%s",
date('Y-m-d H:i:s'),
$level,
$message,
PHP_EOL,
);
file_put_contents(
$this->filePath,
$line,
FILE_APPEND,
);
}
}
NullLogger
<?php
declare(strict_types=1);
final class NullLogger implements LoggerInterface
{
public function info(string $message): void
{
// عمداً هیچ کاری انجام نمیشود.
}
public function error(string $message): void
{
// عمداً هیچ کاری انجام نمیشود.
}
}
استفاده از Logger
<?php
declare(strict_types=1);
final class OrderService
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}
public function createOrder(): void
{
$this->logger->info('Order creation started');
// منطق ساخت سفارش
$this->logger->info('Order created successfully');
}
}
در محیط Production:
<?php
$logger = new FileLogger(__DIR__ . '/app.log');
$orderService = new OrderService($logger);
$orderService->createOrder();
در محیط تست:
<?php
$logger = new NullLogger();
$orderService = new OrderService($logger);
$orderService->createOrder();
در محیط تست، کد OrderService تغییر نمیکند. تنها Logger متفاوتی به آن تزریق میشود.
Null Object در Chain of Responsibility
Null Object میتواند بهعنوان Handler نهایی در Chain of Responsibility استفاده شود.
فرض کنید چند Handler برای بررسی درخواست داریم:
- بررسی احراز هویت
- بررسی سطح دسترسی
- بررسی موجودی
- Handler نهایی
اگر هیچ Handlerای نتواند درخواست را پردازش کند، میتوان بهجای null یک Handler خنثی برگرداند.
نمونه Interface:
<?php
declare(strict_types=1);
interface HandlerInterface
{
public function handle(string $request): string;
}
Handler واقعی:
<?php
declare(strict_types=1);
final class AuthenticationHandler implements HandlerInterface
{
public function handle(string $request): string
{
if ($request === 'login') {
return 'Authentication handled';
}
return 'Request passed';
}
}
Null Handler:
<?php
declare(strict_types=1);
final class NullHandler implements HandlerInterface
{
public function handle(string $request): string
{
return 'No handler action required';
}
}
در این حالت، زنجیره همیشه یک Handler معتبر دارد و نیازی نیست در انتهای زنجیره با مقدار null برخورد کنیم.
Null Object در Laravel
در Laravel میتوان از Null Object در بخشهای مختلف استفاده کرد؛ البته باید آن را متناسب با نیاز پروژه طراحی کرد.
۱. Null Notification Service
فرض کنید برای بعضی کاربران ارسال اعلان غیرفعال است. بهجای قرار دادن شرط در تمام Serviceها، میتوان دو پیادهسازی ساخت:
RealNotificationServiceNullNotificationService
<?php
declare(strict_types=1);
interface NotificationSenderInterface
{
public function send(string $recipient, string $message): void;
}
پیادهسازی واقعی:
<?php
declare(strict_types=1);
final class SmsNotificationSender implements NotificationSenderInterface
{
public function send(string $recipient, string $message): void
{
// ارسال پیامک از طریق سرویسدهنده
}
}
پیادهسازی Null:
<?php
declare(strict_types=1);
final class NullNotificationSender implements NotificationSenderInterface
{
public function send(string $recipient, string $message): void
{
// ارسال اعلان عمداً انجام نمیشود.
}
}
سپس در Service اصلی:
<?php
declare(strict_types=1);
final class BillingService
{
public function __construct(
private readonly NotificationSenderInterface $notificationSender,
) {
}
public function notifyUser(
string $mobile,
string $message,
): void {
$this->notificationSender->send($mobile, $message);
}
}
اکنون BillingService لازم نیست بداند پیامک فعال است یا غیرفعال.
۲. Null Price Calculator
در سیستم فروشگاهی ممکن است بعضی محصولات قیمت محاسبهشده نداشته باشند. میتوان یک Calculator خنثی ساخت که قیمت پیشفرض مشخصی برگرداند.
<?php
declare(strict_types=1);
interface PriceCalculatorInterface
{
public function calculate(int $productId): int;
}
پیادهسازی Null:
<?php
declare(strict_types=1);
final class NullPriceCalculator implements PriceCalculatorInterface
{
public function calculate(int $productId): int
{
return 0;
}
}
البته مقدار 0 فقط زمانی مناسب است که در منطق کسبوکار به معنی «قیمت ندارد» باشد. اگر صفر ممکن است با قیمت واقعی اشتباه شود، بهتر است از Value Object یا وضعیت مشخص دیگری استفاده کنید.
۳. Null Discount Strategy
در سیستم فروشگاهی، همه کاربران تخفیف ندارند. بهجای شرطهای متعدد میتوان از Strategy واقعی و Null Strategy استفاده کرد.
<?php
declare(strict_types=1);
interface DiscountStrategyInterface
{
public function calculate(int $price): int;
}
استراتژی تخفیف:
<?php
declare(strict_types=1);
final class TenPercentDiscount implements DiscountStrategyInterface
{
public function calculate(int $price): int
{
return (int) round($price * 0.10);
}
}
استراتژی بدون تخفیف:
<?php
declare(strict_types=1);
final class NullDiscount implements DiscountStrategyInterface
{
public function calculate(int $price): int
{
return 0;
}
}
استفاده:
<?php
$discount = new NullDiscount();
$price = 500000;
$discountAmount = $discount->calculate($price);
$finalPrice = $price - $discountAmount;
echo $finalPrice;
// 500000
این روش ترکیبی از Strategy Pattern و Null Object Pattern است.
تفاوت Null Object و مقدار null
مقدار null نشان میدهد که هیچ Object یا مقداری وجود ندارد. اما Null Object یک Object واقعی است که رفتار پیشفرض دارد.
| مقدار null | Null Object |
|---|---|
| هیچ Objectی وجود ندارد | یک Object معتبر وجود دارد |
| نیازمند بررسی است | معمولاً بدون شرط استفاده میشود |
| ممکن است خطای فراخوانی متد ایجاد کند | متدهای Contract را ارائه میدهد |
| اطلاعاتی درباره رفتار پیشفرض ندارد | رفتار پیشفرض مشخص دارد |
| برای نبودن مقدار مناسب است | برای نبودن رفتار یا Object جایگزین مناسب است |
نکته مهم این است که Null Object به معنی پنهانکردن همه خطاها نیست. اگر نبودن یک Object باید باعث توقف عملیات شود، نباید آن را با یک Null Object خنثی جایگزین کنیم.
تفاوت Null Object و Optional
در بعضی زبانها مانند Java یا Kotlin، ساختارهایی مانند Optional برای نمایش وجود یا نبودن مقدار استفاده میشوند.
Optional معمولاً به Client میگوید که باید وجود مقدار را بررسی کند؛ اما Null Object تلاش میکند یک رفتار معتبر و پیشفرض ارائه دهد.
برای مثال:
$user = findUser();
if ($user !== null) {
echo $user->getName();
}
در Null Object:
$user = findUser();
echo $user->getName();
در حالت دوم، متد findUser باید همیشه یک Object با Contract معتبر برگرداند.
تفاوت Null Object و Default Value
گاهی میتوان بهجای Object از مقدار پیشفرض استفاده کرد:
$name = $userName ?? 'Guest';
این روش برای مقادیر ساده مانند String یا Integer مناسب است.
اما اگر Object رفتارهای مختلفی داشته باشد، مقدار پیشفرض کافی نیست. برای مثال User ممکن است متدهای زیر را داشته باشد:
$user->getName();
$user->getRole();
$user->getAvatar();
$user->canPurchase();
در چنین شرایطی، یک NullUser میتواند تمام رفتارهای پیشفرض را در یک کلاس متمرکز کند.
چه زمانی از Null Object استفاده کنیم؟
Null Object زمانی مناسب است که:
- نبودن Object یک وضعیت معتبر و قابل پیشبینی باشد.
- رفتار پیشفرض مشخصی برای نبودن Object وجود داشته باشد.
- شرطهای
nullدر چندین بخش تکرار شده باشند. - Client باید با یک Interface ثابت کار کند.
- میخواهید از Polymorphism استفاده کنید.
- Object خنثی میتواند بدون آسیب به منطق برنامه عمل کند.
نمونههای مناسب:
- Logger خنثی
- ارسالکننده اعلان غیرفعال
- کاربر مهمان
- تخفیف صفر
- Handler نهایی
- خروجی خنثی
- Cache Driver بدون ذخیرهسازی
چه زمانی از Null Object استفاده نکنیم؟
اگر نبودن Object نشانه یک خطای جدی است، نباید آن را با Null Object پنهان کنیم.
برای مثال، اگر پیدا نشدن سفارش باید باعث خطای 404 شود، بهتر است همان وضعیت را واضح مدیریت کنید:
$order = $orderRepository->find($id);
if ($order === null) {
throw new OrderNotFoundException();
}
همچنین در موارد زیر باید با احتیاط از Null Object استفاده کرد:
- عملیات مالی حساس
- پرداخت و تراکنش
- احراز هویت
- کنترل دسترسی
- حذف اطلاعات
- عملیاتهایی که انجامنشدن آنها باید مشخصاً گزارش شود
در این موقعیتها، رفتار خنثی ممکن است خطا را پنهان کند و باعث ایجاد مشکل جدی شود.
مزایای Null Object Pattern
کاهش شرطهای null
کد Client به بررسیهای تکراری نیاز ندارد.
خوانایی بهتر
منطق اصلی برنامه از شرطهای جانبی جدا میشود.
استفاده از Polymorphism
Object واقعی و Null Object از یک Interface مشترک استفاده میکنند.
تستپذیری بیشتر
میتوان در تستها بهجای Mock پیچیده از Null Object استفاده کرد.
کاهش وابستگی
Client فقط Contract را میشناسد و به کلاس واقعی وابسته نیست.
رفتار پیشفرض مشخص
رفتار مربوط به نبودن Object در یک کلاس مشخص قرار میگیرد.
معایب Null Object Pattern
افزایش تعداد کلاسها
برای هر Interface ممکن است به یک Null Object جداگانه نیاز داشته باشید.
احتمال پنهانشدن خطا
اگر Null Object در جای نامناسب استفاده شود، خطاهای واقعی ممکن است دیده نشوند.
دشواری انتخاب رفتار پیشفرض
همیشه مشخص نیست که رفتار خنثی باید چه باشد.
پیچیدگی در Interfaceهای بزرگ
اگر Interface متدهای زیادی داشته باشد، ساخت Null Object سختتر میشود.
خطر بازگرداندن مقدار اشتباه
برای مثال برگرداندن 0 بهجای قیمت نامشخص ممکن است با قیمت واقعی اشتباه گرفته شود.
نکات مهم در طراحی Null Object
Interface را کوچک نگه دارید
هرچه Interface کوچکتر باشد، ساخت Null Object سادهتر خواهد بود.
رفتار پیشفرض را مستند کنید
مشخص کنید Null Object دقیقاً چه کاری انجام میدهد و چه کاری انجام نمیدهد.
از Null Object برای پنهانکردن Exception استفاده نکنید
اگر یک عملیات باید خطا بدهد، بهتر است خطا را مدیریت کنید و آن را بیصدا نکنید.
نام کلاس را واضح انتخاب کنید
نامهایی مانند موارد زیر رفتار کلاس را مشخص میکنند:
NullLoggerNullUserNullNotificationSenderNullDiscountNullHandler
Contract را حفظ کنید
Null Object باید همان Interface یا Contract مربوط به Object واقعی را پیادهسازی کند.
رفتار را با منطق کسبوکار هماهنگ کنید
رفتار Null Object باید برای پروژه معنا داشته باشد. برای مثال تخفیف صفر ممکن است منطقی باشد، اما قیمت صفر همیشه معنای درستی ندارد.
تست Null Object با PHPUnit
برای تست Null Object بهتر است رفتار پیشفرض آن را بررسی کنیم.
نمونه تست برای NullUser:
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class NullUserTest extends TestCase
{
public function testNullUserReturnsGuestName(): void
{
$user = new NullUser();
self::assertSame('Guest', $user->getName());
self::assertTrue($user->isGuest());
}
}
نمونه تست برای NullDiscount:
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class NullDiscountTest extends TestCase
{
public function testNullDiscountReturnsZero(): void
{
$discount = new NullDiscount();
self::assertSame(0, $discount->calculate(500000));
}
}
همچنین بهتر است بررسی کنید که Client بدون شرط بتواند با Null Object کار کند:
<?php
declare(strict_types=1);
final class UserPresenter
{
public function present(UserInterface $user): string
{
return sprintf(
'User: %s',
$user->getName(),
);
}
}
تست:
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class UserPresenterTest extends TestCase
{
public function testPresenterWorksWithNullUser(): void
{
$presenter = new UserPresenter();
self::assertSame(
'User: Guest',
$presenter->present(new NullUser()),
);
}
}
اشتباهات رایج در استفاده از Null Object
استفاده از Null Object برای هر نوع خطا
Null Object برای وضعیت معتبر و قابل پیشبینی مناسب است، نه برای تمام خطاها.
بازگرداندن مقدار مبهم
اگر Null Object مقدار 0 یا String خالی برگرداند، باید مشخص باشد که این مقدار چه معنایی دارد.
قرار دادن شرط داخل تمام متدهای Null Object
اگر Null Object در تمام متدهای خود شرطهای پیچیده داشته باشد، ممکن است طراحی بیش از حد پیچیده شود.
نادیدهگرفتن Contract
Null Object باید همان رفتارهای اصلی Interface را ارائه کند.
ساخت Null Object بسیار بزرگ
اگر یک Null Object متدهای زیادی داشته باشد، احتمالاً Interface اصلی بیش از حد بزرگ است و باید به چند Interface کوچکتر تقسیم شود.
جمعبندی
Null Object Pattern در PHP راهکاری برای جایگزینکردن مقدار null با یک Object معتبر و دارای رفتار پیشفرض است. این الگو با استفاده از Interface و Polymorphism باعث میشود کد Client به شرطهای تکراری وابسته نباشد.
در این الگو معمولاً یک Object واقعی و یک Null Object از Contract مشترک استفاده میکنند. Object واقعی عملیات اصلی را انجام میدهد و Null Object رفتار خنثی یا پیشفرض ارائه میدهد.
از کاربردهای مهم Null Object میتوان به موارد زیر اشاره کرد:
NullLoggerNullUserNullHandlerNullNotificationSenderNullDiscount- سرویسهای غیرفعال در Laravel
بااینحال، نباید از Null Object برای پنهانکردن خطاهای مهم مانند خطای پرداخت، نبودن سفارش یا شکست احراز هویت استفاده کرد. انتخاب این الگو باید بر اساس منطق کسبوکار و معنای واقعی وضعیت خنثی انجام شود.
سوالات متداول
Null Object Pattern در PHP چیست؟
Null Object Pattern الگویی است که بهجای بازگرداندن null، یک Object با رفتار پیشفرض برمیگرداند تا کد مصرفکننده به شرطهای تکراری نیاز نداشته باشد.
آیا Null Object یک Design Pattern رسمی GoF است؟
خیر. Null Object معمولاً در دسته الگوهای رسمی GoF قرار نمیگیرد، اما یک الگوی رایج در طراحی شیگرا و Refactoring محسوب میشود.
مهمترین مزیت Null Object چیست؟
کاهش شرطهای null، سادهترشدن کد Client و استفاده از یک Interface مشترک بین Object واقعی و Object خنثی از مهمترین مزایای آن است.
آیا Null Object در Laravel کاربرد دارد؟
بله. میتوان از آن برای Logger خنثی، سرویس اعلان غیرفعال، تخفیف صفر، Handler نهایی و سرویسهای اختیاری استفاده کرد.
آیا Null Object جایگزین Exception است؟
خیر. اگر نبودن Object نشانه خطای جدی باشد، باید Exception یا پاسخ خطای مشخص استفاده شود. Null Object فقط برای وضعیتهای معتبر و قابل پیشبینی مناسب است.
تفاوت Null Object با null چیست؟
null نشاندهنده نبودن مقدار یا Object است؛ اما Null Object یک Object واقعی است که رفتار پیشفرض را از طریق Interface ارائه میدهد.
آیا استفاده از Null Object همیشه بهتر از null است؟
خیر. اگر بررسی نبودن مقدار بخشی از منطق اصلی برنامه باشد، استفاده از null یا Exception ممکن است شفافتر باشد. Null Object زمانی مناسب است که رفتار پیشفرض مشخص و معتبر داشته باشیم.





