120 lines
3.6 KiB
Markdown
120 lines
3.6 KiB
Markdown
# ANTI-PATTERN: Fat Views (Business Logic in Views)
|
|
|
|
## KONTEXT
|
|
Django-Entwicklung, besonders bei wachsenden Projekten mit komplexer Domain-Logik.
|
|
|
|
## WAS IST PASSIERT?
|
|
```python
|
|
# 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
|
|
```python
|
|
# 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)
|
|
```python
|
|
# 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
|
|
```python
|
|
# 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?
|