پرش به مطلب اصلی

اشیاء انتقال داده (DTOs)

مفهوم کلیدی

DTOها اشیاء ساده‌ای هستند که داده‌ها را بین فرآیندها منتقل می‌کنند و مرزهای مشخصی بین لایه‌های سیستم ایجاد کرده و یکپارچگی داده را تضمین می‌کنند.

این سند استانداردها و بهترین شیوه‌های تیم ما برای پیاده‌سازی اشیاء انتقال داده (DTOs) در پروژه‌هایمان را مشخص می‌کند.

مقدمه

اشیاء انتقال داده (DTOs) اشیاء ساده‌ای هستند که داده‌ها را بین فرآیندها منتقل می‌کنند. آنها برای ایجاد مرزهای مشخص بین لایه‌های سیستم و تضمین یکپارچگی داده در سراسر برنامه ضروری هستند. این راهنما استانداردهای تیم ما برای پیاده‌سازی DTOها را تعیین می‌کند.

اطلاع

DTOها از مفاهیم سطح بالاتری مانند Data Structures، Data Types و Custom Types متمایز هستند. گاهی اوقات آنچه به عنوان DTO استفاده می‌شود ممکن است در واقع یک Custom Type باشد. این سند به طور خاص بر روی DTOها تمرکز دارد.

اصول اساسی

۱. الزام سازنده (Constructor)

هر DTO باید یک سازنده داشته باشد. یک DTO بدون سازنده بی‌معنی است زیرا نمی‌تواند یکپارچگی داده‌های خود را تضمین کند.

// DTO با سازنده
class UserDTO
{
public function __construct(
public readonly string $name,
public readonly ?string $email = null
) {}
}

مهم: یک DTO بدون سازنده نمی‌تواند قوانین یکپارچگی داده یا اعتبارسنجی را اعمال کند.

۲. ویژگی‌های اجباری

هر DTO باید حداقل یک ویژگی اجباری در سازنده خود داشته باشد. این تضمین می‌کند که وقتی یک بلوک داده ایجاد می‌شود، حاوی داده‌های ضروری است.

// DTO با ویژگی‌های اجباری
class ArticleDTO
{
public readonly string $title;
public readonly string $content;
public ?string $author = null;

public function __construct(
string $title,
string $content
) {
$this->title = $title;
$this->content = $content;
}
}

۳. بدون رفتار

DTOها نباید حاوی منطق کسب‌وکار یا رفتار باشند. آنها صرفاً حامل‌های داده هستند.

مفهوم کلیدی

ما باید بین "منطق کسب‌وکار" و "منطق ساخت" تمایز قائل شویم. DTOها نباید حاوی منطق کسب‌وکار باشند، اما می‌توانند حاوی منطق ساخت باشند.

تفاوت بین منطق کسب‌وکار و منطق ساخت

منطق کسب‌وکار نیازمند دانش دامنه، قوانین کسب‌وکار یا سرویس‌های خارجی برای تصمیم‌گیری یا انجام یک عملیات است.

ویژگی‌های منطق کسب‌وکار:

  • نیاز به وابستگی‌های خارجی دارد
  • قوانین کسب‌وکار را پیاده‌سازی می‌کند
  • وضعیت سیستم را تغییر می‌دهد
  • به پایگاه‌های داده یا سایر سرویس‌ها متصل می‌شود
// DTO با منطق کسب‌وکار (الگوی ضد)
class OrderDTO
{
public readonly float $amount;

public function __construct(float $amount)
{
$this->amount = $amount;
}

// منطق کسب‌وکار نباید در DTO باشد
public function applyDiscount(DiscountService $discountService, User $user): float
{
// نیاز به سرویس خارجی و دانش دامنه دارد
$discount = $discountService->getDiscountFor($user);
return $this->amount * (1 - $discount);
}
}

قانون طلایی برای شناسایی

قانون طلایی

برای اجرای یک متد در DTO، آیا به چیزی فراتر از پارامترهای ورودی و فیلدهای داخلی خود کلاس نیاز دارد؟

  • خیر؟ احتمالاً منطق ساخت یا یک کمک‌کننده ساده است. (✅ مجاز)
  • بله؟ این قطعاً منطق کسب‌وکار است. (🛑 ممنوع)
// DTO بدون رفتار
class OrderDTO
{
public function __construct(
public readonly float $amount
) {}
}

به یاد داشته باشید: DTOها باید ساختارهای داده ساده بدون هیچگونه منطق کسب‌وکار باشند.

۴. سازماندهی پارامترهای سازنده

پارامترهای سازنده را با این دستورالعمل‌ها سازماندهی کنید:

  1. ابتدا پارامترهای اجباری (بدون مقادیر پیش‌فرض)
  2. در آخر پارامترهای اختیاری (با مقادیر پیش‌فرض)
  3. پارامترهای مرتبط را گروه‌بندی کنید
  4. تعداد کل پارامترها را محدود کنید - در صورت زیاد بودن، استفاده از DTOهای تودرتو را در نظر بگیرید

