В объектно-ориентированном моделировании существуют различные формы связей между классами. Эти отношения позволяют выразить, насколько сильно классы зависят друг от друга, кто кем управляет и какова природа этой зависимости. Понимание различий между этими связями — ключ к созданию гибкой и сопровождаемой архитектуры.

Ассоциация - предварительная связь без уточнения

На ранних этапах проектирования бывает полезно указать, что два компонента как-то взаимодействуют, не вдаваясь в детали. Такая неопределённая связь называется ассоциацией. Она лишь сигнализирует: один объект знает о другом и, возможно, использует его в работе.

classDiagram
  direction LR
  class Worker {
      +Execute()
  }

  class Logger {
      +Log()
  }

  Worker --> Logger : uses

Диаграмма 1. Направленная ассоциация

Эта форма полезна, когда логика взаимодействия ещё не определена — она показывает, что между сущностями уже есть зависимость, но её форма будет уточнена позже.

Наследование — жёсткая связь по типу

Наследование выражает отношение “является” и предполагает, что один класс полностью разделяет поведение другого, расширяя или переопределяя его. Это позволяет работать с базовым типом, не зная конкретной реализации.

classDiagram
  direction TB
  class Repository {
      +Save()
  }

  class InMemoryRepository {
      +Save()
  }

  Repository <|-- InMemoryRepository

Диаграмма 2. Наследование

Наследование обеспечивает полиморфизм, но формирует жёсткую и статическую связь между классами. Это отношение фиксируется на этапе компиляции и не может быть изменено во время выполнения, что снижает гибкость архитектуры.

Композиция и агрегация - составные зависимости

Когда один класс использует другой не как расширение себя, а как отдельный компонент, мы сталкиваемся с отношениями по типу “содержит” или “включает”. Здесь возможны два подхода: композиция и агрегация. Оба описывают структуру вида “один содержит другого”, но различаются степенью контроля.

classDiagram
  direction LR
  class Service {
      +Run()
  }

  class Repository {
      +Save()
  }

  Service *-- Repository

Диаграмма 3. Композиция: Service управляет Repository

classDiagram
  direction LR
  class Service {
      +Run()
  }

  class IRepository {
      +Save()
  }

  Service o-- IRepository

Диаграмма 4. Агрегация: зависимость через абстракцию

Ключевое различие: при композиции внешний объект создаёт и владеет внутренним; при агрегации — получает его извне, не управляя временем жизни.

Пара моментов, чтобы легче запомнить визуальную нотацию:

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

Примеры на C#:

class CompositeService
{
    private readonly Repository _repository = new Repository();
 
    public void Run()
    {
        // _repository создаётся внутри и полностью под контролем
    }
}
 
class AggregatedService
{
    private readonly IRepository _repository;
 
    public AggregatedService(IRepository repository)
    {
        _repository = repository;
    }
 
    public void Run()
    {
        // _repository передаётся извне
    }
}

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

Иногда можно сочетать композицию с абстракцией, например, создавая зависимость через фабрику:

interface IRepositoryFactory
{
    IRepository Create();
}
 
class Service
{
    private readonly IRepositoryFactory _factory;
 
    public Service(IRepositoryFactory factory)
    {
        _factory = factory;
    }
 
    public void Run()
    {
        var repo = _factory.Create();
        // работа с repo
    }
}

Такой подход сохраняет контроль над жизненным циклом объекта, но не фиксирует реализацию.

Выбор связи зависит от контекста

Одна и та же задача может быть решена разными способами в зависимости от предпочтений проектировщика и требований к гибкости. Ниже сравнение двух подходов — с использованием наследования и с применением агрегации:

classDiagram
  direction BT

  namespace Наследование {
    class BaseService {
        +Process()
        #Save()
    }
    class SqlService {
        +Process()
        #Save()
    }
  }

  namespace Агрегация {
    class Service {
        +Process()
    }
    class IRepository {
        +Save()
    }
    class SqlRepository {
        +Save()
    }
  }

  BaseService <|-- SqlService
  Service o-- IRepository
  IRepository <|-- SqlRepository

Диаграмма 5. Сравнение подходов: жёсткое наследование vs гибкая агрегация

Гибкость повышается по мере перехода от наследования к агрегации, в то время как степень связанности уменьшается.

Архитектурные аспекты

Глубокие или разветвлённые иерархии наследования, а также преобладание композиции над агрегацией — признаки тесной связи между частями системы.

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

Широкое применение композиции с конкретными реализациями также снижает адаптивность системы. Это идёт вразрез с принципом инверсии зависимостей, согласно которому предпочтение следует отдавать абстракциям. Агрегация как раз способствует этому подходу — она поощряет передачу зависимостей извне и их использование через интерфейсы, а не жёсткое связывание с конкретными типами.