refactor(ban_service): extract _bans_by_country_load_data helper

Break up long function into focused helper. Load data logic separate from aggregation.
This commit is contained in:
2026-05-03 17:00:34 +02:00
parent 5058a50143
commit 2df029f7e8
8 changed files with 458 additions and 321 deletions

View File

@@ -3386,6 +3386,64 @@ When user-supplied URLs are fetched by the backend, validate them before making
- `async validate_blocklist_url(url: AnyHttpUrl) → None`: Async DNS resolution + private IP check.
- Service layer calls `await validate_blocklist_url(url)` before persisting; router catches `ValueError` and returns 400.
### 17.8 Function Complexity Limits
Functions exceeding ~100 lines introduce maintenance burden and hidden bugs. Hard limits:
- **Service functions**: target ≤ 100 lines, absolute max 150 lines.
- **Utility functions**: target ≤ 50 lines, absolute max 80 lines.
- **Router handlers**: target ≤ 40 lines, absolute max 60 lines.
When a function grows beyond its target:
1. **Identify distinct operations** — data loading, transformation, validation, output building.
2. **Extract each operation into a named helper** with a clear responsibility.
3. **Keep helpers at the same level of abstraction** — don't mix low-level I/O with high-level business rules.
Example — refactoring a 250-line function:
```python
# Before: one monolithic function doing everything
async def bans_by_country(socket_path, range_, *, ...):
# 250 lines of mixed validation, DB queries, geo lookups, aggregation, and response building
...
# After: five focused helpers + one orchestrator
async def _load_ban_data(*, source, socket_path, since, origin, ...):
"""Step 1: Query per-IP ban counts from the right source.""" ...
async def _resolve_geo(unique_ips, *, http_session, geo_cache_lookup, ...):
"""Step 2: Resolve geo info from cache or enricher.""" ...
async def _load_companion_rows(*, source, country_code, geo_map, ...):
"""Step 3: Load companion ban rows, optionally filtered by country.""" ...
def _aggregate_by_country(agg_rows, geo_map, source):
"""Step 4: Build {country_code: count} and {cc: name} maps.""" ...
def _build_ban_items(companion_rows, geo_map, source):
"""Step 5: Convert raw rows to DomainDashboardBanItem domain objects.""" ...
async def bans_by_country(socket_path, range_, *, ...):
agg_rows, total, unique_ips = await _load_ban_data(...)
geo_map = await _resolve_geo(unique_ips, ...)
companion_rows, _ = await _load_companion_rows(...)
countries, country_names = _aggregate_by_country(agg_rows, geo_map, source)
bans = _build_ban_items(companion_rows, geo_map, source)
return DomainBansByCountry(...)
```
**Why this works**:
- Each helper is independently testable.
- Failure modes are isolated — a bug in geo resolution doesn't infect aggregation.
- Code review becomes line-based rather than block-based.
- New requirements slot into a specific step rather than being threaded through one long function.
**Traps**:
- Do not introduce new shared state between helpers — keep them pure where possible.
- Avoid premature abstraction — extract only when the function's intent becomes unclear.
- Profile before and after refactoring — decomposition can change performance characteristics.
## 18. Quick Reference — Do / Don't
| Do | Don't |