در ابتدا اجازه بدهید تعریف درستی از این دو واژه، ارائه کنیم.
DTO (Data Transfer Object)
به بیان خیلی ساده، DTOها برای انتقال اطلاعات استفاده میشوند؛ پس هیچ منطق و رفتاری در این اشیاء تعریف نمیشود .اگر در DTO منطقی پیاده سازی شود، دیگر به آن DTO گفته نمیشود. اجازه بدید منظورمان را از منطق یا رفتار مشخص کنیم. منطق یا رفتار، همان متدهایی هستند که در نوع داده خود تعریف میکنیم. در #C، یک DTO تنها از خصوصیتها (Properties) که از بلوکهای Get و Set تشکیل شدهاند، ساخته میشود. البته بدون کدهایی جهت اعتبار سنجی (Validation) مقادیر.
سؤال: وضعیت attribute ها و Metadataها چه میشود؟
خیلی غیر معمول نیست که از metadataها در DTO، بهمنظور اعتبار سنجی یا اهداف خاص، استفاده کنیم. بعضی از attributeها هیچ رفتاری را به DTOها اضافه نمیکنند؛ ولی استفاده از DTOها را در بخشهای دیگر سیستم، سادهتر میکنند. در نتیجه هیچکدام از attribute ها و metadataها، شرایط DTO بودن را نقض نمیکنند.
مدلهای دیگری مثل ViewModelsها و API Modelها چه میشوند؟
واژه DTO خیلی مبهم است. تنها چیزی که بیان میکند این است که شیء است و فقط و فقط شامل اطلاعات است و رفتاری ندارد. در این تعریف دربارهی کاربرد مورد نظر یک DTO چیزی گفته نشده. در بسیاری از معماریها، DTO نقش خاصی را ایفا میکند. بطور مثال در معماری MVC، از DTOها برای انقیاد داده (Binding) و ارسال اطلاعات به یک View استفاده میکنند. به همین خاطر این DTOها بعنوان ViewModel، در معماری MVC شناخته میشوند که رفتاری را در خود تعریف نمیکنند و تنها فرمت اطلاعات مورد انتظار یک View را مهیا میکنند.
پس در این سناریوی خاص، ViewModel نوعی DTO میباشد. اما باید دقت داشته باشید، همه ViewModelها را نمیتوان DTO محسوب کرد؛ مثلا در معماری MVVM، ویوو مدلهای تعریف شده، شامل رفتار هم میباشند. حتی در معماری MVC نیز گاهی اوقات منطقی به ViewModelها اضافه میشود که دیگر به آنها DTO نمیگوییم.
در صورت امکان، نام DTOها را بر اساس استفادهی آنها تعیین کنید. بطور مثال کلاسی با نام FoodDTO، مشخص نمیکند که این نوع، کجا و چگونه قرار است در معماری برنامه شما مورد استفاده قرار بگیرید؛ برعکس نامگذاری به صورت FoodViewModels کاربرد آن را صراحتا بیان میکند.
مثالی از DTO در زبان سی شارپ :
public class ProductViewModel { public int ProductId { get; set; } public string Name { get; set; } public string Description { get; set; } public string ImageUrl { get; set; } public decimal UnitPrice { get; set; } }
کپسوله سازی و DTO ها
کپسوله سازی، یکی از اصول برنامه نویسی شیءگرا میباشد. اما این کپسوله سازی به DTOها اعمال نمیشوند. به این علت که هدف کپسوله سازی، پنهان کردن فرآیند پشت صحنهی ذخیره سازی اطلاعات است؛ اما در DTO هیچ فرآیندی پیاده سازی نشده و نباید هیچ State پنهانی وجود داشته باشد. پس بحث Encapsulation در DTO منتفی است. پس کار را برای خودتان سخت نکنید؛ با تعریف private setter ها یا تبدیل کردن DTO به یک شیء غیرقابل تغییر (immutable). شما باید بهراحتی بتوانید عملیات ایجاد، نوشتن و خواندن DTOها را انجام دهید؛ همچنین باید بتوانید عملیات سریالایز کردن بر روی DTOها را بدون فرآیند سفارشی اضافهای، انجام دهید.
Field ها یا Property ها
سؤالی که مطرح میشود این است که وقتی کپسوله سازی در DTO مفهومی ندارد، چرا باید همیشه از property ها استفاده کنیم؟ چرا از فیلدها استفاده نکنیم (فیلدهای public )؟
ما میتوانیم هم از property استفاده کنیم و هم از fieldها؛ اما بعضی از فریم ورکها که کار Serialization را انجام میدهند، فقط با property ها کار میکنند. بنابراین بسته به نیاز خودتان، از fieldهای عمومی یا propertyها استفاده کنید. اما عموما از Property استفاده میکنند. البته در این پیوند، پرسش و پاسخ مفصلی در این رابطه وجود دارد.
غیرقابل تغییر بودن (Immutability) و نوع رکورد ( Record Type )
غیرقابل تغییر بودن، یکی از مزیتهای مهم در توسعه نرم افزار است. اما همانطور که در مثل بالا بیان شد، نیازی به غیرقابل تغییر کردن DTOها نیست. با ارائه رکورد در سی شارپ 9 شرایط کمی تغییر کرد. شاید عبارت مخفف دیگری که اضافه شده Data transfer Records یا (DTRs) است. یکی از روشهای تعریف DTR در سی شارپ 9، به شکل زیر است:
public record ProductDTO(int Id, string Name, string Description);
البته روش دیگری هم وجود دارد که شما propertyها را تعریف کنید و از طریق سازنده، مقدار دهی شوند. ویژگی جدید init-only این امکان را فراهم میکند که فقط در زمان مقدار دهی اولیه (initialization)، خصوصیات مقداردهی شوند و در ادامهی چرخه حیات شیء، property ها فقط خواندنی هستند. این ویژگی، recordها را غیر قابل تغییر میکند.
مثال:
public record ProductDTO { public int Id { get; init; } public string Name { get; init; } } var dto = new ProductDTO { Id = 1, Name = "some name" };
کلاسهای POCO یا همان Plain Old CLR/C# Object
شی Plain Old چیست؟ هر شیءای که Plain Old باشد، میتواند در هر جایی از برنامهی ما مورد استفاده قرار بگیرد؛ حتی در کلاسهای Test برنامه. این اشیاء هیچگونه وابستگی برای اجرا وظایف خود، به بانکهای اطلاعاتی و کتابخانههای ثالت ندارند.
برای درک بهتر این نوع کلاسها، به مثال زیر دقت کنید:
public class Product : DataObject<Product> { public Product(int id) { Id = id; InitializeFromDatabase(); } private void InitializeFromDatabase() { DataHelpers.LoadFromDatabase(this); } public int Id { get; private set; } // other properties and methods }
همانطور که مشاهده میکنید، این کلاس به متد استاتیکی برای کار با دیتابیس وابسته است؛ در نتیجه باعث میشود که کل کلاس، به وجود بانک اطلاعاتی وابسته شود. همچنین با ارث بری از کلاس پایهی دیگری، وابستگی به یک کتابخانهی ثالث ایجاد شدهاست. اجرای آزمون واحد برای چنین کلاسی، سبب fail شدن عملیات میشود. به این علت که ارتباط با بانک اطلاعاتی مورد نیاز متد DataHelpers، تامین نشدهاست. این شرایط، مثالی از الگوی Active Record Pattern میباشند. همچنین این کلاس دسترسی به منبع داده را در درون خود گنجانده است که این به معنای نقض اصل Persistence Ignorant (اصل Persistence Ignorance به طور خلاصه بیان میکند که در تحلیل و طراحی Business Logic به موضوع ذخیرهسازی (Persistence) فکر نکنید (تا جای ممکن) یا به عبارت دیگر، ذهن خود را درگیر پیچیدگیهای ذخیره سازی نکنید. برگرفته شده از breakpoint.blog.ir : روح الله دلپاک)می باشد. یکی از ویژگیهای POCO عدم نقض الگوی فوق است.
مثالی از POCO :
public class Product { public Product(int id) { Id = id; } private Product() { // required for EF } public int Id { get; private set; } // other properties and methods }
این کلاس یک POCO است:
- برای اجرای وظایف خود به فریم ورک ثالثی وابسته نیست.
- به کلاس پایهای ( Base class) نیاز ندارد.
- وابستگی به متد استاتیکی ندارد.
- می تواند در هر جایی از پروژه، نمونه سازی شود.
- اصل Persistence Ignorant را بیشتر رعایت کرده، نه بطور کامل؛ چون یک سازنده دارد که به کتابخانهی ثالثی نیازمند است (سازندهی بدون پارامتر که مورد نیاز EF میباشد).
POCO و DTO :
شاید این دو مفهموم گیج کننده باشند، ولی DTO همان POCO هست. اگر یک کلاس، DTO باشد، حتما POCO نیز هست. (مرور ویژگیهای دو مورد در بخشهای قبلی) ولی برعکس این وضعیت ممکن است صادق نباشد؛ مثال قبلی که در آن وابستگی به کتابخانهی ثالثی در سازندهی بدون پارامتر وجود داشت، DTO بودن را نقض میکرد. پس اگر هر دو حالت صادق بود، میتوان گفت این دو مفهوم یکی است.