.NET Core: Vertical Slice Architecture

Vertical Slice Architecture, bir uygulamayı, bağımsız çalışabilir “dikey” kesitlere ayırma pratiğidir. Her bir dilim, kullanıcı arayüzünden veritabanı erişimine kadar tüm katmanları içerir ve bir özellik veya işlevsellik parçasını tamamlar.

Murat Dinç
Atlastek Labs
5 min readNov 23, 2023

--

Yazılım mimarisi, uygulamalarımızın nasıl geliştirileceğini, ölçekleneceğini ve bakımının nasıl yapılacağını belirleyen temel yapı taşlarından biridir. Geleneksel katmanlı mimarilerin yerini alarak popülerlik kazanan Vertical Slice Architecture, modern yazılım geliştirmenin ön saflarında yer alıyor. Bu mimari yaklaşım, kodun mantıklı kesitler halinde düzenlenmesi üzerine kuruludur, böylece her bir “dilim” bir kullanıcı hikayesi veya özelliğin tamamını uçtan uca kapsar.

Vertical Slice Architecture, bir uygulamayı, bağımsız çalışabilir “dikey” kesitlere ayırma pratiğidir. Her bir dilim, kullanıcı arayüzünden veritabanı erişimine kadar tüm katmanları içerir ve bir özellik veya işlevsellik parçasını tamamlar. Bu yaklaşım, takımların paralel olarak çalışabilmesini, daha hızlı teslimatları ve kod tabanının daha iyi anlaşılmasını sağlar.

Layered Architecture Sorunları

Katmanlı mimariler, yazılım sistemini katmanlara veya seviyelere ayırır. Genellikle her bir katman çözümünüzdeki bir projeyi temsil eder. Popüler uygulamalar arasında N-tier Architecture veya Clean Architecture bulunmaktadır.

Katmanlı mimariler, çeşitli bileşenlerin sorumluluklarını ayırmaya odaklanır. Bu, projeyi anlamayı ve bakımını yapmayı kolaylaştırır. Yapılandırılmış yazılım tasarımının birçok faydası vardır, örneğin bakım kolaylığı, esneklik ve loose coupling.

Ancak, katmanlı mimariler sistem üzerinde bazı kısıtlamalar veya katı kurallar da getirir. Katmanlar arasındaki bağımlılıkların yönü önceden belirlenmiştir.

Örneğin, Clean Architecture:

  • Domain katmanının hiçbir bağımlılığı olmamalıdır.
  • Application katmanı, Domain’e referans verebilir.
  • Infrastructure hem Application hem de Domain katmanlarına referans verebilir.
  • Presentation katmanı da hem Application hem de Domain katmanlarına referans verebilir.

Bir katman içinde yüksek bağımlılık, katmanlar arasında ise düşük bağımlılığa sahip olursunuz. Bu, katmanlı mimarilerin kötü olduğu anlamına gelmez. Ancak, katmanlar arası çok sayıda soyutlama olacağı anlamına gelir. Ve daha fazla soyutlama, bakımı yapılacak daha fazla bileşen olduğu için artan karmaşıklık anlamına gelir.

Vertical Slice Architecture’ın Avantajları

  • Esneklik: Yeni özellikler eklemek veya mevcutları değiştirmek kolaydır, çünkü her dilim kendi içinde tamamlanmıştır ve diğer dilimlerle minimum bağlantıya sahiptir.
  • Anlaşılabilirlik: Geliştiriciler, uygulamanın küçük bir bölümüne odaklanabilir ve böylece karmaşıklığı azaltabilir.
  • Test Edilebilirlik: Her dilimi bağımsız olarak test etmek mümkündür, bu da otomatik testlerin ve sürekli entegrasyonun yolunu açar.
  • Ölçeklenebilirlik: Sistem, gerektiğinde sadece gerekli olan dilimleri ölçeklendirerek kaynakları daha verimli kullanabilir.
  • Paralel Geliştirme: Farklı takımlar veya geliştiriciler, birbirinden bağımsız dilimler üzerinde çalışabilir, bu da projenin hızını artırır.

Örnek 🚀

Diyelim ki bir e-ticaret uygulaması geliştiriyorsunuz. Vertical Slice Architecture uygulayarak projeyi nasıl yapabiliriz?

Örnek projeye GitHub üzerinden erişebilirsiniz

Projeyi docker compose up -d komutu ile çalıştırabilirsiniz.

Proje Yapısı

Projede 3 adet işlevsel modül bulunmaktadır.

  • Customer
  • Product
  • Order

