All Notes

Strategy Pattern für Algorithmen in SAP CAP

Design PatternCAPTypeScriptAlgorithms

Strategy Pattern in SAP CAP

Das Strategy Pattern kapselt Algorithmen als austauschbare Objekte. In SAP CAP ist es ideal für verschiedene Berechnungslogiken oder Generierungsstrategien.

Problem

Unterschiedliche Algorithmen werden oft mit Conditional Logic implementiert:

// ❌ Anti-Pattern: If/Else-Kaskaden
export class GenerateTimeEntriesCommand {
  async execute(req: Request, params: any) {
    if (params.period === 'monthly') {
      // 50 Zeilen Monats-Logik
      const startDate = `${params.year}-${params.month}-01`
      const endDate = this.getLastDayOfMonth(params.year, params.month)
      const workingDays = this.getWorkingDays(startDate, endDate)
 
      for (const day of workingDays) {
        // Feiertage checken
        // Wochenenden überspringen
        // Entry erstellen
      }
    } else if (params.period === 'yearly') {
      // 80 Zeilen Jahres-Logik
      for (let month = 1; month <= 12; month++) {
        const startDate = `${params.year}-${month}-01`
        // ...mehr Logik
      }
    } else if (params.period === 'quarterly') {
      // 60 Zeilen Quartals-Logik
      // ...
    }
  }
}

Probleme:

  • ❌ God Class (200+ Zeilen in einer Methode)
  • ❌ Schwer testbar (alle Szenarien in einem Test)
  • ❌ Keine Wiederverwendung einzelner Algorithmen
  • ❌ Violation von Open/Closed Principle

Lösung: Strategy Pattern

Strategy Interface

export interface GenerationStrategy {
  generate(tx: Transaction, params: GenerationParams, holidayMap: Map<string, string>): Promise<TimeEntry[]>
}
 
export interface GenerationParams {
  user_ID: string
  year: number
  month?: number
  quarter?: number
  stateCode?: string
}

Konkrete Strategien

MonthlyGenerationStrategy

export class MonthlyGenerationStrategy implements GenerationStrategy {
  constructor(
    private dateUtils: DateUtils,
    private factory: TimeEntryFactory
  ) {}
 
  async generate(tx: Transaction, params: GenerationParams, holidayMap: Map<string, string>): Promise<TimeEntry[]> {
    const { user_ID, year, month } = params
 
    // 1. Monatsgrenzen berechnen
    const startDate = new Date(year, month! - 1, 1)
    const endDate = this.dateUtils.getLastDayOfMonth(year, month!)
 
    // 2. Alle Tage im Monat
    const entries: TimeEntry[] = []
    let currentDate = new Date(startDate)
 
    while (currentDate <= endDate) {
      const dateStr = this.dateUtils.toISODate(currentDate)
 
      // 3. Wochenende überspringen
      if (this.dateUtils.isWeekend(currentDate)) {
        currentDate.setDate(currentDate.getDate() + 1)
        continue
      }
 
      // 4. Feiertag?
      const entryType = holidayMap.has(dateStr) ? 'H' : 'W'
 
      // 5. Entry erstellen
      entries.push(this.factory.createGenerated(user_ID, dateStr, entryType))
 
      currentDate.setDate(currentDate.getDate() + 1)
    }
 
    return entries
  }
}

YearlyGenerationStrategy

export class YearlyGenerationStrategy implements GenerationStrategy {
  constructor(private monthlyStrategy: MonthlyGenerationStrategy) {}
 
  async generate(tx: Transaction, params: GenerationParams, holidayMap: Map<string, string>): Promise<TimeEntry[]> {
    const { user_ID, year } = params
    const allEntries: TimeEntry[] = []
 
    // Strategie: Wiederverwendung der MonthlyStrategy
    for (let month = 1; month <= 12; month++) {
      const monthlyEntries = await this.monthlyStrategy.generate(tx, { ...params, month }, holidayMap)
 
      allEntries.push(...monthlyEntries)
    }
 
    return allEntries
  }
}

Context (Command) nutzt Strategy

export class GenerateTimeEntriesCommand {
  constructor(
    private strategies: Map<string, GenerationStrategy>,
    private holidayService: HolidayService,
    private validator: GenerationValidator
  ) {}
 
  async execute(req: Request, params: GenerationParams): Promise<TimeEntry[]> {
    // 1. Strategy auswählen
    const period = params.month ? 'monthly' : 'yearly'
    const strategy = this.strategies.get(period)
 
    if (!strategy) {
      throw new Error(`Unknown generation period: ${period}`)
    }
 
    // 2. Feiertage laden
    const holidayMap = await this.holidayService.getHolidays(params.year, params.stateCode)
 
    // 3. Strategy ausführen
    const entries = await strategy.generate(req, params, holidayMap)
 
    // 4. Validieren
    await this.validator.validateGenerated(entries)
 
    return entries
  }
}

Vorteile

1. Open/Closed Principle

Neue Strategien ohne Änderung bestehenden Codes:

// Neue Strategy hinzufügen
export class QuarterlyGenerationStrategy implements GenerationStrategy {
  async generate(tx, params, holidayMap): Promise<TimeEntry[]> {
    // Quartals-Logik
    const startMonth = (params.quarter! - 1) * 3 + 1
    const endMonth = startMonth + 2
 
    // ...
  }
}
 
// Im Container registrieren
container.strategies.set('quarterly', new QuarterlyGenerationStrategy(dateUtils, factory))

2. Single Responsibility

Jede Strategy hat eine Verantwortung:

MonthlyGenerationStrategy  → Generiert 1 Monat
YearlyGenerationStrategy   → Generiert 12 Monate (nutzt Monthly)
QuarterlyGenerationStrategy → Generiert 3 Monate
CustomRangeStrategy        → Generiert beliebigen Zeitraum

3. Komposition statt Vererbung

// Yearly Strategy nutzt Monthly Strategy
export class YearlyGenerationStrategy {
  constructor(private monthlyStrategy: MonthlyGenerationStrategy) {}
 
  async generate(...) {
    // Wiederverwendung statt Duplikation
    for (let month = 1; month <= 12; month++) {
      const entries = await this.monthlyStrategy.generate(...);
      allEntries.push(...entries);
    }
  }
}

4. Einfaches Testing

describe('MonthlyGenerationStrategy', () => {
  it('should skip weekends', async () => {
    const strategy = new MonthlyGenerationStrategy(dateUtils, factory)
 
    const entries = await strategy.generate(
      tx,
      {
        user_ID: 'user-1',
        year: 2025,
        month: 1,
      },
      new Map()
    )
 
    // Prüfe: Keine Samstage/Sonntage
    const weekendEntries = entries.filter((e) => dateUtils.isWeekend(new Date(e.workDate)))
 
    expect(weekendEntries).toHaveLength(0)
  })
 
  it('should mark holidays correctly', async () => {
    const holidays = new Map([['2025-01-01', 'Neujahr']])
 
    const entries = await strategy.generate(tx, params, holidays)
 
    const neujahr = entries.find((e) => e.workDate === '2025-01-01')
    expect(neujahr?.entryType_code).toBe('H')
  })
})

Implementierung in CAPture Time

Strategy-Hierarchie

srv/track-service/handler/strategies/
├── MonthlyGenerationStrategy.ts
├── YearlyGenerationStrategy.ts
└── index.ts  # Barrel Export

ServiceContainer Integration

class ServiceContainer {
  buildStrategies(): void {
    const dateUtils = this.getService<DateUtils>('dateUtils')
    const factory = this.getFactory<TimeEntryFactory>('timeEntry')
 
    // Monthly Strategy
    const monthlyStrategy = new MonthlyGenerationStrategy(dateUtils, factory)
    this.strategies.set('monthly', monthlyStrategy)
 
    // Yearly Strategy (nutzt Monthly)
    const yearlyStrategy = new YearlyGenerationStrategy(monthlyStrategy)
    this.strategies.set('yearly', yearlyStrategy)
  }
 
  getStrategy<T extends GenerationStrategy>(name: string): T {
    if (!this.strategies.has(name)) {
      throw new Error(`Strategy '${name}' not found`)
    }
    return this.strategies.get(name) as T
  }
}

Nutzung in Command

export class GenerateMonthlyCommand {
  constructor(
    private strategy: MonthlyGenerationStrategy,
    private repo: TimeEntryRepository,
    private holidayService: HolidayService
  ) {}
 
  async execute(req: Request, params: GenerationParams): Promise<Result> {
    // 1. Feiertage laden
    const holidays = await this.holidayService.getHolidays(params.year, params.stateCode)
 
    // 2. Strategy ausführen
    const newEntries = await this.strategy.generate(req, params, holidays)
 
    // 3. Persistieren
    for (const entry of newEntries) {
      await this.repo.create(req, entry)
    }
 
    return { created: newEntries.length }
  }
}

Best Practices

1. Strategy Selection Pattern

class StrategySelector {
  select(params: GenerationParams): string {
    if (params.month) return 'monthly'
    if (params.quarter) return 'quarterly'
    return 'yearly'
  }
}
 
