Liigu peamise sisu juurde

S - Single Responsibility Principle

Sissejuhatus

A class should have only one reason to change.

Ehk Klassil peaks olema ainult üks põhjus muutumiseks. See on Single Responsibility Principle (SRP) ehk ühe vastutuse printsiip.

„Muutumise põhjus“ tähendab nõuete allikat: huvipoolt, aspekti või domeeni osa, mille reeglid võivad muutuda teistest sõltumatult. Kui klassil on kaks erinevat muutumise põhjust, võib ühe aspekti poolt ajendatud muudatus kogemata rikkuda teisega seotud käitumise.

SRP on tihedalt seotud kapseldamisega. Hästi kapseldatud klass peidab oma sisemised andmed ja pakub avalikku kontrollitud liidest nendele ligi pääsemiseks. SRP viib selle sammu võrra edasi - mitte ainult andmed ei peaks olema peidetud, vaid ka klassi eesmärk peaks olema selge ja üheselt määratletud.

Klass mis vastutab mitme aspekti eest

Vaatleme klassi, mis haldab kasutajakontot lihtsas pangarakenduses:

class UserAccount {
private String owner;
private double balance;

public UserAccount(String owner, double initialBalance) {
this.owner = owner;
this.balance = initialBalance;
}

public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
balance += amount;
}

public void withdraw(double amount) {
if (amount > balance) throw new IllegalArgumentException("Insufficient funds");
balance -= amount;
}

// Formats an account statement for printing
public String generateStatement() {
return "Account: " + owner + "\nBalance: " + balance + " EUR";
}

// Sends the statement to the user by email
public void sendStatementByEmail(String emailAddress) {
String statement = generateStatement();
// ... email sending logic ...
System.out.println("Sending to " + emailAddress + ":\n" + statement);
}
}

Antud klass vastutab kolme erineva aspekti eest:

  1. Konto haldamine - raha sissemaksmine ja väljavõtmine ning ärireeglite jõustamine.
  2. Aruande vormindamine - konto andmeid tekstina esitatamine.
  3. E-kirjade edastamine - sõnumite saatmine üle võrgu.

Neil kolmel aspektil on täiesti erinevad muutumise põhjused:

  • väljamaksetega seotud ärireeglid võivad muutuda (näiteks lisatakse arvelduskaitse ehk pole võimalik välja võtta rohkem kui kontol vabu vahendeid on).
  • kontoväljavõtte vorming võib muutuda (näiteks uus paigutus).
  • e-posti teenusepakkuja võib muutuda (näiteks minnakse üle ühelt e-posti API-lt teisele).

Iga sellise muudatuse jaoks peab muutma UserAccount klassi, mis lõpuks võib potentsiaalselt mõjutada ka teisi aspekte, mis antud klassis on.

SRP rakendamine

Olukord laheneb, kui antud klass tükeldada mitmeks erinevaks klassiks, kus iga klass vastutab ühe kindla aspekti eest:

class UserAccount {
private String owner;
private double balance;

public UserAccount(String owner, double initialBalance) {
this.owner = owner;
this.balance = initialBalance;
}

public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
balance += amount;
}

public void withdraw(double amount) {
if (amount > balance) throw new IllegalArgumentException("Insufficient funds");
balance -= amount;
}

public String getOwner() { return owner; }
public double getBalance() { return balance; }
}
class AccountStatementFormatter {
public String format(UserAccount account) {
return "Account: " + account.getOwner()
+ "\nBalance: " + account.getBalance() + " EUR";
}
}
class EmailService {
public void send(String address, String body) {
// ... email sending logic ...
System.out.println("Sending to " + address + ":\n" + body);
}
}

Nüüd igal klassil on ainult üks põhjus, miks nad eales peaksid muutuma:

  • UserAccount muutub ainult siis, kui kontodega seotud äriloogika muutub.
  • AccountStatementFormatter muutub ainult siis, kui kontoväljavõtte loogika/vormindamine muutub.
  • EmailService muutub kui e-posti teenuses midagi muutub.

Kõiki kolme klassi on nüüd võimalik arendada, testida ja asendada üksteisest sõltumatult.

Eraldatud klasside kasutamine

Kasutaja paneb nüüd vajalikud tükid ise kokku:

UserAccount account = new UserAccount("Alice", 1000.0);
account.deposit(500.0);

AccountStatementFormatter formatter = new AccountStatementFormatter();
String statement = formatter.format(account);

EmailService email = new EmailService();
email.send("alice@example.com", statement);

Iga funktsionaalsuse jaoks luuakse eraldi objekt ning selle kaudu kutsutakse välja vastavad meetodid. Esmapilgul võib see tähendada veidi rohkem koodi, kuid see lisastruktuur tasub end ära: iga komponent on iseseisvalt kasutatav, testitav ja laiendatav.

  • AccountStatementFormatter ei pea teadma midagi e-posti teenuse kohta.
  • EmailService saab töötada vabalt, ilma kasutajatest midagi teadmata.

Selline eraldatus vähendab omavahelisi sõltuvusi ning muudab süsteemi paindlikumaks ja vastupidavamaks muutustele.

Kuidas tuvastada SPR järgimist

Hea viis klassi SRP printsiibi järgimist tuvastada on seda klassi selgitades. Klassi peaks ideaalis saama selgitada ühe konkreetse lausega. Võtame näiteks peatüki alguses kirjeldatud klassi. UserAccount klass haldab kontoandmeid ja vormindab kontoväljavõtet ja vastutab e-kirjade saatmise eest. Kui selgituses esineb sõna "ja", siis see on märk sellest, et antud klass teeb liiga palju asju korraga.

Teiseks indikaatoriks on see, et klassil on meetodid, mis töötavad erinevate andmete pealt. UserAccount klassil olid väljad balance ja owner. Samas seal oli ka meetod sendStatementByEmail, mis ei kasutanud eelnevalt mainitud väljasid ning emailiaadress tuli üldse kuskilt mujalt. E-posti aadress ei ole antud juhul seotud konto haldamisega, seetõttu sellega seotud loogika ei peaks ka olema UserAccount klassis.

nõuanne

SRP ei tähenda, et iga klass peab olema väike. Klassil võib olla palju meetodeid, kui need kõik on seotud ühe vastutusega ning kui kõik need teenivad samat sidusat eesmärki. Oluline küsimus on, kas kogu klassi käitumine on ajendatud ühest ja samast „muutumise põhjusest“.