Kompozycja zamiast dziedziczenia
Część sekcji Programowanie obiektowe ścieżki C# w Coddy. Lekcja 41 z 70.
Dziedziczenie tworzy silne powiązania między klasami. Gdy Car dziedziczy po Vehicle, każda zmiana w Vehicle może zepsuć Car. Kompozycja oferuje bardziej elastyczną alternatywę: zamiast dziedziczyć zachowanie, klasa zawiera obiekty, które je zapewniają.
Rozważ postać z gry, która może się poruszać i atakować. Dzięki dziedziczeniu możesz stworzyć złożoną hierarchię. Dzięki kompozycji po prostu przypisujesz postaci potrzebne jej komponenty:
public class MovementBehavior
{
public void Move() => Console.WriteLine("Moving...");
}
public class AttackBehavior
{
public void Attack() => Console.WriteLine("Attacking!");
}
public class Character
{
private MovementBehavior movement = new MovementBehavior();
private AttackBehavior attack = new AttackBehavior();
public void Move() => movement.Move();
public void Attack() => attack.Attack();
}Kluczowa różnica dotyczy relacji. Dziedziczenie reprezentuje relację „jest” (Dog jest Animal), natomiast kompozycja reprezentuje relację „ma” (Character ma zachowanie związane z ruchem). Kompozycja pozwala wymieniać zachowania w czasie działania programu, swobodnie łączyć możliwości i unikać problemu kruchej klasy bazowej.
Używaj dziedziczenia, gdy istnieje rzeczywista relacja hierarchiczna. Wybieraj kompozycję, gdy potrzebujesz elastyczności, gdy zachowania mogą być współdzielone przez niepowiązane klasy lub gdy chcesz dynamicznie zmieniać zachowanie.
Wyzwanie
ŁatwyZbudujmy system powiadomień, który pokazuje zalety kompozycji nad dziedziczeniem. Zamiast tworzyć złożoną hierarchię klas powiadomień, utworzysz obiekt NotificationSender, wyposażając go w wymienne komponenty zachowania.
Rozmieścisz swój kod w trzech plikach:
Behaviors.cs: Utwórz dwie klasy zachowań w przestrzeni nazwNotifications, które obsługują różne aspekty wysyłania powiadomień:FormattingBehavior— ma metodęFormat(string message), która zwraca komunikat otoczony przez[ALERT]na początku i[END]na końcuDeliveryBehavior— ma metodęDeliver(string channel, string message), która zwraca ciąg znaków w formacieSending via {channel}: {message}
NotificationSender.cs: Utwórz klasęNotificationSenderw przestrzeni nazwNotifications, która łączy zachowania za pomocą kompozycji. Twój obiekt nadawcy powinien:- Zawierać instancje zarówno
FormattingBehavior, jak iDeliveryBehaviorjako pola prywatne - Mieć właściwość
Channel(string), która przechowuje nazwę kanału dostarczania - Mieć konstruktor, który przyjmuje nazwę kanału
- Mieć metodę
Send(string message), która najpierw formatuje komunikat za pomocą zachowania formatującego, następnie dostarcza go za pomocą zachowania dostarczającego i zwraca końcowy wynik
- Zawierać instancje zarówno
Program.cs: W pliku głównym utwórz obiektNotificationSenderz kanałem pobranym z wejścia, a następnie wyślij komunikat pobrany z wejścia. Wypisz wynik, aby pokazać, jak współdziałają połączone zachowania.
Otrzymasz dwa dane wejściowe:
- Nazwę kanału dostarczania (np.
Email,SMS) - Komunikat do wysłania
Wypisz wynik wywołania Send() na obiekcie nadawcy powiadomień.
Na przykład, jeśli danymi wejściowymi są Email i Server is down, wynikiem powinno być:
Sending via Email: [ALERT] Server is down [END]Zauważ, że NotificationSender nie dziedziczy po żadnej klasie — po prostu „ma” zachowania formatujące i dostarczające. Takie podejście oparte na kompozycji oznacza, że możesz łatwo wymienić dowolne z tych zachowań bez zmieniania klasy nadawcy albo wykorzystać je ponownie w zupełnie innych klasach!
Spróbuj swoich sił
using System;
using Notifications;
class Program
{
public static void Main(string[] args)
{
// Odczytaj dane wejściowe
string channel = Console.ReadLine();
string message = Console.ReadLine();
// TODO: Utwórz NotificationSender z podanym kanałem
// TODO: Wyślij wiadomość i wypisz wynik
}
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy programowania obiektowego
Pliki zewnętrznePrzestrzenie nazw i dyrektywyWprowadzenie do klas i obiektówSłowo kluczowe „this”Metody i parametryPola a właściwościKonstruktoryInicjalizatory obiektówPodsumowanie — prosty kalkulator4Dziedziczenie
Podstawowa składnia dziedziczenia (:)Słowo kluczowe 'base'Słowa kluczowe virtual i overrideKlasy sealedBazowa klasa 'object'Powtórzenie – hierarchia pracowników7Zaawansowane funkcje
Przeciążanie operatorówIndeksery (this[])Nadpisywanie ToString()Metody rozszerzającePodsumowanie – własna lista2Właściwości i składowe statyczne
Właściwości automatyczneWłaściwości tylko do odczytu i zapisuPola i metody statyczneKlasy statyczneSkładowe z ciałem wyrażeniowym5Polimorfizm i interfejsy
Polimorfizm czasu kompilacji a czasu wykonaniaInterfejs a klasa abstrakcyjnaWiele interfejsówJawne implementowanie interfejsówRzutowanie w górę i w dółPowtórka — kalkulator kształtów8Zaawansowane koncepcje OOP
Kompozycja zamiast dziedziczeniaTypy generyczne (klasy i metody)Delegaty i zdarzeniaAtrybuty i refleksjaIDisposable i instrukcja usingPodstawy wstrzykiwania zależności11Wzorce projektowe, część 2
Wzorzec poleceniaWzorzec adapteraWzorzec dekoratoraWzorzec metody szablonowejWzorzec stanuWzorzec kompozytu3Architektura klas
Dane instancji a dane statyczneSłowa kluczowe 'readonly' i 'const'Pola zapasowePowtórzenie — menedżer kont bankowych6Hermetyzacja
Modyfikatory dostępuWłaściwości w hermetyzacjiImplementacja ukrywania danychWzorce niezmiennościPowtórka — dane studentów9Argumenty zmiennej liczby
Słowo kluczowe „params”Parametry opcjonalneArgumenty nazwanePrzeciążanie metodPoćwicz samodzielnie: Kompilator C# online