// Im Command
const strategyName = this.selector.select(params)
const strategy = this.container.getStrategy(strategyName)

2. Default Strategy

class GenerationCommand {
  private defaultStrategy: GenerationStrategy;
 
  constructor(strategies: Map<string, GenerationStrategy>) {
    this.defaultStrategy = strategies.get('monthly')!;
  }
 
  async execute(params: GenerationParams) {
    const strategy = this.selectStrategy(params) ?? this.defaultStrategy;
    return await strategy.generate(...);
  }
}

3. Strategy Composition

// Decorator für Logging
class LoggingGenerationStrategy implements GenerationStrategy {
  constructor(private inner: GenerationStrategy) {}
 
  async generate(tx, params, holidays): Promise<TimeEntry[]> {
    logger.info('Generation started', params)
 
    const entries = await this.inner.generate(tx, params, holidays)
 
    logger.info(`Generated ${entries.length} entries`)
    return entries
  }
}
 
// Wrap Strategy
const strategy = new LoggingGenerationStrategy(new MonthlyGenerationStrategy(dateUtils, factory))

4. Strategy Factory

class GenerationStrategyFactory {
  create(type: string): GenerationStrategy {
    switch (type) {
      case 'monthly':
        return new MonthlyGenerationStrategy(this.dateUtils, this.factory)
      case 'yearly':
        return new YearlyGenerationStrategy(this.create('monthly') as MonthlyGenerationStrategy)
      default:
        throw new Error(`Unknown strategy type: ${type}`)
    }
  }
}

Weitere Anwendungsfälle

Berechnungsstrategien

interface TimeCalculationStrategy {
  calculate(entry: TimeEntryInput): TimeCalculationResult
}
 
class StandardTimeCalculation implements TimeCalculationStrategy {
  calculate(entry: TimeEntryInput): TimeCalculationResult {
    const gross = this.diffInHours(entry.startTime, entry.endTime)
    const net = gross - entry.breakMin / 60
    return { gross, net, overtime: Math.max(0, net - 8) }
  }
}
 
class FlexTimeCalculation implements TimeCalculationStrategy {
  calculate(entry: TimeEntryInput): TimeCalculationResult {
    // Flexible Arbeitszeit ohne feste 8h
    // ...
  }
}

Validierungsstrategien

interface ValidationStrategy {
  validate(entry: TimeEntry): ValidationResult
}
 
class StrictValidation implements ValidationStrategy {
  validate(entry: TimeEntry): ValidationResult {
    // Strenge Regeln: Pausenzeiten, Min/Max Arbeitszeit
  }
}
 
class LenientValidation implements ValidationStrategy {
  validate(entry: TimeEntry): ValidationResult {
    // Lockere Regeln für spezielle User-Gruppen
  }
}

Alternativen

Option 1: If/Else in Command

❌ Violates Open/Closed, nicht erweiterbar

Option 2: Subclassing

⚠️ Führt zu tiefen Vererbungshierarchien

Option 3: Strategy Pattern (gewählt)

✅ Flexibel, testbar, komposierbar

Trade-offs

Vorteile:

  • ✅ Open/Closed Principle (neue Strategies ohne Änderung)
  • ✅ Single Responsibility (eine Strategy = ein Algorithmus)
  • ✅ Testbar (jede Strategy isoliert testbar)
  • ✅ Komposition (Strategies nutzen andere Strategies)

Nachteile:

  • ⚠️ Mehr Klassen (2 Strategies in CAPture Time)
  • ⚠️ Indirektion (Command → Strategy)
  • ⚠️ Strategy-Selection-Logik nötig

Wann Strategy Pattern nutzen?

✅ Nutzen wenn:

  • Mehrere Varianten eines Algorithmus existieren
  • Conditional Logic zu komplex wird (> 3 Branches)
  • Algorithmen austauschbar sein sollen
  • Algorithmen wiederverwendet werden

❌ Nicht nutzen wenn:

  • Nur ein Algorithmus existiert
  • Algorithmus trivial ist (< 10 Zeilen)
  • Keine Variation erwartet wird

Fazit

Das Strategy Pattern ist ideal für austauschbare Algorithmen in SAP CAP. Es ermöglicht:

  • Flexible Berechnungslogik ohne If/Else-Kaskaden
  • Wiederverwendbare Algorithmen durch Komposition
  • Testbare Business-Logik durch isolierte Strategies
  • Erweiterbarkeit ohne bestehenden Code zu ändern

In Kombination mit Command Pattern und Repository Pattern entsteht eine Clean Architecture.

Siehe auch