In Lektion 2.14.1 stand eine Testattrappe auf der Liste der Gerüche: eine Klasse, die eine Schnittstelle des Speichers umsetzt und die meisten Methoden mit throw new NotImplementedException() füllt, weil der Test sie nicht braucht. Diese Attrappe ist kein Fehler des Tests. Sie zeigt, dass die Schnittstelle zu breit für ihre Aufrufer ist. Das I in SOLID, das Interface-Segregation-Prinzip, sagt: Kein Aufrufer soll von Methoden abhängen, die er nicht benutzt. In dieser Lektion schneiden Sie den Speicher der Kladde so, dass jeder Aufrufer genau das bekommt, was er braucht.

Der Geruch: eine Schnittstelle, die mitgewachsen ist

Seit Lektion 1.4.4 hat die Kladde ihren Vertrag IKladdeStore. In Lektion 2.4.11 war er klein: alles laden, alles speichern. Mit der Datenbank kam der Wunsch, nicht mehr alles zu laden, und in vielen Kladden ist der Vertrag seitdem gewachsen, eine Methode für jede neue Abfrage. So oder ähnlich sieht er dann aus:

C#
/// <summary>Before: everything the Kladde ever needed from its store, in one contract.</summary>
public interface IKladdeStore
{
    Task<IReadOnlyList<Contact>> SearchContactsAsync(string term, int page, CancellationToken cancellationToken);
    Task<Contact?> FindContactAsync(int id, CancellationToken cancellationToken);
    Task<Result<Contact>> AddContactAsync(string name, string email, CancellationToken cancellationToken);
    Task UpdateContactAsync(Contact contact, CancellationToken cancellationToken);
    Task DeleteContactAsync(int id, CancellationToken cancellationToken);
    Task<IReadOnlyList<Appointment>> AppointmentsBetweenAsync(DateTimeOffset from, DateTimeOffset to, CancellationToken cancellationToken);
    Task<Result<Appointment>> AddAppointmentAsync(string title, TimeRange time, CancellationToken cancellationToken);
    Task DeleteAppointmentAsync(int id, CancellationToken cancellationToken);
    Task<IReadOnlyList<Booking>> BookingsBetweenAsync(DateTimeOffset from, DateTimeOffset to, CancellationToken cancellationToken);
    Task<Result<Booking>> AddBookingAsync(string name, string email, TimeRange time, CancellationToken cancellationToken);
    Task ConfirmBookingAsync(Guid id, CancellationToken cancellationToken);
    Task DeleteBookingAsync(Guid id, CancellationToken cancellationToken);
}

Zwölf Methoden, und in Ihrer Kladde können es mehr sein. Ist Ihr IKladdeStore noch der aus Lektion 2.4.11 mit LoadAsync und SaveAsync, steckt dieselbe Breite an anderer Stelle: Die Seite der freien Zeiten lädt die ganze Kladde mit allen Kontakten und Notizen, um die Termine eines Tages zu sehen, und jede Attrappe muss einen vollständigen KladdeContents bauen. Der Schnitt dieser Lektion ist derselbe.

Wer braucht was? Die Seite der freien Zeiten (Lektion 2.5.6) braucht eine Sache: die belegten Zeiten eines Tages, aus Terminen und Buchungen. Sie bekommt zwölf Methoden, darunter das Löschen von Kontakten. Daraus folgen drei Schmerzen:

  • Jede Attrappe baut alles. Ein Test der freien Zeiten braucht eine Attrappe mit zwölf Methoden, elf davon mit NotImplementedException. Kommt eine dreizehnte Methode dazu, bricht jede Attrappe in jedem Testprojekt, auch wo sie nichts mit der neuen Methode zu tun hat.
  • Jede Änderung trifft alle. Ändert sich die Unterschrift von SearchContactsAsync, muss jede Klasse neu übersetzt werden, die IKladdeStore kennt, auch die Seite der freien Zeiten.
  • Die Seite könnte mehr, als sie darf. Eine öffentliche Seite, die Kontakte löschen könnte, ist eine unnötige Gefahr (Lektion 2.6.6). Was ein Teil nicht bekommt, kann er nicht falsch machen.