Программа курса← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF
7Методические материалы

High Cohesion (Высокая связность)

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

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

Как выглядит низкая связность

Класс, который вырос из «ну пусть пока полежит здесь»:

// UserManager: регистрация, письма, HTML и бэкапы в одном классе
public class UserManager
{
    public void Register(User user)
    {
        _users.Save(user);
        SendEmail(user.Email, RenderWelcomeHtml(user));
    }

    public void SendEmail(string to, string body) =>
        new SmtpClient("smtp.example.com").Send("noreply@shop.io", to, "Регистрация", body);

    public string RenderProfileHtml(User user) =>
        $"<div class='profile'><h2>{user.Name}</h2><p>{user.Email}</p></div>";

    public void BackupDatabase() => Process.Start("pg_dump", "-d shop -f /backup/shop.sql");
}
// UserManager: регистрация, письма, HTML и бэкапы в одном классе
class UserManager {
public:
    void Register(const User& user) {
        users_.Save(user);
        SendEmail(user.email, RenderWelcomeHtml(user));
    }

    void SendEmail(const std::string& to, const std::string& body) {
        SmtpClient("smtp.example.com").Send("noreply@shop.io", to, "Регистрация", body);
    }

    std::string RenderProfileHtml(const User& user) {
        return "<div class='profile'><h2>" + user.name + "</h2></div>";
    }

    void BackupDatabase() { std::system("pg_dump -d shop -f /backup/shop.sql"); }

private:
    UserRepository users_;
};
// UserManager: регистрация, письма, HTML и бэкапы в одном типе
type UserManager struct {
	users UserRepository
}

func (m UserManager) Register(user User) error {
	if err := m.users.Save(user); err != nil {
		return err
	}

	return m.SendEmail(user.Email, m.RenderWelcomeHTML(user))
}

func (UserManager) SendEmail(to, body string) error {
	return smtp.SendMail("smtp.example.com:25", nil, "noreply@shop.io", []string{to}, []byte(body))
}

func (UserManager) RenderProfileHTML(user User) string {
	return "<div class=\"profile\"><h2>" + user.Name + "</h2></div>"
}

func (UserManager) BackupDatabase() error {
	return exec.Command("pg_dump", "-d", "shop", "-f", "/backup/shop.sql").Run()
}
# UserManager: регистрация, письма, HTML и бэкапы в одном классе
class UserManager:
    def register(self, user: User) -> None:
        self._users.save(user)
        self.send_email(user.email, self.render_welcome_html(user))

    def send_email(self, to: str, body: str) -> None:
        smtplib.SMTP("smtp.example.com").sendmail("noreply@shop.io", to, body)

    def render_profile_html(self, user: User) -> str:
        return f"<div class='profile'><h2>{user.name}</h2><p>{user.email}</p></div>"

    def backup_database(self) -> None:
        subprocess.run(["pg_dump", "-d", "shop", "-f", "/backup/shop.sql"], check=True)
// UserManager: регистрация, письма, HTML и бэкапы в одном классе
class UserManager {
    register(user: User): void {
        this.users.save(user);
        this.sendEmail(user.email, this.renderWelcomeHtml(user));
    }

    sendEmail(to: string, body: string): void {
        new SmtpClient('smtp.example.com').send('noreply@shop.io', to, 'Регистрация', body);
    }

    renderProfileHtml(user: User): string {
        return `<div class="profile"><h2>${user.name}</h2><p>${user.email}</p></div>`;
    }

    backupDatabase(): void {
        execSync('pg_dump -d shop -f /backup/shop.sql');
    }
}
// UserManager: регистрация, письма, HTML и бэкапы в одном классе
public class UserManager {
    public void register(User user) {
        users.save(user);
        sendEmail(user.getEmail(), renderWelcomeHtml(user));
    }

    public void sendEmail(String to, String body) {
        new SmtpClient("smtp.example.com").send("noreply@shop.io", to, "Регистрация", body);
    }

    public String renderProfileHtml(User user) {
        return "<div class=\"profile\"><h2>" + user.getName() + "</h2></div>";
    }

    public void backupDatabase() throws IOException {
        new ProcessBuilder("pg_dump", "-d", "shop", "-f", "/backup/shop.sql").start();
    }
}
// UserManager: регистрация, письма, HTML и бэкапы в одном классе
class UserManager(private val users: UserRepository) {
    fun register(user: User) {
        users.save(user)
        sendEmail(user.email, renderWelcomeHtml(user))
    }

    fun sendEmail(to: String, body: String) =
        SmtpClient("smtp.example.com").send("noreply@shop.io", to, "Регистрация", body)

    fun renderProfileHtml(user: User) =
        """<div class="profile"><h2>${user.name}</h2><p>${user.email}</p></div>"""

    fun backupDatabase() =
        ProcessBuilder("pg_dump", "-d", "shop", "-f", "/backup/shop.sql").start()
}

Слово «менеджер» в имени – уже подсказка: класс собрал четыре независимых занятия. Чем это оборачивается:

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

Как разделить

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

// Каждый класс про одно
public class UserRegistration
{
    private readonly IUserRepository _users;
    private readonly INotifier _notifier;

    public UserRegistration(IUserRepository users, INotifier notifier)
    {
        _users = users;
        _notifier = notifier;
    }

