Vertikale Slices über N-Tier-Architekturen hinaus umarmen
James Reed
Infrastructure Engineer · Leapcell

Die monolithische Teilung: Anwendungsstrukturen neu überdenken
In der sich ständig weiterentwickelnden Landschaft der Softwareentwicklung hat die Art und Weise, wie wir unsere Anwendungen strukturieren, tiefgreifende Auswirkungen auf ihre Wartbarkeit, Skalierbarkeit und die Entwicklererfahrung. Seit Jahrzehnten ist die N-Tier-Architektur mit ihren klaren Ebenen für Präsentation, Geschäftslogik und Datenzugriff der De-facto-Standard. Während sie eine klare Trennung von Belangen bietet, führt dieser Ansatz oft zu horizontaler Kopplung – Änderungen in einem Teil einer Ebene können sich durch die gesamte Anwendung ziehen, was die Entwicklung umständlich und die Bereitstellung riskant macht, insbesondere wenn Anwendungen komplex werden. Dieser Kampf zwingt uns zu der Frage, ob das traditionelle N-Tier-Modell den Bedürfnissen heutiger agiler Entwicklungsumgebungen wirklich gerecht wird. Dieser Artikel stellt ein alternatives Paradigma vor, die "Vertical Slice Architecture", als überzeugende Lösung für den Aufbau kohärenterer und überschaubarerer Anwendungen, insbesondere im Kontext von ASP.NET Core und FastAPI.
Dekodierung von Vertical Slices und ihren Kernprinzipien
Bevor wir uns den praktischen Aspekten widmen, wollen wir die Schlüsselkonzepte klären, die der Vertical Slice-Architektur zugrunde liegen.
N-Tier-Architektur: Dieses ist ein traditionelles Architekturmuster, bei dem eine Anwendung in logische Ebenen unterteilt wird, wie z. B. Präsentation, Geschäftslogik (Service-Schicht) und Datenzugriff (Repository-Schicht). Jede Ebene hat eine spezifische Verantwortung, und die Kommunikation fließt typischerweise unidirektional zwischen ihnen.
Vertical Slice: Im Gegensatz zur horizontalen Aufteilung von Belangen in N-Tier kapselt ein Vertical Slice alle Komponenten, die zur Bereitstellung eines einzelnen Features oder Anwendungsfalls End-to-End erforderlich sind. Dies umfasst seinen API-Endpunkt, die Geschäftslogik, den Datenzugriff und sogar seine spezifischen UI-Komponenten (obwohl sich dieser Artikel hauptsächlich auf das Backend konzentriert). Jeder Slice ist unabhängig und kann oft isoliert entwickelt, getestet und bereitgestellt werden.
Domain-Driven Design (DDD): Obwohl nicht zwingend erforderlich, passt Vertical Slicing gut zu DDD-Prinzipien, bei denen Features rund um Geschäftsbereiche organisiert werden. Dies führt zu kohärenten Slices, die spezifische Domänenfähigkeiten darstellen.
Clean Architecture / Hexagonal Architecture: Diese Architekturen betonen die Trennung von Belangen basierend auf ihrer Entfernung zur Kern-Geschäftslogik. Vertical Slices können als praktische Implementierungsstrategie innerhalb dieser Architekturstile betrachtet werden, die es jedem Slice ermöglicht, diese Prinzipien unabhängig einzuhalten.
Das Kernprinzip des Vertical Slicing ist die Priorisierung von Kohäsion pro Feature über Kohäsion pro technischem Belang. Anstatt eines einzigen UserService, der alle benutzerspezifischen Operationen abwickelt, könnten Sie separate Slices für "Benutzer erstellen", "Benutzerdetails abrufen" und "Benutzerprofil aktualisieren" haben. Jeder Slice ist eine Miniatur, in sich geschlossene Anwendung, die die Kopplung zwischen nicht verwandten Features drastisch reduziert.
Implementierung von Vertical Slices in ASP.NET Core
Lassen Sie uns Vertical Slicing mit einer einfachen ASP.NET Core-Anwendung zur Verwaltung von Produkten veranschaulichen. Anstelle eines ProductService und ProductRepository erstellen wir separate Slices für CreateProduct, GetProductById und ListProducts. Wir verwenden MediatR zur Behandlung von Anfragen und Befehlen, was gut zum Vertical Slice-Muster passt, indem Anfragen an spezifische Handler weitergeleitet werden.
Installieren Sie zuerst MediatR:
dotnet add package MediatR dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Betrachten Sie ein CreateProduct-Feature. Sein gesamter Umfang, vom Endpunkt bis zur Datenbank, befindet sich in einem einzigen, dedizierten Ordner.
// Features/Products/CreateProduct.cs using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Threading; using System.Threading.Tasks; namespace MyWebApp.Features.Products { public static class CreateProduct { // 1. Command (Input) public class Command : IRequest<Response> { public string Name { get; set; } public decimal Price { get; set; } } // 2. Response (Output) public class Response { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } // 3. Handler (Business Logic & Data Access) public class Handler : IRequestHandler<Command, Response> { private readonly ProductContext _context; public Handler(ProductContext context) { _context = context; } public async Task<Response> Handle(Command request, CancellationToken cancellationToken) { var product = new Product { Name = request.Name, Price = request.Price }; _context.Products.Add(product); await _context.SaveChangesAsync(cancellationToken); return new Response { Id = product.Id, Name = product.Name, Price = product.Price }; } } // 4. API Endpoint (Controller) [ApiController] [Route("api/products")] public class ProductsController : ControllerBase { private readonly IMediator _mediator; public ProductsController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task<ActionResult<Response>> Post(Command command) { var response = await _mediator.Send(command); return CreatedAtAction(nameof(Post), new { id = response.Id }, response); } } } // Shared: A simple Entity Framework Core DbContext and Product entity public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } public class ProductContext : DbContext { public DbSet<Product> Products { get; set; } public ProductContext(DbContextOptions<ProductContext> options) : base(options) { } } }
In Program.cs oder Startup.cs MediatR und Ihren DbContext konfigurieren:
// Program.cs using Microsoft.EntityFrameworkCore; using MediatR; using MyWebApp.Features.Products; // Important for reflection var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext<ProductContext>(options => options.UseInMemoryDatabase("ProductDb")); // Using in-memory for simplicity builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); // Scan for MediatR handlers var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
Beachten Sie, wie CreateProduct.cs fast alles enthält, was mit diesem spezifischen Feature zu tun hat. Der Controller fungiert als dünne Fassade, die den Befehl an die MediatR-Pipeline weiterleitet.
Implementierung von Vertical Slices in FastAPI
FastAPI eignet sich mit seinem starken Fokus auf Pydantic-Modelle und Dependency Injection ebenfalls hervorragend für Vertical Slicing. Wir können eine ähnliche Struktur erreichen, indem wir unsere Routen, Modelle und Logik für jedes Feature in einem dedizierten Modul oder Verzeichnis definieren.
# app/features/products/create_product.py from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from typing import Optional # Angenommen, eine einfache In-Memory-"Datenbank" zur Veranschaulichung # In einer echten App wäre dies ein ORM wie SQLAlchemy, das mit einer Datenbank interagiert class ProductDB: def __init__(self): self.products = [] self.next_id = 1 def create_product(self, name: str, price: float): product_data = {"id": self.next_id, "name": name, "price": price} self.products.append(product_data) self.next_id += 1 return product_data def get_product(self, product_id: int): for product in self.products: if product["id"] == product_id: return product return None # Dependency, um eine Datenbankinstanz bereitzustellen (kann durch eine echte DB-Sitzung ersetzt werden) def get_db(): return ProductDB() # 1. Request Body Model class CreateProductRequest(BaseModel): name: str price: float # 2. Response Model class ProductResponse(BaseModel): id: int name: str price: float # 3. Router (API Endpoint & Business Logic) router = APIRouter(prefix="/products", tags=["products"]) @router.post("/", response_model=ProductResponse, status_code=status.HTTP_201_CREATED) async def create_product_endpoint( request: CreateProductRequest, db: ProductDB = Depends(get_db) # Inject our "database" ): """ Erstellt ein neues Produkt. """ created_product_data = db.create_product(request.name, request.price) return ProductResponse(**created_product_data) @router.get("/{product_id}", response_model=ProductResponse) async def get_product_endpoint( product_id: int, db: ProductDB = Depends(get_db) ): """ Ruft ein Produkt anhand seiner ID ab. """ product = db.get_product(product_id) if product is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") return ProductResponse(**product)
In app/main.py:
# app/main.py from fastapi import FastAPI from .features.products import create_product app = FastAPI(title="Vertical Slice Product API") # Include the feature-specific router app.include_router(create_product.router) @app.get("/") async def root(): return {"message": "Welcome to the Vertical Slice API!"}
Hier kapselt create_product.py seine eigenen Modelle, den Router (der als Endpunkt fungiert und die Geschäftslogik verarbeitet) und sogar seine spezifische "Datenbank"-Interaktion. Dependency Injection (Depends(get_db)) innerhalb von FastAPI stellt sicher, dass jede Funktion ihre spezifischen Abhängigkeiten deklarieren kann, ohne andere zu beeinträchtigen.
Wann Vertical Slices in Betracht gezogen werden sollten
Die Vertical Slice-Architektur glänzt in mehreren Szenarien:
- Wachsende Monolithen: Wenn eine N-Tier-Anwendung schwierig zu navigieren und zu ändern wird, kann Vertical Slicing helfen, neue Features zu isolieren und bestehende schrittweise in Slices zu refaktorieren.
 - Migration zu Microservices: Sie kann als ausgezeichneter Sprungbrett für die Extraktion in einen separaten Dienst dienen.
 - Feature-gesteuerte Entwicklung: Teams, die sich um Features und nicht um technische Schichten organisieren, werden feststellen, dass dieses Muster gut zu ihrem Workflow passt.
 - Kleinere Teams: Durch die Reduzierung der kognitiven Belastung und die Begrenzung des Explosionsradius von Änderungen können kleine Teams Features unabhängiger entwickeln und bereitstellen.
 - Stark iterative Produkte: Wenn Features häufig hinzugefügt, geändert oder entfernt werden, machen die durch Vertical Slices bereitgestellte Isolation diese Vorgänge sicherer und schneller.
 