نکته: از ویژگی constructor property promotion در PHP 8 برای کد تمیزتر استفاده کنید.

// ❌ غیر بهینه: ترکیب ویژگی‌های اجباری و اختیاری در سازنده
class ProductDTO
{
public function __construct(
public readonly string $name,
public readonly float $price,
public readonly ?string $description = null,
public readonly ?string $category = null,
public readonly ?array $tags = null
) {}
}

// ✅ بهتر: فقط ویژگی‌های اجباری در سازنده
class ProductDTO
{
public readonly ?string $description = null;
public readonly ?string $category = null;
public readonly ?array $tags = null;

public function __construct(
public readonly string $name,
public readonly float $price
) {}
}

ترتیب پارامترهای سازنده

هنگام سازماندهی پارامترها در یک سازنده، این ترتیب را دنبال کنید:

  1. پارامترهای اجباری بدون مقادیر پیش‌فرض
  2. پارامترهای nullable بدون مقادیر پیش‌فرض
  3. پارامترهای با مقادیر پیش‌فرض
class UserProfileDTO 
{
public function __construct(
// 1. پارامترهای اجباری بدون مقادیر پیش‌فرض
public readonly string $userId,
public readonly string $username,

// 2. پارامترهای nullable بدون مقادیر پیش‌فرض
public readonly ?string $email = null,
public readonly ?string $phone = null,

// 3. پارامترهای با مقادیر پیش‌فرض
public readonly string $country = 'USA',
public readonly bool $isActive = true
) {}
}

۵. بدون Getter و Setter

الگوی ضد

DTOها نیازی به getter و setter ندارند زیرا رفتاری ندارند. Getter و setter زمانی معنادار هستند که بخواهید رفتاری برای یک کلاس ایجاد و کنترل کنید.

// DTO با ویژگی public readonly
class ProductDTO
{
public function __construct(
public readonly string $name
) {}
}

انواع ویژگی‌ها

✅ ویژگی‌های Public Readonly

  • توصیه شده برای اکثر موارد
  • تغییرناپذیری را فراهم می‌کند
  • از تغییر تصادفی جلوگیری می‌کند
  • کد تمیزتر و مختصرتر

⚠️ ویژگی‌های Public

  • فقط زمانی استفاده کنید که تغییرپذیری لازم باشد
  • کمتر از ویژگی‌های readonly امن هستند
  • می‌توانند منجر به رفتار غیرمنتظره شوند
  • نیاز به مدیریت دقیق دارند
بهترین شیوه

ویژگی‌های خصوصی در DTOها در صورت افزودن یک getter، عملاً همان ویژگی‌های public readonly هستند. برای جلوگیری از کد غیرضروری، مستقیماً از ویژگی‌های public readonly استفاده کنید.

// ویژگی public readonly
class CustomerDTO
{
public function __construct(
public readonly string $name
) {}
}

ویژگی‌های Readonly

بهترین شیوه: هر زمان که ممکن است، ویژگی‌های DTO را readonly کنید. این یک ساختار قوی‌تر ایجاد می‌کند و باگ‌ها را کاهش می‌دهد، زیرا داده‌ها نمی‌توانند در طول مسیر دستکاری شوند.

یادداشت

با ویژگی‌های readonly، می‌توانید با اطمینان DTO خود را از چندین لایه عبور دهید بدون اینکه نگران تغییر تصادفی داده‌ها باشید.

اعتبارسنجی

اعتبارسنجی در مقابل رفتار

اعتبارسنجی‌هایی که در سازنده انجام می‌شوند و به عوامل خارجی (مانند پایگاه داده یا سرویس‌های شخص ثالث) وابسته نیستند، بخشی از ماهیت DTO محسوب می‌شوند، نه رفتار.

class EmailDTO 
{
public readonly string $address;

public function __construct(string $address)
{
if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address');
}

$this->address = $address;
}
}

نمونه‌های عملی از منطق ساخت و منطق کسب‌وکار

نکته مهم

تشخیص صحیح بین منطق ساخت (مجاز) و منطق کسب‌وکار (ممنوع) در DTOها یکی از مهم‌ترین مهارت‌ها در طراحی معماری لایه‌ای است.

نمونه‌هایی از منطق ساخت (مجاز در DTOها)

class ProductDTO
{
public readonly string $name;
public readonly float $price;
public readonly string $formattedPrice;
public readonly array $categories;

private function __construct(
string $name,
float $price,
string $formattedPrice,
array $categories
) {
$this->name = $name;
$this->price = $price;
$this->formattedPrice = $formattedPrice;
$this->categories = $categories;
}

// ✅ منطق ساخت: فقط از داده‌های ورودی استفاده می‌کند
public static function fromEntity(Product $product): self
{
return new self(
$product->getName(),
$product->getPrice(),
number_format($product->getPrice(), 2) . ' USD',
array_map(fn($cat) => $cat->getName(), $product->getCategories())
);
}

// ✅ منطق ساخت: DTO را به آرایه تبدیل می‌کند
public function toArray(): array
{
return [
'name' => $this->name,
'price' => $this->price,
'formatted_price' => $this->formattedPrice,
'categories' => $this->categories,
];
}
}

