تفاوت Usecase و DTO در معماری نرم‌افزار – راهنمای کامل برای توسعه‌دهندگان

نکته‌ی مهم

من بیشتر برنامه‌هایی که توسعه می‌دهم با #C هستند و تمایل دارم که با Go آنها را انجام دهم. از آنجایی که سرعت توسعه برنامه‌ها در #C برای من بیشتر است، کمتر سراغ Go می‌روم. اما در این مقاله، برای نشان دادن تفاوت‌ها، از یک پروژه‌ی عملی Go استفاده کرده‌ام تا هم با زبان Go آشنا شوید و هم تفاوت این دو مفهوم را به‌خوبی درک کنید.

۱. مقدمه

اگر تا به حال با معماری‌های لایه‌ای مثل Clean Architecture، Onion Architecture یا Hexagonal Architecture کار کرده باشید، حتماً با دو اصطلاح Usecase (مورد استفاده) و DTO (شیء انتقال داده) برخورد کرده‌اید. بسیاری از توسعه‌دهندگان تازه‌کار این دو را یکی می‌دانند، اما خیر، به هیچ وجه یکی نیستند! این دو مفهوم کاملاً متفاوت هستند و هر کدام نقش جداگانه‌ای در معماری نرم‌افزار دارند.

در این مقاله، با مثال‌های عملی از یک پروژه‌ی Go که اخیراً برای یکی از دوستان نوشتم، تفاوت را به‌خوبی نشان می‌دهیم.

۲. Usecase (یا سرویس) چیست؟