Bu modüller, mimaride dikey dilimler olarak ayrılmıştır ve tüm işlevleri bu dikey dilimler içerisinde yer almaktadır.

Proje genel yapısında Api ve Application katmanları bulunmaktadır.

API projemizi incelediğinizde, bir Controller’ın bulunmadığını fark edeceksiniz. Bu durum, uyguladığımız dikey mimari yaklaşımından kaynaklanmaktadır. Yazının başında bahsettiğim dikey dilim örneğine dönerseniz, her bir Feature bir dilimi temsil eder. Product özelliğini inceleyerek mimariyi daha detaylı bir şekilde ele alalım.

Product ile ilgili bütün işlemleri bu klasör altında topladım. Her bir class bir dilimi temsil etmektedir.
using FluentValidation;
using MediatR;
using Microsoft.AspNetCore.Mvc;

namespace Application.Features.Product;

[ApiExplorerSettings(GroupName = "Product")]
public class CreateProductController : BaseController
{
[HttpPost("/api/products")]
public async Task<ActionResult<int>> Create(CreateProductCommand command)
{
return await Mediator.Send(command);
}
}

public record CreateProductCommand : IRequest<int>
{
public string Title { get; init; }
public decimal Price { get; init; }
}

public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
public CreateProductCommandValidator()
{
RuleFor(x => x.Title)
.MaximumLength(200)
.NotEmpty();

RuleFor(x => x.Price)
.GreaterThan(0);
}
}

internal sealed class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, int>
{
private readonly ApplicationDbContext _context;

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

public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
var entity = new Entities.Product
{
Title = request.Title,
Price = request.Price
};

_context.Product.Add(entity);

await _context.SaveChangesAsync(cancellationToken);

return entity.Id;
}
}

Örneğimizde, ürün oluşturma işlevini gerçekleştiren CreateProduct dilimine odaklanacağız. Bu dilim, bir HTTP POST isteği ile yeni bir ürün eklemek için gerekli tüm bileşenleri içerir.

İlk olarak, CreateProductController sınıfımız API endpoint'imizi tanımlar. BaseController sınıfından türeyen bu controller, /api/products yolu üzerinden gelen POST isteklerini karşılar. MediatR kütüphanesini kullanarak, gelen CreateProductCommand komutunu işleyecektir.

[HttpPost("/api/products")]
public async Task<ActionResult<int>> Create(CreateProductCommand command)
{
return await Mediator.Send(command);
}

ApiExplorerSettings Attribute ile çıkan endpoint’leri gruplayarak Swagger üzerinde grupladığım yöntemde gösterilmesini sağlıyorum.

CreateProductCommand sınıfı, bir ürün oluşturmak için gerekli olan verileri içerir. IRequest<int> interface’ini uygular ve MediatR tarafından bir komut olarak işlenir. Bu komut, ürünün başlığını ve fiyatını temsil eden özelliklere sahiptir.

public record CreateProductCommand : IRequest<int>
{
public string Title { get; init; }
public decimal Price { get; init; }
}

Her komutun doğru bir şekilde validasyonu önemlidir. CreateProductCommandValidator sınıfı, FluentValidation kütüphanesi kullanılarak CreateProductCommand için doğrulama kurallarını tanımlar. Bu doğrulayıcı, ürün başlığının boş olmamasını ve maksimum uzunluğun 200 karakter ile sınırlı olmasını; fiyatın ise sıfırdan büyük olmasını sağlar.

public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
public CreateProductCommandValidator()
{
RuleFor(x => x.Title)
.MaximumLength(200)
.NotEmpty();

RuleFor(x => x.Price)
.GreaterThan(0);
}
}

Son olarak, CreateProductCommandHandler sınıfı, komutun işlenmesi sırasında çağrılır. IRequestHandler<CreateProductCommand, int> arayüzünü uygular ve bir ApplicationDbContext üzerinden yeni bir ürün oluşturup veritabanına kaydeder.

internal sealed class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, int>
{
public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
var entity = new Entities.Product
{
Title = request.Title,
Price = request.Price
};

_context.Product.Add(entity);

await _context.SaveChangesAsync(cancellationToken);

return entity.Id;
}
}

Her bir dilim, kendi içerisinde kapalı ve bağımsız bir işlevsel bütün olarak ele alınır, bu da bakım ve geliştirme süreçlerini kolaylaştırırken, projenin genel kalitesini de artırır.

Makaleyi faydalı bulduysanız takip ederek destek olabilirsiniz 🙏

Bir sonraki yazıda görüşmek üzere 😊

--

--