Es ist jedoch keine Wunderwaffe. Für sehr kleine, einfache Anwendungen mit minimaler Komplexität ist der Aufwand für die Einrichtung und Einhaltung von Vertical Slices möglicherweise nicht erforderlich. Gemeinsame Kernlogik oder übergreifende Belange (wie Authentifizierung, Protokollierung) erfordern immer noch ein durchdachtes Design, oft unter Verwendung von Middleware oder Pipelines, die alle Slices betreffen.
Kohäsion und Agilität vereinen
Die Vertical Slice-Architektur fordert die langjährige Tradition des N-Tier-Designs heraus, indem sie unseren Fokus von der technischen Schichtung auf die Feature-zentrierte Kohäsion verlagert. Indem wir alle Komponenten, die zu einem bestimmten Anwendungsfall gehören, in einer einzigen Einheit zusammenführen, können Entwickler mehr Autonomie erreichen, versehentliche Kopplungen reduzieren und die Entwicklung komplexer Anwendungen in Frameworks wie ASP.NET Core und FastAPI beschleunigen. Nutzen Sie Vertical Slices, um Anwendungen zu erstellen, die nicht nur funktional, sondern auch unglaublich agil und wartbar sind, und ebnen Sie den Weg für robustere und skalierbarere Softwaresysteme.