2026-02-04 16:49:53 +01:00

3.6 KiB

ANTI-PATTERN: Fat Views (Business Logic in Views)

KONTEXT

Django-Entwicklung, besonders bei wachsenden Projekten mit komplexer Domain-Logik.

WAS IST PASSIERT?

# SCHLECHT: Alle Logik in der View
@api_view(['POST'])
def create_match_schedule(request, scenario_id):
    scenario = Scenario.objects.get(id=scenario_id)
    
    # 50+ Zeilen Business Logic direkt in der View
    teams = scenario.teams.all()
    venues = Venue.objects.filter(team__in=teams)
    
    # Komplexe Validierung
    if len(teams) < 2:
        return Response({'error': 'Too few teams'}, status=400)
    if not all(t.venue for t in teams):
        return Response({'error': 'Missing venues'}, status=400)
    
    # Scheduling-Logik
    matches = []
    for home in teams:
        for away in teams:
            if home != away:
                # 20 Zeilen Match-Erstellung...
                match = Match.objects.create(...)
                matches.append(match)
    
    # Email-Benachrichtigung
    for manager in scenario.season.league.managers.all():
        send_mail(...)
    
    return Response({'created': len(matches)})

WARUM WAR ES SCHLECHT?

  • Nicht testbar: View-Tests brauchen HTTP-Request-Mocking
  • Nicht wiederverwendbar: Logik nur über HTTP erreichbar
  • Schwer lesbar: 200+ Zeilen Views
  • Verstößt gegen SRP: View macht Validierung, DB, Email, Response

DIE BESSERE ALTERNATIVE

Fat Models

# models.py
class Scenario(models.Model):
    def can_create_schedule(self) -> tuple[bool, str]:
        """Validate if schedule creation is possible."""
        if self.teams.count() < 2:
            return False, "Too few teams"
        if self.teams.filter(venue__isnull=True).exists():
            return False, "Missing venues"
        return True, ""
    
    def create_round_robin_matches(self) -> list['Match']:
        """Create all matches for round-robin tournament."""
        matches = []
        teams = list(self.teams.all())
        for i, home in enumerate(teams):
            for away in teams[i+1:]:
                matches.append(Match(
                    scenario=self,
                    home_team=home,
                    away_team=away,
                ))
        return Match.objects.bulk_create(matches)

Service Layer (für komplexe Workflows)

# services/scheduling.py
class SchedulingService:
    def __init__(self, scenario: Scenario):
        self.scenario = scenario
    
    def create_schedule(self) -> list[Match]:
        valid, error = self.scenario.can_create_schedule()
        if not valid:
            raise ValidationError(error)
        
        matches = self.scenario.create_round_robin_matches()
        self._notify_managers(matches)
        return matches
    
    def _notify_managers(self, matches: list[Match]):
        # Email-Logik isoliert
        ...

Thin View

# views.py
@api_view(['POST'])
def create_match_schedule(request, scenario_id):
    scenario = get_object_or_404(Scenario, id=scenario_id)
    
    try:
        service = SchedulingService(scenario)
        matches = service.create_schedule()
    except ValidationError as e:
        return Response({'error': str(e)}, status=400)
    
    return Response({'created': len(matches)})

ERKENNUNGSREGELN

  • View-Funktion > 30 Zeilen → Refactoring-Kandidat
  • Mehrere objects.create() in einer View → Service extrahieren
  • Gleiche Logik in mehreren Views → Ins Model oder Helper

CHECKLISTE

  • Views nur für Request/Response Handling?
  • Business Logic im Model oder Service?
  • Validierung im Model (clean()) oder Serializer?
  • Side Effects (Email, Logging) in separater Klasse?