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_dumpalong 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 inProfileView, the sign-up scenario inUserRegistration; - the name honestly describes the contents – it is clear from the title where to go with a task;
- e-mail is reusable –
EmailNotifierwill 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
Notifiergoes in instead ofEmailNotifier, 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.