    public void Register(User user)
    {
        _users.Save(user);
        _notifier.Welcome(user);
    }
}

public class EmailNotifier : INotifier
{
    public void Welcome(User user) =>
        new SmtpClient("smtp.example.com").Send("noreply@shop.io", user.Email, "Регистрация", "...");
}

public class ProfileView
{
    public string Render(User user) =>
        $"<div class='profile'><h2>{user.Name}</h2><p>{user.Email}</p></div>";
}
// Каждый класс про одно
class UserRegistration {
public:
    UserRegistration(UserRepository& users, Notifier& notifier)
        : users_(users), notifier_(notifier) {}

    void Register(const User& user) {
        users_.Save(user);
        notifier_.Welcome(user);
    }

private:
    UserRepository& users_;
    Notifier& notifier_;
};

class EmailNotifier : public Notifier {
public:
    void Welcome(const User& user) override {
        SmtpClient("smtp.example.com").Send("noreply@shop.io", user.email, "Регистрация", "...");
    }
};

class ProfileView {
public:
    std::string Render(const User& user) {
        return "<div class='profile'><h2>" + user.name + "</h2></div>";
    }
};
// Каждый тип про одно
type UserRegistration struct {
	users    UserRepository
	notifier Notifier
}

func (r UserRegistration) Register(user User) error {
	if err := r.users.Save(user); err != nil {
		return err
	}

	return r.notifier.Welcome(user)
}

type EmailNotifier struct{}

func (EmailNotifier) Welcome(user User) error {
	return smtp.SendMail("smtp.example.com:25", nil, "noreply@shop.io", []string{user.Email}, welcome(user))
}

type ProfileView struct{}

func (ProfileView) Render(user User) string {
	return "<div class=\"profile\"><h2>" + user.Name + "</h2></div>"
}
# Каждый класс про одно
class UserRegistration:
    def __init__(self, users: UserRepository, notifier: Notifier) -> None:
        self._users = users
        self._notifier = notifier

    def register(self, user: User) -> None:
        self._users.save(user)
        self._notifier.welcome(user)

class EmailNotifier:
    def welcome(self, user: User) -> None:
        smtplib.SMTP("smtp.example.com").sendmail("noreply@shop.io", user.email, welcome_body(user))

class ProfileView:
    def render(self, user: User) -> str:
        return f"<div class='profile'><h2>{user.name}</h2><p>{user.email}</p></div>"
// Каждый класс про одно
class UserRegistration {
    constructor(
        private readonly users: UserRepository,
        private readonly notifier: Notifier,
    ) {}

    register(user: User): void {
        this.users.save(user);
        this.notifier.welcome(user);
    }
}

class EmailNotifier implements Notifier {
    welcome(user: User): void {
        new SmtpClient('smtp.example.com').send('noreply@shop.io', user.email, 'Регистрация', '...');
    }
}

class ProfileView {
    render(user: User): string {
        return `<div class="profile"><h2>${user.name}</h2><p>${user.email}</p></div>`;
    }
}
// Каждый класс про одно
public class UserRegistration {
    private final UserRepository users;
    private final Notifier notifier;

    public UserRegistration(UserRepository users, Notifier notifier) {
        this.users = users;
        this.notifier = notifier;
    }

    public void register(User user) {
        users.save(user);
        notifier.welcome(user);
    }
}

public class EmailNotifier implements Notifier {
    @Override
    public void welcome(User user) {
        new SmtpClient("smtp.example.com").send("noreply@shop.io", user.getEmail(), "Регистрация", "...");
    }
}

public class ProfileView {
    public String render(User user) {
        return "<div class=\"profile\"><h2>" + user.getName() + "</h2></div>";
    }
}
// Каждый класс про одно
class UserRegistration(
    private val users: UserRepository,
    private val notifier: Notifier,
) {
    fun register(user: User) {
        users.save(user)
        notifier.welcome(user)
    }
}

class EmailNotifier : Notifier {
    override fun welcome(user: User) =
        SmtpClient("smtp.example.com").send("noreply@shop.io", user.email, "Регистрация", "...")
}

class ProfileView {
    fun render(user: User) =
        """<div class="profile"><h2>${user.name}</h2><p>${user.email}</p></div>"""
}

Классов стало больше, а работы – меньше:

  • у каждого класса одна причина меняться – шаблон письма правит EmailNotifier, вёрстку ProfileView, сценарий регистрации UserRegistration;
  • имя честно описывает содержимое – по названию видно, куда идти с задачей;
  • письма переиспользуются – EmailNotifier пригодится в восстановлении пароля и в уведомлениях о заказе, регистрация ему не нужна;
  • тест регистрации обходится заглушкой – вместо EmailNotifier подставляется пустой Notifier, SMTP не поднимается;
  • бэкап ушёл из кода совсем – это задача расписания и администратора БД, классу здесь делать нечего.

Проверка на связность

Попробуйте описать, чем занимается класс, одним предложением без союза «и». Если не получается – связность низкая.

Обратите внимание на пару с предыдущим принципом: High Cohesion и Low Coupling дополняют и поддерживают друг друга, а все остальные принципы GRASP по сути служат тому же – уменьшить зацепление и увеличить связность.