1. Styl deklarowania i nazywania wydarzeń
Wydarzenia — to nie tylko delegaty. To odrębna jednostka komunikacji między częściami aplikacji i jej deklaracja powinna być czytelna.
Używaj właściwego typu delegata
W 99% przypadków używaj standardowych delegatów:
- EventHandler — dla wydarzeń bez danych.
- EventHandler<TEventArgs> — gdy trzeba przekazać parametry.
Standaryzacja ułatwia utrzymanie kodu i integrację z bibliotekami .NET. Nie wymyślaj własnego delegata, jeśli pasuje EventHandler.
public event EventHandler SomethingHappened; // Brak danych
public event EventHandler<MyEventArgs> DataReceived; // Są dodatkowe dane
Jeśli potrzebujesz specjalnej customizacji — zadeklaruj własny delegat, ale to rzadkość.
Nazewnictwo wydarzeń
W .NET wydarzenia nazywa się w czasie przeszłym: Completed, Clicked, Changed, Received. Podkreśla to fakt, że coś się wydarzyło.
Przykłady:
public event EventHandler DataLoaded; // Dane zostały załadowane
public event EventHandler<MessageEventArgs> MessageReceived; // Wiadomość odebrana
public event EventHandler Saving; // Rozpoczęto proces zapisu
Czasem używa się formy Changing dla wydarzeń "przed" zmianą, żeby dać możliwość ingerencji.
2. Organizacja klasy-wydawcy: chroniona metoda wirtualna OnEvent
Zawsze dodawaj chronioną wirtualną metodę, która wywołuje wydarzenie: scentralizowany punkt wywołań, rozszerzalność przy dziedziczeniu i przewidywalność zachowania.
public class FileLoader
{
public event EventHandler<FileLoadedEventArgs> FileLoaded;
protected virtual void OnFileLoaded(FileLoadedEventArgs e)
{
FileLoaded?.Invoke(this, e);
}
public void Load(string filename)
{
// ... logika ładowania pliku ...
OnFileLoaded(new FileLoadedEventArgs(filename));
}
}
public class FileLoadedEventArgs : EventArgs
{
public string FileName { get; }
public FileLoadedEventArgs(string fileName) => FileName = fileName;
}
Niech tylko OnFileLoaded wywołuje wydarzenie — łatwiej wtedy utrzymywać i testować.
3. Zasady subskrypcji i odsubskrypcji: cykl życia, IDisposable
Jeśli czas życia subskrybenta jest krótszy niż wydawcy, koniecznie odsubskrybuj przed zniszczeniem subskrybenta. Wygodnie zaimplementować IDisposable i odsubskrybować w Dispose().
public class TemporaryListener : IDisposable
{
private readonly Publisher _publisher;
public TemporaryListener(Publisher publisher)
{
_publisher = publisher;
_publisher.DataReceived += HandleData;
}
private void HandleData(object sender, EventArgs e)
{
// Praca z danymi
}
public void Dispose()
{
_publisher.DataReceived -= HandleData;
}
}
// Użycie z using:
using (var listener = new TemporaryListener(myPublisher))
{
// listener nasłuchuje wydarzeń tutaj
}
// Po wyjściu z using - Dispose wywołany, odsubskrybowano
Jeśli zapomnisz odsubskrybować, wydawca utrzyma referencję do delegata subskrybenta — dostaniesz wyciek pamięci i "zombie-obyekty".
4. Bezpieczeństwo wątkowe wywołań wydarzeń
W wielowątkowym kodzie subskrybenci mogą być dodawani/usuwani równolegle z wywołaniem wydarzenia. To grozi wyścigami i NullReferenceException. Użyj bezpiecznego wzorca: skopiuj delegat do lokalnej zmiennej.
protected virtual void OnSomethingHappened()
{
EventHandler handler = SomethingHappened;
handler?.Invoke(this, EventArgs.Empty);
}
W C# 6+ wystarczy:
SomethingHappened?.Invoke(this, EventArgs.Empty);
5. Używaj EventArgs zamiast object
Nie przekazuj danych przez object ani pola klasy. Używaj silnego typowania przez dziedziczące EventArgs.
public class DownloadCompletedEventArgs : EventArgs
{
public string FileName { get; }
public long Size { get; }
public DownloadCompletedEventArgs(string fileName, long size)
{
FileName = fileName;
Size = size;
}
}
public event EventHandler<DownloadCompletedEventArgs> DownloadCompleted;
6. Dokumentowanie wydarzeń i subskrybentów
Dokumentuj: kiedy wydarzenie jest wywoływane, znaczenie pól EventArgs, czy i kiedy trzeba odsubskrybować.
/// <summary>
/// Wydarzenie występuje po pomyślnym załadowaniu danych.
/// </summary>
public event EventHandler<DataLoadedEventArgs> DataLoaded;
7. Zbiorcze rekomendacje dotyczące architektury wydarzeń
Rozdzielaj obowiązki
Wydawca tylko informuje o zdarzeniu. Subskrybent sam decyduje, kiedy subskrybować i odsubskrybować.
Unikaj "bombardowania" wydarzeniami
Nie generuj tego samego wydarzenia dziesiątki razy na sekundę bez potrzeby — to nadmierne obciążenie.
Stosuj wydarzenia raczej nie do komunikacji dwukierunkowej
Wydarzenia są do schematu "jeden raportuje — wielu nasłuchuje". Dla komunikacji dwukierunkowej rozważ interfejsy, callbacki lub inne mechanizmy.
Nie przechowuj w klasie subskrybentów
Nie trzymaj jawnych referencji do subskrybentów — wydarzenia i delegaty zrobią to automatycznie.
8. Klasyczne antywzorce
Wydarzenia bez typów
public event Action<object> SomethingHappened; // Nie wiadomo, co jest w środku
Słabo: łamie się typowanie, potrzebne są rzutowania, traci się utrzymywalność.
Zapomniano o odsubskrybowaniu
public class ShortLivedListener
{
public ShortLivedListener(Publisher p) =>
p.DataReceived += DoWork;
private void DoWork(object sender, EventArgs e) { /* ... */ }
// Brak Dispose, brak odsubskrybowania => zombie-obyekty!
}
Łamanie SRP
Klasa jednocześnie wydawca, subskrybent i handler — mieszanie ról. Rozdziel odpowiedzialności.
9. Praktyczne zastosowanie na rozmowach kwalifikacyjnych i w projektach
W wielu projektach z publikacją-subskrypcją poprawna organizacja wydarzeń to klucz do skalowalności i utrzymania. Na rozmowach technicznych często proszą o:
- zaimplementowanie systemu wydarzeń z poprawnym typowaniem,
- pokazanie zarządzania cyklem życia subskrybentów,
- wyjaśnienie bezpiecznego wątkowo wywoływania wydarzeń.
Czysty, udokumentowany, poprawnie zorganizowany kod oparty na wydarzeniach od razu wyróżnia kandydata.
GO TO FULL VERSION