Класс должен выполнять как можно меньше не специфичных для него задач и иметь вполне определённую область применения.
Если модуль несёт много разноплановой ответственности, он сложнее в поддержке и переиспользовании, а меняться будет чаще остальных – по причинам, не связанным друг с другом.
Как выглядит низкая связность
Класс, который вырос из «ну пусть пока полежит здесь»:
// 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 по сути служат тому же – уменьшить зацепление и увеличить связность.