Menu
Coddy logo textTech

Композиция вместо наследования

Часть раздела Объектно-ориентированное программирование путешествия по C# на Coddy — урок 41 из 70.

Наследование создает сильную связанность между классами. Когда Car наследуется от Vehicle, любое изменение в Vehicle может нарушить работу Car. Композиция предлагает более гибкую альтернативу: вместо наследования поведения класс содержит объекты, которые обеспечивают это поведение.

Рассмотрим игрового персонажа, который может двигаться и атаковать. С помощью наследования вы могли бы создать сложную иерархию. С помощью композиции вы просто даете персонажу необходимые ему компоненты:

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();
}

Ключевое различие заключается в типе отношений. Наследование представляет собой связь «is-a» (собака является животным), в то время как композиция представляет собой связь «has-a» (у персонажа есть поведение движения). Композиция позволяет менять поведение во время выполнения, свободно комбинировать возможности и избегать проблемы хрупкого базового класса.

Используйте наследование, когда существует подлинная иерархическая связь. Выбирайте композицию, когда вам нужна гибкость, когда поведение может разделяться между несвязанными классами или когда вы хотите динамически изменять поведение.

challenge icon

Задание

Легко

Давайте создадим систему уведомлений, которая демонстрирует мощь композиции над наследованием. Вместо создания сложной иерархии классов уведомлений, вы скомпонуете NotificationSender, наделив его взаимозаменяемыми компонентами поведения.

Вы организуете свой код в трех файлах:

  • Behaviors.cs: Создайте два класса поведения в пространстве имен Notifications, которые обрабатывают различные аспекты отправки уведомлений:
    • FormattingBehavior — имеет метод Format(string message), который возвращает сообщение, обернутое в [ALERT] в начале и [END] в конце
    • DeliveryBehavior — имеет метод Deliver(string channel, string message), который возвращает строку в формате Sending via {channel}: {message}
  • NotificationSender.cs: Создайте класс NotificationSender в пространстве имен Notifications, который использует композицию для объединения поведений. Ваш отправитель должен:
    • Содержать экземпляры как FormattingBehavior, так и DeliveryBehavior в качестве закрытых полей
    • Иметь свойство Channel (string), в котором хранится название канала доставки
    • Иметь конструктор, принимающий название канала
    • Иметь метод Send(string message), который сначала форматирует сообщение с помощью поведения форматирования, затем доставляет его с помощью поведения доставки и возвращает окончательный результат
  • Program.cs: В основном файле создайте NotificationSender с каналом из входных данных, затем отправьте сообщение из входных данных. Выведите результат, чтобы показать, как скомпонованные поведения работают вместе.

Вы получите два входных значения:

  • Название канала доставки (например, Email, SMS)
  • Сообщение для отправки

Выведите результат вызова Send() вашего отправителя уведомлений.

Например, если входные данные — Email и Server is down, вывод должен быть следующим:

Sending via Email: [ALERT] Server is down [END]

Обратите внимание, что NotificationSender ни от чего не наследуется — он просто «имеет» поведения форматирования и доставки. Такой подход с использованием композиции означает, что вы можете легко заменить любое поведение, не меняя класс отправителя, или повторно использовать эти поведения в совершенно других классах!

Шпаргалка

Композиция — это альтернатива наследованию, которая позволяет создавать более гибкий код за счет того, что класс содержит объекты, обеспечивающие поведение, вместо того чтобы наследовать поведение от родительского класса.

Наследование против композиции:

  • Наследование представляет отношения типа «является» (Собака является Животным)
  • Композиция представляет отношения типа «имеет» (Персонаж имеет поведение движения)

Преимущества композиции:

  • Смена поведения во время выполнения
  • Свободное комбинирование возможностей
  • Избежание сильной связанности и проблем хрупкого базового класса
  • Совместное использование поведения в несвязанных классах

Пример с компонентами поведения:

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();
}

Когда использовать каждый из подходов:

  • Используйте наследование для подлинных иерархических отношений
  • Выбирайте композицию для гибкости, общего поведения или динамического изменения поведения

Попробуйте сами

using System;
using Notifications;

class Program
{
    public static void Main(string[] args)
    {
        // Чтение входных данных
        string channel = Console.ReadLine();
        string message = Console.ReadLine();

        // TODO: Создать NotificationSender с каналом
        
        // TODO: Отправить сообщение и вывести результат
        
    }
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование