مرور سریع معماری نرمافزار و الگوهای طراحی
جمعبندی معماریهای رایج (Clean، Microservices، CQRS)، الگوهای طراحی پرکاربرد و مفاهیم DDD/IoC/DI برای مرور شب قبل مصاحبه.
این جزوه مهمترین نکات معماری نرمافزار و الگوهای طراحی را که در مصاحبههای ارشد و میانرده پرتکرارند، در یک صفحه جمع کرده تا در کمترین زمان مرور کنید. نکات ⭐ (طلایی) پرتکرارترین سؤالات مصاحبه هستند — اگر وقت کم دارید، فقط همانها را بخوانید.
فهرست بخشها
- معماریهای رایج پروژه در داتنت0/5
- CQRS (Command Query Responsibility Segregation)0/5
- Cohesion (انسجام)0/5
- Coupling (کوپلینگ / وابستگی بین ماژولها)0/5
- DI Lifetime: Transient در برابر Scoped در برابر Singleton0/5
- Dependency Injection (DI)0/5
- Design Patterns پرکاربرد در داتنت0/5
- Domain Driven Design (DDD)0/5
- Event Sourcing0/5
- Inversion of Control (IoC)0/5
- الگوی Mediator با MediatR0/5
- معماری Microservices0/5
- اصول SOLID (تکمیلی)0/5
- Stateful در برابر Stateless0/4
- System Design در پروژههای .NET0/5
- Use Case در لایه Application0/5
- Event-Driven Architecture (EDA)0/5
- Ocelot API Gateway0/5
- الگوی Repository0/5
- Monolithic در برابر SOA0/4
- الگوی Unit of Work0/5
معماریهای رایج پروژه در داتنت0/5
مرور سریع معماریهای لایهای، Clean، Onion/Hexagonal، Microservices و CQRS و اینکه هرکدام برای چه اندازهای از پروژه مناسباند.
قانون سریع انتخاب معماری بر اساس اندازه پروژه: پروژه کوچک → Layered، پروژه متوسط → Clean یا Onion، پروژه بزرگ → Clean + CQRS، سیستم خیلی بزرگ و توزیعشده → Microservices.
CQRS (Command Query Responsibility Segregation)0/5
تفکیک مسیر نوشتن (Command) از مسیر خواندن (Query) در داتنت، معمولاً با MediatR پیادهسازی میشود.
public record CreateUserCommand(string Name, string Email);
public record GetUserByIdQuery(int Id);
public class CreateUserHandler
{
public Task Handle(CreateUserCommand command)
{
// ذخیره در دیتابیس
}
}
public class GetUserByIdHandler
{
public Task<UserDto> Handle(GetUserByIdQuery query)
{
// خواندن از دیتابیس
}
}
public record CreateUserCommand(string Name) : IRequest<int>;
فکر کردن به اینکه CQRS یعنی همیشه باید پیچیدهتر کرد پروژه را؛ برای پروژههای ساده اصلاً لازم نیست و فقط در پروژههای بزرگ ارزشش مشخص میشود.
Cohesion (انسجام)0/5
میزان مرتبط و متمرکز بودن مسئولیتهای داخل یک کلاس یا ماژول؛ هرچه بالاتر باشد، طراحی بهتر است.
class UserManager
{
void SaveUser() { }
void SendEmail() { }
void GenerateReport() { }
}
class UserRepository
{
void SaveUser() { }
}
class EmailService
{
void SendEmail() { }
}
class ReportService
{
void GenerateReport() { }
}
اگر پرسیدند تفاوت Cohesion و Coupling چیست؟ بگویید Cohesion داخل یک کلاس را میسنجد (باید بالا باشد) و Coupling وابستگی بین کلاسها را میسنجد (باید پایین باشد)؛ این دو مکمل هم هستند.
Coupling (کوپلینگ / وابستگی بین ماژولها)0/5
میزان وابستگی بین کلاسها یا ماژولها؛ هرچه کمتر باشد، کد تمیزتر و قابل تغییرتر است.
class EmailService
{
public void Send() => Console.WriteLine("Send Email");
}
class UserService
{
private EmailService _email = new EmailService();
public void Register()
{
_email.Send();
}
}
interface IMessageService
{
void Send();
}
class EmailService : IMessageService
{
public void Send() => Console.WriteLine("Send Email");
}
class UserService
{
private readonly IMessageService _messageService;
public UserService(IMessageService messageService)
{
_messageService = messageService;
}
public void Register()
{
_messageService.Send();
}
}
services.AddScoped<IMessageService, EmailService>();
DI Lifetime: Transient در برابر Scoped در برابر Singleton0/5
سه نوع اصلی طول عمر (Lifetime) سرویسها در Dependency Injection داتنت و تفاوت رفتار آنها در طول یک درخواست.
builder.Services.AddTransient<IMessageService, MessageService>();
public class ServiceA
{
public ServiceA()
{
Console.WriteLine("Created");
}
}
var s1 = provider.GetService<ServiceA>();
var s2 = provider.GetService<ServiceA>();
// نتیجه: دو بار "Created" چاپ میشود؛ دو شیء مجزا ساخته شدهاند
builder.Services.AddScoped<UserService, UserService>();
// Request 1: Controller + ServiceA + ServiceB همه از همان UserService استفاده میکنند
// Request 2: یک UserService جدید ساخته میشود
builder.Services.AddSingleton<ICacheService, CacheService>();
تزریق یک سرویس Scoped داخل یک سرویس Singleton (مثلاً DbContext داخل یک Singleton) باعث خطای Captive Dependency میشود، چون Singleton فقط یک بار ساخته میشود ولی Scoped باید هر Request عوض شود.
Dependency Injection (DI)0/5
تزریق وابستگیها از بیرون بهجای ساختن آنها داخل کلاس، برای کاهش Tight Coupling و افزایش تستپذیری.
public class UserService
{
private EmailService _emailService = new EmailService();
public void Register()
{
_emailService.Send();
}
}
public class UserService
{
private readonly EmailService _emailService;
public UserService(EmailService emailService)
{
_emailService = emailService;
}
}
// حالا:
var emailService = new EmailService();
var userService = new UserService(emailService);
public interface IEmailService
{
void Send();
}
public class EmailService : IEmailService
{
public void Send()
{
Console.WriteLine("Email Sent");
}
}
public class UserService
{
private readonly IEmailService _emailService;
public UserService(IEmailService emailService)
{
_emailService = emailService;
}
}
// جایگزینی بدون تغییر UserService:
public class SmsService : IEmailService
{
public void Send()
{
Console.WriteLine("SMS Sent");
}
}
// Constructor Injection (بهترین روش)
public UserService(IEmailService emailService)
{
_emailService = emailService;
}
// Property Injection
public IEmailService EmailService { get; set; }
// Method Injection
public void Register(IEmailService emailService)
{
}
builder.Services.AddScoped<IEmailService, EmailService>();
public class UserController : Controller
{
private readonly IEmailService _emailService;
public UserController(IEmailService emailService)
{
_emailService = emailService;
}
}
استفاده از new Something() داخل کلاسهای Business Logic که قرار است تستپذیر و DI-friendly باشند؛ این کار وابستگی سفت ایجاد میکند و باعث میشود کلاس دیگر Mock-پذیر نباشد.
Design Patterns پرکاربرد در داتنت0/5
۱۰ الگوی طراحی رایج در پروژههای ASP.NET Core به همراه هدف، مثال و کد نمونه هرکدام.
// Singleton
public class Logger
{
private static Logger _instance = new Logger();
private Logger() { }
public static Logger Instance => _instance;
}
// Factory
// بدون Factory: var car = new Car();
// با Factory:
VehicleFactory.Create("Car");
// Repository
// بدون Repository: context.Users.Add(user);
// با Repository:
userRepository.Add(user);
// بدون DI
EmailService email = new EmailService();
// با DI
public UserService(IEmailService email)
{
_email = email;
}
ترتیب یادگیری پیشنهادی Design Patternها برای داتنت: Dependency Injection، Repository، Factory، Singleton، Strategy، Builder، Observer، Decorator، Adapter، CQRS. بیشترین استفاده در پروژههای ASP.NET Core معمولاً بین Repository، Factory، Strategy، DI و CQRS است.
interface IPayment
{
void Pay();
}
var user = new UserBuilder()
.SetName("Ali")
.SetAge(25)
.SetEmail("a@test.com")
.Build();
Domain Driven Design (DDD)0/5
طراحی نرمافزار بر اساس منطق واقعی کسبوکار (Domain) بهجای فقط دیتابیس یا UI، با زبان مشترک بین برنامهنویس و کارشناس بیزینس.
استفاده از DDD کامل (Entity، Value Object، Aggregate، Bounded Context، ...) برای یک CRUD ساده؛ این کار فقط زمان توسعه را زیاد میکند بدون فایده واقعی.
Event Sourcing0/5
بهجای ذخیره فقط وضعیت فعلی (State)، تمام اتفاقات (Event) که منجر به آن وضعیت شدهاند ذخیره میشوند و وضعیت فعلی از بازپخش رویدادها به دست میآید.
public record MoneyDeposited(decimal Amount);
public class BankAccount
{
public decimal Balance { get; private set; }
public void Apply(MoneyDeposited e)
{
Balance += e.Amount;
}
}
// بازسازی وضعیت با Replay کردن Eventها
foreach (var e in events)
{
account.Apply(e);
}
// در پایان: Balance = 700
Inversion of Control (IoC)0/5
واگذاری کنترل ساخت و مدیریت objectها از داخل کلاسها به بیرون (Framework/Container)؛ DI یکی از روشهای پیادهسازی IoC است.
class UserService
{
private readonly UserRepository repo = new UserRepository();
}
class UserService
{
private readonly IUserRepository _repo;
public UserService(IUserRepository repo)
{
_repo = repo;
}
}
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<UserService>();
اگر پرسیدند تفاوت IoC و DI چیست؟ بگویید IoC یک اصل کلیتر است (واگذاری کنترل ساخت objectها به بیرون) و DI یکی از روشهای عملی پیادهسازی همان اصل.
الگوی Mediator با MediatR0/5
کتابخانه MediatR در داتنت الگوی Mediator را پیادهسازی میکند تا وابستگی بین کلاسها کم شود و جریان برنامه کنترلپذیرتر باشد.
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
// Request
public record GetUserByIdQuery(int Id) : IRequest<string>;
// Handler
public class GetUserByIdHandler : IRequestHandler<GetUserByIdQuery, string>
{
public Task<string> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
{
return Task.FromResult($"User Id = {request.Id}");
}
}
// Controller
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator) => _mediator = mediator;
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var result = await _mediator.Send(new GetUserByIdQuery(id));
return Ok(result);
}
}
public record CreateUserCommand(string Name) : IRequest<int>;
public class CreateUserHandler : IRequestHandler<CreateUserCommand, int>
{
public Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
return Task.FromResult(1); // Id جدید کاربر
}
}
معماری Microservices0/5
شکستن یک اپلیکیشن بزرگ به چند سرویس کوچک، مستقل و قابل توسعه و Deploy جداگانه.
اشتباه رایج: چند سرویس را به یک دیتابیس مشترک وصل کردن که باعث میشود سیستم دیگر Microservice واقعی نباشد بلکه یک Monolith پنهان با چند API باشد.
اصول SOLID (تکمیلی)0/5
پنج اصل طراحی شیءگرا برای کد تمیز، قابل توسعه و قابل نگهداری؛ نسخه عمیقتر با مثال کد برای هر ۵ حرف.
قانون طلایی SRP: 'هر کلاس فقط یک دلیل برای تغییر داشته باشد'.
interface IPayment
{
void Pay();
}
// بدون تغییر کد اصلی، فقط پیادهسازی جدید اضافه میشود
class CardPayment : IPayment { public void Pay() { } }
class PaypalPayment : IPayment { public void Pay() { } }
ارثبری صرفاً برای استفادهٔ مجدد از کد، بدون رعایت رفتار یکسان با والد، نقض LSP است (مثل Penguin : Bird با متد Fly).
// بد: interface بزرگ
interface IWorker
{
void Work();
void Eat();
}
// خوب: تفکیک به interfaceهای کوچک
interface IWork { void Work(); }
interface IEat { void Eat(); }
// بد: وابستگی مستقیم به پیادهسازی
class Service
{
SqlDatabase db = new SqlDatabase();
}
// خوب: وابستگی به abstraction + تزریق از بیرون
class Service
{
private readonly IDatabase _db;
public Service(IDatabase db)
{
_db = db;
}
}
Stateful در برابر Stateless0/4
Stateful یعنی برنامه اطلاعات کاربر را بین درخواستها به خاطر میسپارد؛ Stateless یعنی هر درخواست کاملاً مستقل است.
System Design در پروژههای .NET0/5
تصمیمگیری دربارهٔ ساختار کلی نرمافزار، لایهبندی و ارتباط اجزا برای مقیاسپذیری و نگهداری بهتر.
// Presentation
[ApiController]
public class ProductController : ControllerBase
{
private readonly IProductService _service;
public ProductController(IProductService service) => _service = service;
}
// Business
public class ProductService : IProductService
{
public async Task<Product> GetById(int id)
{
return await _repository.GetById(id);
}
}
// Data Access
public class ProductRepository
{
private readonly AppDbContext _db;
public ProductRepository(AppDbContext db) => _db = db;
}
// Domain
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
// Application
public class CreateProductCommand
{
public string Name { get; set; }
}
// Infrastructure
public class ProductRepository : IProductRepository
{
}
Use Case در لایه Application0/5
هر Use Case معادل یک سناریوی کامل از کاری است که سیستم برای کاربر انجام میدهد.
نوشتن کوئری یا کدهای دسترسی مستقیم به دیتابیس داخل Use Case، لایهٔ Application را با لایهٔ Data Access قاطی میکند.
Application
|- CreateOrderUseCase
|- CancelOrderUseCase
|- LoginUserUseCase
|- RegisterUserUseCase
|- ChangePasswordUseCase
Event-Driven Architecture (EDA)0/5
سرویسها بهجای فراخوانی مستقیم هم، رویداد منتشر میکنند و سرویسهای علاقهمند به آن رویداد واکنش نشان میدهند.
// تعریف Event
public record OrderCreatedEvent(
Guid OrderId,
string CustomerName,
decimal Amount);
// Publish کردن Event
await bus.Publish(new OrderCreatedEvent(
order.Id,
order.CustomerName,
order.Amount));
// Consumer
public class OrderCreatedConsumer
{
public async Task Consume(OrderCreatedEvent message)
{
Console.WriteLine(
$"Email Sent For {message.OrderId}");
}
}
اگر پرسیدند فرق Command و Event چیست، روی تعداد دریافتکننده تمرکز کن: Command = یک گیرنده و دستوری، Event = چند گیرنده و اعلامی (اتفاقی که افتاده).
Ocelot API Gateway0/5
Ocelot یک API Gateway اختصاصی برای میکروسرویسهای .NET است که مسیر دادن، امنیت و مدیریت درخواستها را یکجا انجام میدهد.
{
"Routes": [
{
"UpstreamPathTemplate": "/users",
"DownstreamPathTemplate": "/api/users",
"DownstreamHostAndPorts": [
{ "Host": "localhost", "Port": 5001 }
],
"DownstreamScheme": "http"
}
]
}
// نصب: dotnet add package Ocelot
// Program.cs
builder.Services.AddOcelot();
var app = builder.Build();
await app.UseOcelot();
الگوی Repository0/5
لایه واسط بین دیتابیس و منطق برنامه برای دسترسی به داده تمیز، قابل تست و مستقل از بقیه کد.
public interface IUserRepository
{
IEnumerable<User> GetAll();
User GetById(int id);
void Add(User user);
void Delete(int id);
}
public class UserRepository : IUserRepository
{
private readonly AppDbContext _context;
public UserRepository(AppDbContext context) => _context = context;
public IEnumerable<User> GetAll() => _context.Users.ToList();
public User GetById(int id) => _context.Users.Find(id);
public void Add(User user)
{
_context.Users.Add(user);
_context.SaveChanges();
}
public void Delete(int id)
{
var user = _context.Users.Find(id);
if (user != null)
{
_context.Users.Remove(user);
_context.SaveChanges();
}
}
}
public class UserService
{
private readonly IUserRepository _repo;
public UserService(IUserRepository repo) => _repo = repo;
public List<User> GetUsers() => _repo.GetAll().ToList();
}
Monolithic در برابر SOA0/4
مقایسه معماری یکپارچه (Monolithic) و معماری سرویسگرا (SOA) از نظر ساختار، مزایا و معایب.
الگوی Unit of Work0/5
الگویی برای مدیریت چند عملیات دیتابیسی بهصورت یک تراکنش واحد، هماهنگ با چند Repository.
فراخوانی SaveChanges جداگانه در هر Repository بهجای یک CompleteAsync مشترک در UnitOfWork، دقیقاً همان مشکل تراکنش ناقص را ایجاد میکند که Unit of Work قرار است حل کند.
public interface IUnitOfWork
{
IProductRepository Products { get; }
IOrderRepository Orders { get; }
Task<int> CompleteAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _context;
public IProductRepository Products { get; }
public IOrderRepository Orders { get; }
public UnitOfWork(AppDbContext context)
{
_context = context;
Products = new ProductRepository(_context);
Orders = new OrderRepository(_context);
}
public async Task<int> CompleteAsync()
{
return await _context.SaveChangesAsync();
}
}
await _unitOfWork.Orders.AddAsync(order);
await _unitOfWork.Products.DecreaseStock(productId);
await _unitOfWork.CompleteAsync(); // همه تغییرات فقط اینجا ذخیره میشوند
