معماری CLean چیست؟ راهنمای پیاده‌سازی در یک پروژه واقعی

معماری پاک (Clean Architecture)، یک رویکرد طراحی نرم‌افزار است که بر جداسازی دغدغه‌ها (Separation of Concerns) و کاهش وابستگی‌ها در یک سیستم نرم‌افزاری تأکید دارد. این معماری که توسط رابرت سی. مارتین (عمو باب) معرفی شد، با هدف ایجاد سیستم‌هایی طراحی شده است که مستقل از فریم‌ورک، قابل تست، مستقل از رابط کاربری و مستقل از پایگاه داده باشند. در این مقاله، به بررسی عمیق و پیاده‌سازی عملی معماری پاک در یک پروژه واقعی با استفاده از زبان C# و پلتفرم ASP.NET Core می‌پردازیم.
کینگتو - آموزش برنامه نویسی تخصصصی - دات نت - سی شارپ - بانک اطلاعاتی و امنیت

معماری CLean چیست؟ راهنمای پیاده‌سازی در یک پروژه واقعی

110 بازدید 0 نظر ۱۴۰۴/۰۶/۰۵

چرا معماری پاک؟ مزایای کلیدی

قبل از ورود به جزئیات فنی، درک مزایای این معماری ضروری است. پیاده‌سازی صحیح معماری پاک مزایای قابل توجهی را به همراه دارد:

  • قابلیت نگهداری بالا (High Maintainability): با جدا کردن منطق اصلی کسب‌وکار از جزئیات فنی مانند پایگاه داده و رابط کاربری، تغییر در این جزئیات تأثیر حداقلی بر هسته سیستم خواهد داشت.

  • تست‌پذیری (Testability): منطق کسب‌وکار در لایه‌های درونی و بدون وابستگی به عوامل خارجی قرار دارد، که این امر نوشتن تست‌های واحد (Unit Tests) را بسیار ساده می‌کند.

  • انعطاف‌پذیری و استقلال (Flexibility and Independence): شما می‌توانید به راحتی فریم‌ورک وب، کتابخانه دسترسی به داده یا هر ابزار دیگری را بدون بازنویسی منطق اصلی برنامه، جایگزین کنید.

  • تمرکز بر کسب‌وکار: این معماری توسعه‌دهندگان را مجبور می‌کند تا ابتدا به قوانین و منطق کسب‌وکار (Domain Logic) فکر کنند و سپس به جزئیات پیاده‌سازی بپردازند.

 

ساختار لایه‌ها در معماری پاک

معماری پاک بر اساس یک سری دوایر متحدالمرکز بنا شده است که هر دایره یک لایه از نرم‌افزار را نمایندگی می‌کند. قانون اصلی این معماری، قانون وابستگی (The Dependency Rule) است: وابستگی‌ها همیشه باید به سمت داخل باشند. به عبارت دیگر، کدهای لایه‌های بیرونی به کدهای لایه‌های درونی وابسته هستند، اما لایه‌های درونی هیچ اطلاعی از لایه‌های بیرونی ندارند.

در یک پروژه C#، این لایه‌ها معمولاً به صورت پروژه‌های Class Library مجزا پیاده‌سازی می‌شوند:

  1. Domain (دامنه): داخلی‌ترین و هسته‌ای‌ترین لایه. این لایه شامل موجودیت‌ها (Entities)، اشیاء مقدار (Value Objects) و قوانین اصلی کسب‌وکار است. این پروژه هیچ وابستگی به پروژه‌های دیگر در راهکار (Solution) ندارد.

  2. Application (کاربرد): این لایه شامل منطق خاص برنامه یا همان موارد استفاده (Use Cases) است. این لایه گردش کار برنامه را هماهنگ می‌کند اما منطق اصلی کسب‌وکار را در خود ندارد. این پروژه به لایه Domain وابسته است.

  3. Infrastructure (زیرساخت): این لایه شامل پیاده‌سازی‌های فنی و جزئیات مربوط به ابزارهای خارجی است. مواردی مانند دسترسی به پایگاه داده (با استفاده از Entity Framework Core)، ارسال ایمیل، فراخوانی سرویس‌های خارجی و غیره در این لایه قرار می‌گیرند. این پروژه به لایه Application وابسته است.

  4. Presentation (ارائه): بیرونی‌ترین لایه که با کاربر یا سیستم‌های دیگر در تعامل است. در یک پروژه وب، این لایه شامل کنترلرهای ASP.NET Core Web API، نماهای MVC یا کامپوننت‌های Blazor است. این لایه به لایه Application وابسته است.

 