نمونه‌هایی از منطق کسب‌وکار (ممنوع در DTOها)

// ❌ الگوی ضد: DTO با وابستگی سرویس خارجی
class OrderDTO
{
public function __construct(
public readonly string $orderId,
public readonly float $amount,
public readonly array $items
) {}

// ❌ منطق کسب‌وکار: نیاز به سرویس‌های خارجی دارد
public function calculateFinalPrice(TaxService $taxService, DiscountService $discountService): float
{
$taxRate = $taxService->getTaxRateForOrder($this);
$discount = $discountService->getApplicableDiscount($this->orderId);

return $this->amount * (1 + $taxRate) * (1 - $discount);
}
}

نمودار جریان تصمیم‌گیری

ترتیب پارامترهای سازنده

هنگام سازماندهی پارامترها در یک سازنده، این ترتیب را دنبال کنید:

  1. پارامترهای اجباری بدون مقادیر پیش‌فرض
  2. پارامترهای nullable بدون مقادیر پیش‌فرض
  3. پارامترهای با مقادیر پیش‌فرض
class PersonDTO 
{
public readonly string $name;
public readonly int $age;
public readonly ?string $email;
public readonly ?string $phone;
public readonly string $country;
public readonly bool $isActive;

public function __construct(
// 1. پارامترهای اجباری بدون مقادیر پیش‌فرض
string $name,
int $age,

// 2. پارامترهای nullable بدون مقادیر پیش‌فرض
?string $email = null,
?string $phone = null,

// 3. پارامترهای با مقادیر پیش‌فرض
string $country = 'USA',
bool $isActive = true
) {
$this->name = $name;
$this->age = $age;
$this->email = $email;
$this->phone = $phone;
$this->country = $country;
$this->isActive = $isActive;
}
}

مثال دنیای واقعی

class AddressDTO
{
public readonly string $street;
public readonly string $city;
public readonly string $zipCode;
public readonly ?string $state;
public readonly string $country;

public function __construct(
string $street,
string $city,
string $zipCode,
?string $state = null,
string $country = 'USA'
) {
$this->street = $street;
$this->city = $city;
$this->zipCode = $zipCode;
$this->state = $state;
$this->country = $country;
}
}

خلاصه

راهنمایی‌های کلیدی DTO

  1. هر DTO باید یک سازنده داشته باشد
  2. هر DTO باید حداقل یک ویژگی اجباری داشته باشد
  3. DTOها نباید حاوی منطق کسب‌وکار یا رفتار باشند
  4. منطق ساخت (مانند متدهای کارخانه‌ای استاتیک) در DTOها مجاز است
  5. به جای ویژگی‌های خصوصی با getter، از ویژگی‌های public readonly استفاده کنید
  6. هر زمان که ممکن است، از ویژگی‌های readonly استفاده کنید
  7. اعتبارسنجی‌ها را در سازنده برای حفظ یکپارچگی داده قرار دهید
  8. ترتیب توصیه شده پارامترها را در سازنده‌ها رعایت کنید
قانون طلایی برای شناسایی رفتارهای مجاز و ممنوع

برای تعیین اینکه یک متد در DTO مجاز است یا خیر، از خود بپرسید:

آیا این متد به چیزی فراتر از پارامترهای ورودی و وضعیت داخلی خود کلاس برای انجام کارش نیاز دارد؟

  • خیر؟ احتمالاً منطق ساخت یا یک کمک‌کننده ساده است. (✅ مجاز)
  • بله؟ این قطعاً منطق کسب‌وکار است. (🛑 ممنوع)

تفاوت بین منطق کسب‌وکار و منطق ساخت

🛑 منطق کسب‌وکار (ممنوع)

  • نیازمند وابستگی‌های خارجی
  • اجرای قوانین کسب‌وکار
  • تغییر وضعیت سیستم
  • اتصال به پایگاه‌های داده یا سایر سرویس‌ها

✅ منطق ساخت (مجاز)

  • بدون وابستگی‌های خارجی
  • فقط با داده‌های ورودی و فیلدهای داخلی کار می‌کند
  • هدف آن تبدیل داده است، نه اجرای قوانین کسب‌وکار
  • مثال‌ها: متدهای کارخانه‌ای استاتیک، تبدیل آرایه

قوانین عملی:

  • DTOهای خود را در بررسی‌های کد برای اطمینان از پیروی از این استانداردها بررسی کنید
  • از قانون طلایی برای تعیین اینکه یک متد به DTO تعلق دارد یا خیر استفاده کنید
  • استفاده از الگوی متد کارخانه‌ای استاتیک برای ساخت DTO را در نظر بگیرید
  • از ابزارهای تحلیل استاتیک برای اعمال خودکار این قوانین استفاده کنید