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

High Cohesion

This lesson was translated automatically – it may contain errors.

A class should take on as few tasks that are not specific to it as possible and have a clearly defined area of use.

A module that carries a lot of unrelated responsibility is harder to maintain and to reuse, and it changes more often than the rest – for reasons that have nothing to do with each other.

What low cohesion looks like

A class that grew out of «let it sit here for now»:

// UserManager: sign-up, e-mail, HTML and backups in a single class
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, "Sign-up", 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: sign-up, e-mail, HTML and backups in a single class
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, "Sign-up", 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: sign-up, e-mail, HTML and backups in a single type
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: sign-up, e-mail, HTML and backups in a single class
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: sign-up, e-mail, HTML and backups in a single class
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, 'Sign-up', 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: sign-up, e-mail, HTML and backups in a single class
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, "Sign-up", 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: sign-up, e-mail, HTML and backups in a single class
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, "Sign-up", 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()
}

The word «manager» in the name is already a hint: the class has collected four independent occupations. Here is what that turns into:

  • four reasons to change – the sign-up rules, the e-mail template, the profile markup and the backup policy come from four different places and at different times;
  • edits collide head-on – the front-end developer, the back-end developer and the administrator all edit the same file;
  • nothing can be reused – sending e-mail from another scenario drags the sign-up and pg_dump along with it;
  • the sign-up test needs SMTP – sending is wired straight into the method, there is no way to switch it off.

How to split it up

We cut along the reasons to change: every occupation moves into its own class, and the sign-up keeps only the scenario.

// Each class is about one thing
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, "Sign-up", "...");
}

public class ProfileView
{
    public string Render(User user) =>
        $"<div class='profile'><h2>{user.Name}</h2><p>{user.Email}</p></div>";
}
// Each class is about one thing
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, "Sign-up", "...");
    }
};

class ProfileView {
public:
    std::string Render(const User& user) {
        return "<div class='profile'><h2>" + user.name + "</h2></div>";
    }
};
// Each type is about one thing
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>"
}
# Each class is about one thing
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>"
// Each class is about one thing
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, 'Sign-up', '...');
    }
}

class ProfileView {
    render(user: User): string {
        return `<div class="profile"><h2>${user.name}</h2><p>${user.email}</p></div>`;
    }
}
// Each class is about one thing
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(), "Sign-up", "...");
    }
}

public class ProfileView {
    public String render(User user) {
        return "<div class=\"profile\"><h2>" + user.getName() + "</h2></div>";
    }
}
// Each class is about one thing
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, "Sign-up", "...")
}

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

There are more classes now, and less work:

  • each class has one reason to change – the e-mail template is edited in EmailNotifier, the markup in ProfileView, the sign-up scenario in UserRegistration;
  • the name honestly describes the contents – it is clear from the title where to go with a task;
  • e-mail is reusable – EmailNotifier will do for password recovery and order notifications, it has no need for the sign-up;
  • the sign-up test gets by with a stub – an empty Notifier goes in instead of EmailNotifier, no SMTP is started;
  • the backup left the code entirely – that is a job for a schedule and a database administrator, a class has nothing to do here.

A cohesion check

Try to describe what a class does in one sentence without the word «and». If it does not work out – cohesion is low.

Note the pair with the previous principle: High Cohesion and Low Coupling complement and support each other, and all the other GRASP principles essentially serve the same end – less coupling and more cohesion.