Usecase لایه‌ی منطق کسب‌وکار (Business Logic) است. یعنی جایی که قوانین اصلی برنامه شما نوشته می‌شود.

  • رفتار (Behavior) دارد: شامل متدها و توابع است.
  • کارش: اعتبارسنجی‌های سنگین، محاسبات، شرط‌ها و تصمیم‌گیری‌های اصلی را انجام می‌دهد.
  • به هیچ‌کس وابسته نیست: به فریم‌ورک (مثل Echo در Go یا ASP.NET Core در #C) و دیتابیس (مثل GORM یا Entity Framework) دسترسی مستقیم ندارد و فقط با اینترفیس‌ها (Repository) کار می‌کند.

مثال عینی در یک پروژه‌ی Go

// internal/usecase/shift_usecase.go

type shiftUsecase struct {
    workerRepo WorkerRepository
    shiftRepo  ShiftRepository
}

func (u *shiftUsecase) CreateBulk(workerIDs []uint, lineNumber int, shiftType string, startTime, endTime time.Time, supervisorName string) (int, error) {
    // ۱. اعتبارسنجی
    if startTime.After(endTime) {
        return 0, errors.New("ساعت شروع باید قبل از ساعت پایان باشد")
    }
    
    // ۲. بررسی اینکه کارگرها در دیتابیس وجود دارند
    workers, err := u.workerRepo.GetByIDs(workerIDs)
    if err != nil {
        return 0, err
    }
    
    // ۳. محاسبه‌ی دقیقه‌ها
    totalMinutes := int(endTime.Sub(startTime).Minutes())
    
    // ۴. ساخت رکوردها و ذخیره‌سازی
    records := make([]Shift, len(workers))
    for i, worker := range workers {
        records[i] = Shift{
            WorkerID:      worker.ID,
            LineNumber:    lineNumber,
            ShiftType:     shiftType,
            StartTime:     startTime,
            EndTime:       endTime,
            TotalMinutes:  totalMinutes,
            SupervisorName: supervisorName,
        }
    }
    
    // ۵. دستور ذخیره‌سازی را به Repository می‌دهد
    err = u.shiftRepo.CreateBulk(records)
    if err != nil {
        return 0, err
    }
    
    return len(records), nil
}

معادل این کد در #C (با Clean Architecture)

public class ShiftService : IShiftService
{
    private readonly IWorkerRepository _workerRepo;
    private readonly IShiftRepository _shiftRepo;

    public ShiftService(IWorkerRepository workerRepo, IShiftRepository shiftRepo)
    {
        _workerRepo = workerRepo;
        _shiftRepo = shiftRepo;
    }

    public async Task<int> CreateBulkAsync(List<int> workerIds, int lineNumber, string shiftType, 
                                          DateTime startTime, DateTime endTime, string supervisorName)
    {
        // ۱. اعتبارسنجی
        if (startTime > endTime)
            throw new ArgumentException("ساعت شروع باید قبل از ساعت پایان باشد");

        // ۲. بررسی وجود کارگرها
        var workers = await _workerRepo.GetByIdsAsync(workerIds);
        
        // ۳. محاسبه‌ی دقیقه‌ها
        var totalMinutes = (int)endTime.Subtract(startTime).TotalMinutes;
        
        // ۴. ساخت رکوردها و ذخیره‌سازی
        var records = workers.Select(w => new Shift
        {
            WorkerId = w.Id,
            LineNumber = lineNumber,
            ShiftType = shiftType,
            StartTime = startTime,
            EndTime = endTime,
            TotalMinutes = totalMinutes,
            SupervisorName = supervisorName
        }).ToList();
        
        // ۵. ذخیره‌سازی
        await _shiftRepo.CreateBulkAsync(records);
        
        return records.Count;
    }
}

۳. DTO (Data Transfer Object) چیست؟

DTO یک حامل داده (حاوی خواص/فیلد) است که هیچ منطقی (متد) درون خود ندارد.

  • رفتار ندارد: فقط getter و setter (یا در Go همان فیلدهای ساده) دارد.
  • کارش: انتقال داده بین لایه‌های مختلف (مثلاً از لایه‌ی Handler به Usecase، یا از Usecase به Client) است.
  • مزیت: DTOها باعث می‌شوند مدل‌های داخلی دیتابیس (Core) در معرض دید بیرون قرار نگیرند و امنیت و استقلال برنامه حفظ شود.

مثال عینی در پروژه‌ی Go (همان پروژه‌ی شیفت)

// internal/handler/shift_handler.go

// این یک DTO است. فقط داده را از JSON خارج می‌کند و به Usecase می‌دهد.
type CreateBulkRequest struct {
    WorkerIDs      []uint `json:"worker_ids"`
    LineNumber     int    `json:"line_number"`
    ShiftType      string `json:"shift_type"`
    CartonCount    *int   `json:"carton_count"`
    StartTime      string `json:"start_time"`
    EndTime        string `json:"end_time"`
    SupervisorName string `json:"supervisor_name"`
}

معادل این DTO در #C

public class CreateBulkRequestDto
{
    public List<int> WorkerIds { get; set; }
    public int LineNumber { get; set; }
    public string ShiftType { get; set; }
    public int? CartonCount { get; set; }
    public string StartTime { get; set; }
    public string EndTime { get; set; }
    public string SupervisorName { get; set; }
}

۴. مقایسه‌ی مستقیم در یک جدول

ویژگی Usecase (مورد استفاده) DTO (شیء انتقال داده)
نقش اصلی اجرای قوانین کسب‌وکار و منطق برنامه حمل و نقل داده بین لایه‌ها
محتوای آن متدها و توابع (func) فقط فیلدها و پراپرتی‌ها (struct)
آیا متد دارد؟ بله، پر از متد است خیر، فقط داده دارد (یا متدهای خیلی ساده برای تبدیل)
در کجای پروژه است؟ در پوشه‌ی internal/usecase معمولاً در پوشه‌ی internal/handler (ورودی) یا در یک پوشه‌ی مجزای dtos
معادل در #C کلاس ShiftService یا ShiftApplicationService کلاس ShiftCreateDto با { get; set; }
آیا به فریم‌ورک وابسته است؟ خیر (مستقل است) خیر (فقط داده است)

۵. تفاوت در #C (برای روشن‌تر شدن ذهنیت شما)

اگر بخواهیم همان پروژه را در Clean Architecture #C پیاده‌سازی کنیم:

  • Usecase: همان ShiftService در لایه‌ی Application که متد Handle(CreateShiftCommand command) را دارد. (اجرای منطق)
  • DTO: همان CreateShiftRequestDto که در لایه‌ی API (کنترلر) قرار دارد و فقط شامل public int WorkerId { get; set; } است.

نکته‌ی مهم در Clean Architecture: در معماری تمیز، Usecase هرگز نباید DTO را مستقیم دریافت کند! در کد Goای که نوشتم، برای سرعت، DTO را در Handler گرفتم و آن را به پارامترهای ساده برای Usecase تبدیل کردم. اما در پروژه‌های بزرگ‌تر، شما یک DTO مخصوص Usecase هم تعریف می‌کنید (مثلاً CreateShiftCommand) تا داده‌ها را با ساختاری مشخص به Usecase بدهید.

۶. خلاصه‌ی نهایی

مفهوم نقش مثال در Go مثال در #C
Usecase مغز متفکر برنامه (منطق + محاسبات) shiftUsecase.CreateBulk() ShiftService.CreateBulkAsync()
DTO پاکت نامه‌ای که داده‌ها را جابه‌جا می‌کند (بدون مغز) CreateBulkRequest CreateBulkRequestDto

به‌خاطر داشته باشید:

  • Usecase = Behavior (رفتار)
  • DTO = Data (داده)

۷. توصیه‌ی نهایی

اگر تازه‌کار هستید یا پروژه‌ی کوچکی دارید، ممکن است تفاوت این دو برایتان چندان مهم نباشد. اما در پروژه‌های بزرگ و تیمی، رعایت این تفکیک باعث می‌شود:

  1. کد تمیزتر و خواناتر باشد.
  2. تست‌نویسی آسان‌تر شود.
  3. تغییرات در لایه‌های مختلف (مثل تغییر دیتابیس یا فریم‌ورک) تأثیر کمتری روی بقیه‌ی کد بگذارد.

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

🔗 منابع بیشتر

نظر شما چیست؟ آیا تا به حال با این دو مفهوم در پروژه‌های خود مواجه شده‌اید؟ تجربیات خود را در بخش نظرات با ما به اشتراک بگذارید.

م
مدیر سایت

نویسنده وبلاگ DEVEX

دیدگاه‌ها (0)

ارسال دیدگاه

هنوز دیدگاهی ثبت نشده است. اولین نفر باشید!