D - Dependency Inversion Principle
Sissejuhatus
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Seda väidab Dependency Inversion Principle (DIP) ehk sõltuvuste inversiooni printsiip. Tarkvarakomponendid peaksid sõltuma abstraktsioonidest, mitte konkreetsetest klassidest.
Selle mõttega tutvusime juba abstraktsiooni peatükis:
// Depending on a concrete class — must change when the implementation changes
PayPalPayment payment = new PayPalPayment();
payment.sendPayPalRequest(amount, apiKey);
// Depending on an abstraction — does not change when the implementation changes
Payment payment = new StripePayment();
payment.pay(amount);
DIP annab sellele mustrile nime ja täpsema määratluse. See ei kehti ainult üksikute muutujate deklareerimise kohta, vaid kogu komponentide disaini kohta.
Kõrgtaseme ja madala taseme moodulid
Kõrgtaseme all mõeldakse koodi, mis väljendab äriloogikat ehk mida antud süsteem teeb. Madala tasemel all mõeödakse koodi, mis tegeleb selle loogika teostamisega ehk kuidas seda saavutatakse (andmebaasipäringud, e-kirjade saatmine, failioperatsioonid jne.).
Kui kõrgtaseme loogika loob ja kasutab otse madala taseme komponente, muutuvad need tihedalt omavahel seotuks. Muutus madalamal tasemel sunnib muutma ka kõrgtaseme loogikat, kuigi ärireeglid ise võivad muutumata olla.
Probleemist täpsemalt
Kasutame näitena tellimuste töötlemise süsteemi. Kui tellimus esitatakse, peaks klient saama selle kohta ka kinnitusteavituse. Üks viis seda saavutada oleks:
class EmailNotifier {
public void sendEmail(String address, String message) {
// Some low-level email handling logic
System.out.println("Email to " + address + ": " + message);
}
}
class OrderProcessor {
private EmailNotifier notifier = new EmailNotifier(); // direct dependency
public void processOrder(String customerEmail, String orderDetails) {
// ... process the order ...
notifier.sendEmail(customerEmail, "Your order has been placed: " + orderDetails);
}
}
OrderProcessor on kõrgtaseme moodul, mis sisaldab äriloogikat selle kohta, mis juhtub tellimuse esitamisel.
EmailNotifier on madalama taseme moodul, mis tegeleb e-kirjade välja saatmisega.
Antud juhul on probleemiks see, et OrderProcessor loob otse enda sees EmailNotifier-i eksemplari.
Kui ärinõuded muutuvad ja e-kirjade asemel tuleb saata SMS-teavitusi, tuleb OrderProcessor-it muuta:
// Must open OrderProcessor and change this line:
private SmsNotifier notifier = new SmsNotifier();
Äriloogika jäi samaks, ainult teavituse kanal muutus. Ometi tuli muuta tellimuste töötlemisega seotud koodi.
DIP rakendamine
Selle probleemi lahendamiseks tuleks sisse tuua abstraktsioon, millele peale saavad mõlemad osapooled toetuda:
interface Notifier {
void notify(String recipient, String message);
}
Madala taseme teostused sõltuvad sellest abstraktsioonist:
class EmailNotifier implements Notifier {
@Override
public void notify(String recipient, String message) {
// Some low-level email handling logic
System.out.println("Email to " + recipient + ": " + message);
}
}
class SmsNotifier implements Notifier {
@Override
public void notify(String recipient, String message) {
// Some low-level SMS handling logic
System.out.println("SMS to " + recipient + ": " + message);
}
}
OrderProcessor nüüd sõltub Notifier abstraktsioonist, mille loomise eest vastutab mingi väline osapool:
class OrderProcessor {
private final Notifier notifier;
public OrderProcessor(Notifier notifier) {
this.notifier = notifier;
}
public void processOrder(String customerRecipient, String orderDetails) {
// ... process the order ...
notifier.notify(customerRecipient, "Your order has been placed: " + orderDetails);
}
}
OrderProcessor-it ei pea nüüd muutma, kui teavituse kanal muutub:
// Use email
OrderProcessor processor = new OrderProcessor(new EmailNotifier());
processor.processOrder("alice@example.com", "Order #1042");
// Switch to SMS — OrderProcessor is untouched
OrderProcessor processor = new OrderProcessor(new SmsNotifier());
processor.processOrder("+372 5000 0000", "Order #1043");
Inversioon
Algse lahenduse puhul suundus sõltuvus allapoole:
OrderProcessor -> EmailNotifier
Peale DIP rakendamist sõltuvad mõlemad osapooled ühisest vahelülist:
OrderProcessor -> Notifier <- EmailNotifier
Konkreetse sõltuvuse suund on pööratud.
EmailNotifier sõltub nüüd abstraktsioonist (Notifier), mitte ei sõltu OrderProcessor otseselt EmailNotifier-ist.
Seos teiste printsiipide ja mõistetega
DIP on seotud järgmiste mõistetega
- DIP tugineb liidestele abstraktsiooni saavutamiseks.
- See kinnitab OCP-d - abstraktsioonist sõltudes on
OrderProcessorlaiendatav (uued teavitustüübid) ilma muutmiseta. - See kinnitab SRP-d -
OrderProcessorvastutab ainult tellimuste loogika eest. Teavituste edastamine on kellegi teise vastutusala.
Sõltuvuste süstimine (Dependency injection)
Pane tähele, et OrderProcessor ei loo enam ise oma sõltuvust - see antakse talle konstruktoris ette. Seda mustrit nimetatakse sõltuvuste süstimiseks (dependency injection).
Ülaltoodud näites loob kutsuja EmailNotifier-i käsitsi ja annab selle edasi.
Suuremates süsteemides automatiseerib selle sõltuvuste süstimise raamistik (dependency injection framework).
Sõltuvuste süstimist käsitletakse lähemalt dependency injection peatükis, kus näed, kuidas see muster muudab koodi märkimisväärselt lihtsamini testitavaks.
Kui klass loob ise mingit sõltuvust new SomeConcreteClass() kaudu, küsi endalt, kas seda konkreetset klassi võib kunagi olla vaja muuta või testides asendada.
Kui vastus on jah, kaalu liidese eraldamist ja sõltuvuse süstimist.