گام به گام: پیاده‌سازی یک پروژه نمونه

بیایید یک سیستم مدیریت محصول ساده را به عنوان مثال در نظر بگیریم و ساختار آن را با معماری پاک پیاده‌سازی کنیم.

 

 

۱. راه‌اندازی ساختار پروژه

ابتدا یک Solution جدید در ویژوال استودیو ایجاد کرده و پروژه‌های زیر را به آن اضافه کنید:

  • MyProductManager.Domain (Class Library)

  • MyProductManager.Application (Class Library)

  • MyProductManager.Infrastructure (Class Library)

  • MyProductManager.WebAPI (ASP.NET Core Web API)

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

  • Application به Domain ارجاع می‌دهد.

  • Infrastructure به Application ارجاع می‌دهد.

  • WebAPI به Application و Infrastructure ارجاع می‌دهد.

 

۲. لایه Domain: قلب تپنده سیستم

این لایه هسته کسب‌وکار شماست. برای مثال، یک موجودیت Product را تعریف می‌کنیم.

MyProductManager.Domain/Entities/Product.cs

public class Product
{
    public Guid Id { get; private set; }
    public string Name { get; private set; }
    public decimal Price { get; private set; }

    private Product(Guid id, string name, decimal price)
    {
        Id = id;
        Name = name;
        Price = price;
    }

    public static Product Create(string name, decimal price)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("Product name cannot be empty.", nameof(name));
        if (price <= 0)
            throw new ArgumentException("Price must be greater than zero.", nameof(price));

        return new Product(Guid.NewGuid(), name, price);
    }

    public void UpdatePrice(decimal newPrice)
    {
        if (newPrice <= 0)
            throw new ArgumentException("Price must be greater than zero.", nameof(newPrice));
        
        Price = newPrice;
    }
}

توجه کنید که منطق ساخت و اعتبارسنجی اولیه درون خود موجودیت قرار دارد و Setterها private هستند تا از تغییرات نامعتبر وضعیت جلوگیری شود (Encapsulation).

 

۳. لایه Application: تعریف موارد استفاده (Use Cases)

این لایه گردش کارهای برنامه را تعریف می‌کند. در اینجا از الگوی CQRS (Command Query Responsibility Segregation) به کمک کتابخانه محبوب MediatR استفاده می‌کنیم. CQRS منطق خواندن (Query) را از منطق نوشتن (Command) جدا می‌کند.

ابتدا اینترفیس‌های مورد نیاز برای انتزاع لایه زیرساخت را تعریف می‌کنیم.

MyProductManager.Application/Interfaces/IProductRepository.cs

using MyProductManager.Domain.Entities;

public interface IProductRepository
{
    Task<Product?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
    Task AddAsync(Product product, CancellationToken cancellationToken = default);
}

حال یک مورد استفاده برای ایجاد یک محصول جدید تعریف می‌کنیم.

MyProductManager.Application/Products/Commands/CreateProductCommand.cs

using MediatR;

public record CreateProductCommand(string Name, decimal Price) : IRequest<Guid>;

MyProductManager.Application/Products/Commands/CreateProductCommandHandler.cs

using MediatR;
using MyProductManager.Domain.Entities;
using MyProductManager.Application.Interfaces;

public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Guid>
{
    private readonly IProductRepository _productRepository;

    public CreateProductCommandHandler(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public async Task<Guid> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        var product = Product.Create(request.Name, request.Price);

        await _productRepository.AddAsync(product, cancellationToken);

        return product.Id;
    }
}

این Handler هیچ اطلاعی از نحوه ذخیره‌سازی محصول (مثلاً در SQL Server یا MongoDB) ندارد و تنها با اینترفیس IProductRepository کار می‌کند.

 

۴. لایه Infrastructure: پیاده‌سازی جزئیات فنی

این لایه پیاده‌سازی‌های کانکریت (Concrete) برای اینترفیس‌های تعریف شده در لایه Application را فراهم می‌کند. در اینجا، با استفاده از Entity Framework Core، دسترسی به داده را پیاده‌سازی می‌کنیم.

MyProductManager.Infrastructure/Persistence/ApplicationDbContext.cs

using Microsoft.EntityFrameworkCore;
using MyProductManager.Domain.Entities;

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

    public DbSet<Product> Products { get; set; }
}

