Umożliwia zdefiniowanie rodziny algorytmów, umieszczenie ich w osobnych klasach oraz ich wymienne stosowanie. Pozwala to zmieniać sposób działania obiektu bez ingerencji w jego kod.
Dlaczego go potrzebujemy ?
Eliminuje duplikację kodu i rozbudowane instrukcje warunkowe
Ułatwia zmianę zachowania w czasie działania programu
Izoluje zmieniające się części kodu
Ułatwia testowanie i rozwój
Poniżej przykładowy kod przed użyciem wzorca Strategy:
public class Painter { private String name;
public Painter(String name) { this.name = name; }
public void paint() { System.out.println("Maluję kredkami"); }
public String getName() { return name; }
public void setName(String name) { this.name = name; } }
public class Main { public static void main(String[] args) { Painter painter = new Painter("Jan Matejko"); painter.paint(); } }
Strategy – schemat działania
Struktura:
Kod:
public interface Paint { void paint(); }
public class CrayonPaint implements Paint { @Override public void paint() { System.out.println("Maluje kredkami"); } }
public class BrushPaint implements Paint { @Override public void paint() { System.out.println("Maluje farbami"); } }
public class Painter { private String name; private Paint picture;
public Painter(String name) { this.name = name; }
public void paint() { this.picture.paint(); }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Paint getPicture() { return picture; }
public void setPicture(Paint picture) { this.picture = picture; } }
public class Main { public static void main(String[] args) { Painter painter = new Painter("Jan Matejko");
Głównym założeniem fabryki, tak jak w rzeczywistym świecie, jest wytwarzanie obiektów. Dzięki wykorzystaniu fabryki można ukryć szczegóły implementacyjne tworzenia obiektów i odseparować je od logiki biznesowej.
Dlaczego go potrzebujemy ?
mamy wiele klas, które dziedziczą po tej samej klasie bazowej lub implementują ten sam interfejs
chcemy uniknąć bezpośredniego tworzenia obiektów przez kod klienta
chcemy odizolować logikę tworzenia obiektów od reszty kodu
chcemy ułatwić testowanie
Poniżej przykładowy kod przed użyciem wzorca Factory:
public abstract class Document { private String title; private String author; private String content;
public class PdfDocument extends Document{ public PdfDocument(String title, String author, DocumentType type, String content) { super(title, author, type, content); } }
public class WordDocument extends Document{ public WordDocument(String title, String author,DocumentType type, String content) { super(title, author, type, content); } }
public enum DocumentType { WORD, PDF }
abstract public class Factory { abstract public Document createDocument(DocumentType type); }
public class DocumentFactory extends Factory { @Override public Document createDocument(DocumentType documentType) { switch (documentType) { case WORD: return new WordDocument("Wzorce projektowe","Sebastian", documentType,"Zawartość dokumentu Word"); case PDF: return new PdfDocument("Wzorce projektowe","Sebastian",documentType,"Zawartość dokumentu PDF"); default: throw new UnsupportedOperationException("No such type"); } } }
public class Main { public static void main(String[] args) { Factory factory = new DocumentFactory();
Wzorzec Factory (oraz jego rozszerzenie – Abstract Factory) pozwala oddzielić logikę tworzenia obiektów od logiki aplikacji, dzięki czemu kod staje się bardziej przejrzysty, łatwiejszy w testowaniu i utrzymaniu.
Dzięki zastosowaniu fabryk:
Ukrywamy szczegóły implementacyjne tworzenia obiektów
Unikamy powtarzania kodu konstrukcyjnego
Możemy dynamicznie decydować, jaką klasę stworzyć
Łatwiej modyfikować i rozwijać aplikację bez naruszania istniejącej logiki
Wersja Factory Method dobrze sprawdza się, gdy mamy jedną rodzinę klas, a Abstract Factory – gdy potrzebujemy tworzyć różne zestawy współpracujących obiektów.
To podejście pozwala pisać czystszy, skalowalny i bardziej elastyczny kod, zgodny z zasadami SOLID.
Builder to kreacyjny wzorzec projektowy, którego celem jest oddzielenie procesu tworzenia obiektu od jego reprezentacji. Umożliwia budowanie złożonych obiektów krok po kroku, a także eliminuje potrzebę stosowania wielu przeciążonych konstruktorów. Sprawdza się szczególnie dobrze w przypadku klas posiadających wiele opcjonalnych pól, ponieważ umożliwia tworzenie obiektów w sposób bardziej przejrzysty i elastyczny.
Dlaczego go potrzebujemy ?
W sytuacjach, gdy:
mamy wiele pól (niektóre opcjonalne),
klasa posiada wiele konstruktorów, co utrudnia ich rozróżnienie i użycie,
kolejność parametrów jest łatwa do pomylenia,
chcemy zwiększyć czytelność i bezpieczeństwo kodu przy tworzeniu obiektów —
wzorzec Builder pozwala nam zminimalizować ryzyko błędów i zwiększyć czytelność kodu. Ułatwia również dodawanie nowych pól w przyszłości, bez potrzeby modyfikowania wielu konstruktorów.
Poniżej przykładowy kod przed użyciem wzorca Builder:
public class Car { private String brand; private String model; private int year; private String color; private String fuelType; private double engineSize; private int horsepower; private String transmission; private int mileage; private boolean isElectric;
public class Main { public static void main(String[] args) { Car car = new Car("Fiat", "Panda" ,2009, "red", "gas", 1.1, 65, "manual", 85000, false); System.out.println(car); } }
Builder z klasą wewnętrzną – schemat działania
Struktura:
Kod:
public class Car { private String brand; private String model; private int year; private String color; private String fuelType; private double engineSize; private int horsepower; private String transmission; private int mileage; private boolean isElectric;
public class Main { public static void main(String[] args) { Car car = new Car.CarBuilder() .buildBrand("Fiat") .buildModel("Panda") .buildYear(2009) .buildColor("red") .buildFuelType("gas") .buildEngineSize(1.1) .buildHorsePower(60) .buildTransmission("manual") .buildMileage(85000) .buildIsElectric(false) .build();
public class PassengerCarBuilder implements CarBuilder { private Car car;
public PassengerCarBuilder() { this.car = new Car(); }
@Override public void buildBrand() { this.car.setBrand("Fiat"); }
@Override public void buildModel() { this.car.setModel("Panda"); }
@Override public void buildYear() { this.car.setYear(2009); }
@Override public void buildColor() { this.car.setColor("red"); }
@Override public void buildFuelType() { this.car.setFuelType("gas"); }
@Override public void buildEngineSize() { this.car.setEngineSize(1.1); }
@Override public void buildHorsePower() { this.car.setHorsepower(60); }
@Override public void buildTransmission() { this.car.setTransmission("manual"); }
@Override public void buildMileage() { this.car.setMileage(85000); }
@Override public void buildIsElectric() { this.car.setElectric(false); }
@Override public Car getCar() { return car; } }
public class VanCarBuilder implements CarBuilder {
private Car car;
public VanCarBuilder() {
this.car = new Car();
}
@Override
public void buildBrand() {
this.car.setBrand("Fiat");
}
@Override
public void buildModel() {
this.car.setModel("Panda");
}
@Override
public void buildYear() {
this.car.setYear(2009);
}
@Override
public void buildColor() {
this.car.setColor("red");
}
@Override
public void buildFuelType() {
this.car.setFuelType("gas");
}
@Override
public void buildEngineSize() {
this.car.setEngineSize(1.1);
}
@Override
public void buildHorsePower() {
this.car.setHorsepower(60);
}
@Override
public void buildTransmission() {
this.car.setTransmission("manual");
}
@Override
public void buildMileage() {
this.car.setMileage(85000);
}
@Override
public void buildIsElectric() {
this.car.setElectric(false);
}
@Override
public Car getCar() {
return car;
}
}
public class Main { public static void main(String[] args) { PassengerCarBuilder passengerCarBuilder = new PassengerCarBuilder(); VanCarBuilder vanCarBuilder = new VanCarBuilder();
CarDirector passengerCarDirector = new CarDirector(passengerCarBuilder); passengerCarDirector.buildCar();
CarDirector varCarDirector = new CarDirector(vanCarBuilder); varCarDirector.buildCar();
Car passengerCar = passengerCarDirector.getCar(); Car vanCar = vanCarBuilder.getCar();
Wzorzec Builder stosujemy przede wszystkim wtedy, gdy klasa ma wiele pól, a tworzenie wielu konstruktorów byłoby niepraktyczne i nieczytelne. Builder pozwala na wygodne i bezpieczne konstruowanie obiektów krok po kroku, bez konieczności udostępniania setterów, dzięki czemu po utworzeniu obiektu jego stan pozostaje niezmienny.
Wariant z klasą wewnętrzną umożliwia użytkownikowi elastyczne i czytelne tworzenie obiektu, umożliwiając ustawienie tylko tych pól, które są potrzebne. Po zbudowaniu obiektu dalsza modyfikacja jest niemożliwa.
Wersja klasyczna Buildera, wykorzystująca dyrektora i osobne klasy budownicze, sprawdza się, gdy chcemy precyzyjnie kontrolować proces tworzenia obiektu i oddzielić go od użytkownika — dyrektor decyduje, w jaki sposób obiekt zostanie zbudowany, a użytkownik nie musi się tym zajmować.
Builder to popularny wzorzec projektowy, który znacząco poprawia czytelność, elastyczność i bezpieczeństwo kodu przy tworzeniu złożonych obiektów. W praktyce często spotyka się jego różne wariacje, ale charakterystyczne cechy pozostają niezmienne — stopniowe budowanie obiektu i oddzielenie konstrukcji od reprezentacji.
Zasada odwrócenia zależności (DIP – Dependency Inversion Principle) to jedna z pięciu zasad projektowych SOLID. Jej kluczowe założenia to:
Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Oba powinny zależeć od abstrakcji.
Abstrakcje nie powinny zależeć od szczegółów. To szczegóły powinny zależeć od abstrakcji.
Dzięki zastosowaniu DIP logika aplikacji jest odseparowana od szczegółowych implementacji, co zwiększa jej elastyczność, testowalność oraz ułatwia rozbudowę.
Przykład programu przed wdrożeniem DIP
public class LightBulb {
public void turnOn() {
System.out.println("LightBulb is turned on");
}
public void turnOff() {
System.out.println("LightBulb is turned off");
}
}
public class Switch {
private LightBulb lightBulb;
public Switch(LightBulb lightBulb) {
this.lightBulb = lightBulb;
}
public void flip(boolean on) {
if (on) {
lightBulb.turnOn();
} else {
lightBulb.turnOff();
}
}
}
public class Main {
public static void main(String[] args) {
LightBulb lightBulb = new LightBulb();
Switch lightSwitch = new Switch(lightBulb);
lightSwitch.flip(true);
lightSwitch.flip(false);
}
}
W powyższym przykładzie klasa Switch jest ściśle powiązana z klasą LightBulb. Oznacza to, że każda zmiana w implementacji LightBulb może wymagać modyfikacji klasy Switch. Taki kod jest trudny w utrzymaniu i mało elastyczny.
Kod po zastosowaniu zasady DIP
public interface Switchable {
void turnOn();
void turnOff();
}
public class LightBulb implements Switchable{
@Override
public void turnOn() {
System.out.println("LightBulb is turned on");
}
@Override
public void turnOff() {
System.out.println("LightBulb is turned off");
}
}
public class Switch {
private Switchable switchable;
public Switch(Switchable switchable) {
this.switchable = switchable;
}
public void flip(boolean on) {
if(on) {
switchable.turnOn();
} else {
switchable.turnOff();
}
}
}
public class Main {
public static void main(String[] args) {
Switchable lightBulb = new LightBulb();
Switch lightSwitch = new Switch(lightBulb);
lightSwitch.flip(true);
lightSwitch.flip(false);
}
}
Podsumowanie
Dzięki zastosowaniu DIP:
Klasa Switchnie zależy od konkretnej implementacji (LightBulb), lecz od interfejsu (Switchable).
Można łatwo podmienić LightBulb na inną klasę, np. Fan, Heater, bez zmian w klasie Switch.
Kod jest bardziej elastyczny, testowalny i łatwiejszy w utrzymaniu.
Reguła ta oznajmia nam, aby nie tworzyć interfejsów z metodami, których nie używa klasa. Interfejsy powinny być jak najmniejsze i konkretne klasy nie powinny implementować metod których nie potrzebują. Nie powinno dojść do sytuacji, gdy któraś z klas pochodnych nie wykorzystuje zaimplementowanej w interfejsie metody. Wyobraźmy sobie, że mamy interfejs, który jest używany w kilkunastu innych projektach, jednak w każdym projekcie używana jest tylko jedna metoda tego interfejsu. Gdy zajdzie potrzeba zmiany tego interfejsu spotkamy się z problemem ingerowania w każdy projekt. Nie powinniśmy zmuszać klasy do implementowania metod, których nie potrzebuje. Lepiej zdefiniować większą liczbę małych i lekkich interfejsów
public class PDFDocument implements Document{
@Override
public void open() {
System.out.println("Logic for opening a PDF document");
}
@Override
public void save() {
System.out.println("Logic for saving a PDF document");
}
@Override
public void print() {
System.out.println("Logic for printing a PDF document");
}
@Override
public void sendViaEmail() {
System.out.println("Logic for sending a PDF document via email");
}
}
public class WordDocument implements Document{
@Override
public void open() {
System.out.println("Logic for opening a Word document");
}
@Override
public void save() {
System.out.println("Logic for saving a Word document");
}
@Override
public void print() {
System.out.println("Logic for printing a Word document");
}
@Override
public void sendViaEmail() {
System.out.println("Logic for sending a Word document via email");
}
}
Aby zastosować zasadę segregacji interfejsu, musimy podzielić interfejs Document na mniejsze, bardziej szczegółowe interfejsy. Pozwala to klasom implementować tylko to, czego potrzebują. Zobaczmy, jak możemy to zrobić:
Kod po zastosowaniu zasady ISP
public interface OpenSave {
void open();
void save();
}
public interface Print {
void print();
}
public interface SendViaEmail {
void sendViaEmail();
}
public class PDFDocument implements OpenSave, Print{
@Override
public void open() {
System.out.println("Logic for opening a PDF document");
}
@Override
public void save() {
System.out.println("Logic for saving a PDF document");
}
@Override
public void print() {
System.out.println("Logic for printing a PDF document");
}
}
public class WordDocument implements OpenSave, SendViaEmail{
@Override
public void open() {
System.out.println("Logic for opening a Word document");
}
@Override
public void save() {
System.out.println("Logic for saving a Word document");
}
@Override
public void sendViaEmail() {
System.out.println("Logic for sending a Word document via email");
}
}
Podsumowanie
Dzięki tej nowej strukturze każda klasa implementuje tylko interfejsy, których potrzebuje, unikając konieczności implementowania nieistotnych metod. Dzięki temu nasz kod jest bardziej przejrzysty i łatwiejszy w utrzymaniu
SOLID to akronim składający się z pierwszych liter pięciu podstawowych zasad programowania obiektowego. Ich stosowanie pozwala tworzyć kod, który jest czytelny, łatwy do rozwijania i utrzymania. Oto te zasady:
S – Zasada pojedynczej odpowiedzialności (Single Responsibility Principle – SRP)
O – Zasada otwarte-zamknięte (Open/Closed Principle – OCP)
L – Zasada podstawiania Liskov (Liskov Substitution Principle – LSP)
I – Zasada segregacji interfejsu (Interface Segregation Principle – ISP)
D – Zasada odwrócenia zależności (Dependency Inversion Principle – DIP)
Zasada podstawienia Liskov (Liskov Substitution Principle – LSP) mówi, że obiekty klasy pochodnej powinny być w stanie zastąpić obiekty klasy bazowej bez zmiany poprawności działania programu. Oznacza to, że podklasa musi przestrzegać kontraktów swojej klasy bazowej – nie może łamać jej oczekiwań ani gwarancji.
Przykład programu przed wdrożeniem LSP
public class Employee {
public void CalculateSalary() {
System.out.println("Calculate for Employee");
}
public void ShowIdCard() {
System.out.println("Show card Employee");
}
}
public class Ceo extends Employee {
@Override
public void CalculateSalary() {
System.out.println("Calculate for Ceo");
}
@Override
public void ShowIdCard() {
System.out.println("Show card Ceo");
}
}
public class Programmer extends Employee {
@Override
public void CalculateSalary() {
System.out.println("Calculate for Programmer");
}
@Override
public void ShowIdCard() {
System.out.println("Show card Programmer");
}
}
public class Volunteer extends Employee {
@Override
public void CalculateSalary() {
throw new UnsupportedOperationException("Volunteers do not receive a
salary");
}
}
@Override
public void ShowIdCard() {
System.out.println("Show card Volunteer");
}
}
public class Finances {
public void PaySalaries(Employee[] employees) {
for(Employee employee : employees) {
employee.CalculateSalary();
}
}
}
public class Hr {
public void GetIdCards(Employee[] employees) {
for(Employee employee : employees) {
employee.ShowIdCard();
}
}
}
public class Program {
public static void main(String[] args) {
Finances finances = new Finances();
Hr hr = new Hr();
Employee[] employees = new Employee[] {new Programmer(), new Ceo(),
new Volunteer()};
finances.PaySalaries(employees);
hr.GetIdCards(employees);
}
}
Klasa Volunteer rozszerza klasę Employee, ale nie spełnia kontraktu – metoda CalculateSalary() rzuca wyjątek, co łamie oczekiwania względem klasy Employee. Oznacza to, że Volunteer nie może być bezpiecznie użyta tam, gdzie oczekuje się Employee.
Kod po zastosowaniu zasady LSP
Zastosujemy interfejsy dla rozdzielenia zachowań: tylko niektórzy pracownicy są opłacani, ale wszyscy mogą mieć identyfikator.
public interface IPayableEmployee {
void CalculateSalary();
}
public interface IVisitor {
void ShowIdCard();
}
public class Ceo implements IVisitor, IPayableEmployee{
@Override
public void CalculateSalary() {
System.out.println("Calculate for Ceo");
}
@Override
public void ShowIdCard() {
System.out.println("Show card Ceo");
}
}
public class Programmer implements IVisitor, IPayableEmployee{
@Override
public void CalculateSalary() {
System.out.println("Calculate for Programmer");
}
@Override
public void ShowIdCard() {
System.out.println("Show card Programmer");
}
}
public class Volunteer implements IVisitor{
@Override
public void ShowIdCard() {
System.out.println("Show card Volunteer");
}
}
public class Finances {
public void PaySalaries(IPayableEmployee[] iPayableEmployees) {
for(IPayableEmployee iPayableEmployee : iPayableEmployees) {
iPayableEmployee.CalculateSalary();
}
}
}
public class Hr {
public void GetIdCards(IVisitor[] iVisitors) {
for(IVisitor iVisitor : iVisitors) {
iVisitor.ShowIdCard();
}
}
}
public class Program {
public static void main(String[] args) {
Finances finances = new Finances();
Hr hr = new Hr();
Ceo ceo = new Ceo();
Programmer programmer = new Programmer();
Volunteer volunteer = new Volunteer();
IPayableEmployee[] iPayableEmployees = new IPayableEmployee[] {ceo,
programmer};
IVisitor[] iVisitors = new IVisitor[] {ceo, programmer, volunteer};
finances.PaySalaries(iPayableEmployees);
hr.GetIdCards(iVisitors);
}
}
Podsumowanie
Stosując zasadę podstawienia Liskov:
Projektujemy hierarchie klas, które są bezpiecznie zamienne.
Oddzielamy zachowania za pomocą interfejsów (np. IPayableEmployee, IVisitor).
Unikamy sytuacji, w których podklasy łamią oczekiwania klasy bazowej.
W przykładzie, poprzez zastosowanie interfejsów, program staje się bardziej odporny na błędy i łatwiejszy do rozwijania.
Zasada OCP (Open/Closed Principle) mówi, że klasy powinny być otwarte na rozszerzanie, ale zamknięte na modyfikacje. Oznacza to, że podczas projektowania klas należy zadbać o możliwość ich rozbudowy w przyszłości bez konieczności ingerowania w ich istniejący kod.
Wprowadzanie zmian do już istniejących klas wiąże się z ryzykiem – nie zawsze wiadomo, jak takie zmiany wpłyną na pozostałą część systemu. Dlatego zamiast modyfikować istniejące klasy, lepiej jest je rozszerzać.
W praktyce zasadę OCP można realizować za pomocą:
dziedziczenia (tworząc podklasy z nowym zachowaniem),
interfejsów (dzięki którym różne implementacje mogą być traktowane jednakowo),
wzorów projektowych
Dzięki temu kod staje się bardziej elastyczny, łatwiejszy w utrzymaniu i odporny na błędy wynikające z nieprzemywanych modyfikacji.
Przykład programu przed wdrożeniem OCP
public class Ceo {
public void CalculateSalary() {
System.out.println("Pay Ceo");
}
public void ShowIdCard() {
System.out.println("Greet Ceo");
}
}
public class Programmer {
public void CalculateSalary() {
System.out.println("Pay Programmer");
}
public void ShowIdCard() {
System.out.println("Greet Programmer");
}
}
public class Finances {
public void CalculateSalaries(Object[] obiekty) {
for (int i = 0; i < obiekty.length; i++) {
if (obiekty[i] instanceof Ceo) {
Ceo ceo = (Ceo) obiekty[i];
ceo.CalculateSalary();
}
if (obiekty[i] instanceof Programmer) {
Programmer programmer = (Programmer) obiekty[i];
programmer.CalculateSalary();
}
}
}
}
public class Hr {
public void ShowIdCards(Object[] obiekty) {
for (int i = 0; i < obiekty.length; i++) {
if (obiekty[i] instanceof Ceo) {
Ceo ceo = (Ceo) obiekty[i];
ceo.ShowIdCard();
}
if (obiekty[i] instanceof Programmer) {
Programmer programmer = (Programmer) obiekty[i];
programmer.ShowIdCard();
}
}
}
}
public class Program {
public static void main(String[] args) {
Finances finances = new Finances();
Hr hr = new Hr();
Object[] obiekty = new Object[] {new Ceo(), new Programmer()};
finances.CalculateSalaries(obiekty);
hr.ShowIdCards(obiekty);
}
}
Gdy do aplikacji będzie trzeba w przyszłości dodać np. Księgowego utworzone w ten sposób klasy CalculateSalaries, ShowIdCard będzie trzeba zmodyfikować. Lepszym rozwiązaniem zgodnym z zasadą OCP jest zastosowanie np. implementacji z interfejsu. Rozwiązanie to pozwala na dodawanie kolejnych stanowisk pracy, gdy będzie taka potrzeba.
Kod po zastosowaniu OCP
public interface IEmployee {
void CalculateSalary();
void ShowIdCard();
}
public class Ceo implements IEmployee {
@Override
public void CalculateSalary() {
System.out.println("Pay Ceo");
}
@Override
public void ShowIdCard() {
System.out.println("Greet Ceo");
}
}
public class Programmer implements IEmployee {
@Override
public void CalculateSalary() {
System.out.println("Pay Programmer");
}
@Override
public void ShowIdCard() {
System.out.println("Greet Programmer");
}
}
public class Accountant implements IEmployee {
@Override
public void CalculateSalary() {
System.out.println("Pay Accountant");
}
@Override
public void ShowIdCard() {
System.out.println("Greet Accountant");
}
}
public class Finances {
public void CalculateSalaries(IEmployee[] employees) {
for (IEmployee employee : employees) {
employee.CalculateSalary();
}
}
}
public class Hr {
public void ShowIdCards(IEmployee[] employees) {
for(IEmployee employee : employees) {
employee.ShowIdCard();
}
}
}
public class Program {
public static void main(String[] args) {
Finances finances = new Finances();
Hr hr = new Hr();
IEmployee[] employees = new IEmployee[] {new Ceo(), new Programmer(),
new Accountant()};
finances.CalculateSalaries(employees);
hr.ShowIdCards(employees);
}
}
Podsumowanie
Zastosowanie zasady OCP sprawia, że kod jest bardziej odporny na błędy, łatwiej się rozwija i nie wymaga modyfikacji już przetestowanych klas. To podstawa skalowalnych i solidnych aplikacji.
Zasada pojedynczej odpowiedzialności (SRP – Single Responsibility Principle)
Zasada SRP mówi, że klasa (lub moduł) powinna mieć tylko jeden powód do zmiany, czyli powinna odpowiadać tylko za jedno konkretne zadanie lub odpowiedzialność w systemie.
Innymi słowy, każda klasa powinna zajmować się tylko jednym aspektem funkcjonalności aplikacji. Jeśli klasa ma wiele odpowiedzialności, zmiana w jednej z nich może niechcący wpłynąć na inne — co prowadzi do trudności w utrzymaniu i testowaniu kodu.
Przykład programu przed wdrożeniem SRP
public class Assistant {
public int retirementAge = 65;
public int age;
public Assistant(int age) {
this.age = age;
}
public void HandleEmployee() {
System.out.println("Logging data...");
System.out.println(this.retirementAge - this.age);
}
}
Klasa Assistant w powyższym kodzie ma dwie funkcje: logowanie danych, obliczanie ile lat zostało do emerytury. Zgodnie z zasadą SRP, te odpowiedzialności powinny być rozdzielone. Proponuje utworzyć trzy klasy: Assistant, FinancesAssistant, Logger.
Kod po zastosowaniu zasady SRP
public class Assistant {
public FinancesAssistant financesAssistant;
public Logger logger;
public Assistant(int age) {
this.financesAssistant = new FinancesAssistant(30);
this.logger = new Logger();
}
public void HandleEmployee() {
this.logger.Log();
this.financesAssistant.Calculate();
}
}
public class FinancesAssistant {
public int retirementAge = 65;
public int age;
public FinancesAssistant(int age) {
this.age = age;
}
public void Calculate() {
System.out.println(this.retirementAge - this.age);
}
}
public class Logger {
public void Log() {
System.out.println("Logging data...");
}
}
Podsumowanie
Dzięki rozdzieleniu odpowiedzialności każda klasa ma teraz jeden jasno określony cel. Taki kod jest łatwiejszy do testowania, modyfikowania i utrzymania w dłuższej perspektywie. To właśnie jest sedno zasady pojedynczej odpowiedzialności