Что такое связность (cohesion) в разработке ПО?

Ответ

Связность (cohesion) — это метрика, оценивающая, насколько тесно связаны между собой обязанности и функциональность внутри одного модуля (класса, метода, компонента). Высокая связность — один из ключевых принципов хорошего проектирования (наряду со слабой связанностью).

Принцип: Элементы внутри модуля должны быть объединены выполнением одной, четко определённой задачи или ответственности.

Почему высокая связность — это хорошо?

  • Упрощает понимание: Класс с одной ответственностью легче понять и изучить.
  • Упрощает поддержку: Изменения в одной функциональной области затрагивают только один модуль.
  • Повышает переиспользуемость: Чётко выделенный функционал проще использовать в других контекстах.
  • Упрощает тестирование: Легче написать сфокусированные модульные тесты.

Примеры:

1. Класс с высокой связностью (функциональная связность):

// Этот класс отвечает только за валидацию email-адресов.
// Все его методы тесно связаны с одной задачей.
public class EmailValidator
{
    public bool IsValidFormat(string email) 
    { 
        return Regex.IsMatch(email, @"^[^@s]+@[^@s]+.[^@s]+$"); 
    }

    public bool IsDomainAllowed(string email, HashSet<string> allowedDomains)
    {
        var domain = email.Split('@').Last();
        return allowedDomains.Contains(domain);
    }

    public ValidationResult Validate(string email, HashSet<string> allowedDomains)
    {
        if (!IsValidFormat(email)) 
            return ValidationResult.Fail("Invalid format");
        if (!IsDomainAllowed(email, allowedDomains)) 
            return ValidationResult.Fail("Domain not allowed");
        return ValidationResult.Success();
    }
}

2. Класс с низкой связностью (случайная/логическая связность):

// Этот класс объединяет три несвязанные ответственности.
// Изменение правил логирования не должно касаться класса, работающего с заказами.
public class OrderManager // Плохое название для такого набора методов
{
    public void ProcessOrder(Order order) { /* ... */ }
    public void SendPromotionalEmail(Customer customer) { /* ... */ } // Отвечает за коммуникацию
    public void LogErrorToDatabase(Exception ex) { /* ... */ }       // Отвечает за инфраструктуру логирования
}

Рефакторинг: Класс с низкой связностью следует разделить на несколько классов, каждый со своей высокой связностью: OrderProcessor, EmailService, Logger.

Ответ 18+ 🔞

Давай разберём эту тему про связность, а то некоторые её так понимают, что волосы дыбом встают. Представь себе, что ты заходишь на кухню и видишь: в одном углу стоит холодильник, в другом — стиральная машинка, а на плите — дрель заряжается. Ну и как, удобно? С одной стороны, всё в одном месте, не надо бегать по квартире. С другой — хуйня полная, потому что это три разных дела, которые между собой не связаны ни хуя. Вот это и есть низкая связность, когда в одном модуле намешано всего, как в помойном ведре.

А высокая связность — это когда у тебя на кухне всё для готовки: плита, холодильник, посуда, ножи. Всё связано одной целью — пожрать сделать. Понял разницу? Вот и в коде так же.

Зачем это нужно, спросишь? Да затем, что жить с этим потом надо!

  • Понять легко. Открыл класс EmailValidator — и сразу ясно, что он только почту проверяет. Не лезет в базу, не шлёт письма, не танцует ламбаду. Одна работа.
  • Менять не страшно. Захотел поменять правила валидации — ковыряешь только этот валидатор. Не боишься, что случайно сломаешь логирование заказов, потому что они в другом месте, на своём хуторе.
  • Переиспользовать можно. Нашёл красивый валидатор — взял и вставил в пять разных проектов. А если бы он был в одном классе с отправкой смс и расчётом зарплаты, пришлось бы тащить за собой эту гирю.
  • Тестировать — одно удовольствие. Подсунул ему почту и проверяй, что возвращает. Никаких лишних зависимостей, которые надо мокать.

Смотри, вот пример, когда всё сделано по уму:

// Этот парень знает своё место — он валидатор почты и нихуя больше.
// Все его методы про одно и то же.
public class EmailValidator
{
    public bool IsValidFormat(string email) 
    { 
        return Regex.IsMatch(email, @"^[^@s]+@[^@s]+.[^@s]+$"); 
    }

    public bool IsDomainAllowed(string email, HashSet<string> allowedDomains)
    {
        var domain = email.Split('@').Last();
        return allowedDomains.Contains(domain);
    }

    public ValidationResult Validate(string email, HashSet<string> allowedDomains)
    {
        if (!IsValidFormat(email)) 
            return ValidationResult.Fail("Invalid format");
        if (!IsDomainAllowed(email, allowedDomains)) 
            return ValidationResult.Fail("Domain not allowed");
        return ValidationResult.Success();
    }
}

Видишь? Класс как монолит. Одна задача, одна ответственность. Красота.

А теперь смотри на этого уродца:

// Это классический пример "бога-класса" или "божественного объекта".
// Он и заказ обработает, и письмо спамное шлёт, и ошибки в базу пишет.
// Связность ниже плинтуса.
public class OrderManager // Название вводит в заблуждение, он же не только заказами управляет!
{
    public void ProcessOrder(Order order) { /* ... */ }
    public void SendPromotionalEmail(Customer customer) { /* ... */ } // При чём тут это? Это ж сервис рассылки!
    public void LogErrorToDatabase(Exception ex) { /* ... */ }       // А это ваще инфраструктурный код, уёбок!
}

Вот этот класс — настоящий распиздяй. Он берёт на себя всё подряд. Захотишь поменять способ логирования — тебе придётся лезть в менеджер заказов, ёпта! Это как чинить унитаз, а тебе говорят: «Да заодно и проводку в гостиной переложи, раз уж руки испачкал».

Что делать с таким? Да раздробить его, блядь, на три нормальных класса!

  • OrderProcessor — пусть только заказы обрабатывает.
  • EmailService — пусть письмами занимается.
  • Logger — пусть логи пишет куда надо.

Каждый будет заниматься своим делом, связность у каждого будет высоченная, и жить станет проще. И запомни, чувак: если не можешь описать ответственность класса одним коротким предложением без союза «и» — значит, ты уже накосячил.