MyProductManager.Infrastructure/Repositories/ProductRepository.cs

using MyProductManager.Application.Interfaces;
using MyProductManager.Domain.Entities;
using MyProductManager.Infrastructure.Persistence;

public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;

    public ProductRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<Product?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
    {
        return await _context.Products.FindAsync(new object[] { id }, cancellationToken);
    }

    public async Task AddAsync(Product product, CancellationToken cancellationToken = default)
    {
        await _context.Products.AddAsync(product, cancellationToken);
        await _context.SaveChangesAsync(cancellationToken);
    }
}

 

۵. لایه Presentation: نقطه ورود برنامه

این لایه، نقطه ورودی درخواست‌ها است. در پروژه WebAPI، یک کنترلر برای مدیریت محصولات ایجاد می‌کنیم.

MyProductManager.WebAPI/Controllers/ProductsController.cs

using MediatR;
using Microsoft.AspNetCore.Mvc;
using MyProductManager.Application.Products.Commands;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ISender _sender; // MediatR's ISender interface

    public ProductsController(ISender sender)
    {
        _sender = sender;
    }

    [HttpPost]
    public async Task<IActionResult> CreateProduct([FromBody] CreateProductCommand command)
    {
        var productId = await _sender.Send(command);
        return CreatedAtAction(nameof(GetProductById), new { id = productId }, productId);
    }

    // A placeholder for a future GetProductById query endpoint
    [HttpGet("{id}")]
    public IActionResult GetProductById(Guid id)
    {
        // Implementation for getting a product would go here
        return Ok($"Product with id {id} would be retrieved here.");
    }
}

کنترلر بسیار سبک است و منطق خاصی ندارد. تنها وظیفه آن دریافت درخواست، ارسال آن به MediatR و برگرداندن پاسخ مناسب است.

 

۶. اتصال همه چیز به هم: Dependency Injection

در نهایت، در فایل Program.cs پروژه WebAPI، تمام سرویس‌ها و وابستگی‌ها را با استفاده از مکانیزم Dependency Injection (DI) داخلی ASP.NET Core ثبت می‌کنیم.

MyProductManager.WebAPI/Program.cs

using Microsoft.EntityFrameworkCore;
using MyProductManager.Application.Interfaces;
using MyProductManager.Infrastructure.Persistence;
using MyProductManager.Infrastructure.Repositories;
using System.Reflection;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();

// Register MediatR
builder.Services.AddMediatR(cfg => 
    cfg.RegisterServicesFromAssembly(typeof(MyProductManager.Application.AssemblyReference).Assembly));

// Register Infrastructure services
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddScoped<IProductRepository, ProductRepository>();


// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// ... (Swagger UI configuration)

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

// Add a dummy AssemblyReference class in the Application project
// public static class AssemblyReference { }

 

چالش‌ها و ملاحظات

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

  • هزینه نگاشت (Mapping): اغلب نیاز به نگاشت بین موجودیت‌های Domain و مدل‌های داده (DTOs) در لایه Application و Presentation وجود دارد که می‌تواند با استفاده از کتابخانه‌هایی مانند AutoMapper یا Mapster ساده‌تر شود.

  • انضباط تیمی: موفقیت این معماری به شدت به انضباط تیم در رعایت قانون وابستگی و مرزهای بین لایه‌ها بستگی دارد.

 

نتیجه‌گیری

معماری پاک یک سرمایه‌گذاری بلندمدت در کیفیت و پایداری نرم‌افزار است. با جداسازی منطق کسب‌وکار از جزئیات فنی، این معماری به شما اجازه می‌دهد تا برنامه‌هایی بسازید که به راحتی قابل توسعه، تست و نگهداری هستند. اگرچه ممکن است در ابتدا کمی پیچیده به نظر برسد، اما مزایای آن در پروژه‌های بزرگ و پیچیده به وضوح نمایان می‌شود و به تیم‌ها کمک می‌کند تا با اطمینان بیشتری نرم‌افزار خود را در طول زمان تکامل دهند. با استفاده از ابزارهای مدرن .NET مانند MediatR و سیستم تزریق وابستگی قدرتمند آن، پیاده‌سازی این معماری از همیشه ساده‌تر شده است.

 
لینک استاندارد شده: ZjqzUIXElq

0 نظر

    هنوز نظری برای این مقاله ثبت نشده است.
جستجوی مقاله و آموزش
دوره‌ها با تخفیفات ویژه