Strategy – wzorzec projektowy

Przeznaczenie

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");

painter.setPicture(new BrushPaint());
painter.paint();

painter.setPicture(new CrayonPaint());
painter.paint();
}
}

Podsumowanie

Wzorzec Strategy:

  • Pozwala definiować wiele algorytmów (strategii) i stosować je zamiennie bez modyfikowania klienta.
  • Promuje zasadę kompozycji zamiast dziedziczenia.
  • Ułatwia utrzymanie i rozwój kodu, eliminując złożone instrukcje warunkowe.
  • Dobrze sprawdza się, gdy obiekty mogą zmieniać zachowanie w czasie działania programu.

Factory – wzorzec projektowy

Przeznaczenie

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 Document(String title, String author, String content) {
this.title = title;
this.author = author;
this.content = content;
}

public String getTitle() {
return title;
}

public String getContent() {
return content;
}

public String getAuthor() {
return author;
}
}
public class WordDocument extends Document{
public WordDocument(String title, String author, String content) {
super(title, author, content);
}
}
public class PdfDocument extends Document{
public PdfDocument(String title, String author, String content) {
super(title, author, content);
}
}
public class Main {
public static void main(String[] args) {
Document textDocument = new WordDocument("Wzorce projektowe", "Sebastian", "Zawartość dokumentu Word");
Document pdfDocument = new PdfDocument("Wzorce projektowe", "Sebastian", "Zawartość dokumentu PDF");
}
}

Metoda fabrykująca (Factory Method) – schemat działania

Struktura:

Kod:

public abstract class Document {
private String title;
private String author;
private DocumentType type;
private String content;

public Document(String title, String author, DocumentType type, String content) {
this.title = title;
this.author = author;
this.type = type;
this.content = content;
}

public String getTitle() {
return title;
}

public String getContent() {
return content;
}

public String getAuthor() {
return author;
}

public DocumentType getType() {
return type;
}

@Override
public String toString() {
return "Document{" +
"title='" + title + '\'' +
", author='" + author + '\'' +
", type=" + type +
", content='" + 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();

Document wordDocument = factory.createDocument(DocumentType.WORD);
Document pdfDocument = factory.createDocument(DocumentType.PDF);

System.out.println(wordDocument);
System.out.println(pdfDocument);

}
}

Fabryka abstrakcyjna (Abstract Factory) – schemat działania

Struktura:

Kod:

public abstract class Document {
private String title;
private String author;
private DocumentType type;
private String content;

public Document(String title, String author, DocumentType type, String content) {
this.title = title;
this.author = author;
this.type = type;
this.content = content;
}

public String getTitle() {
return title;
}

public String getContent() {
return content;
}

public String getAuthor() {
return author;
}

public DocumentType getType() {
return type;
}

@Override
public String toString() {
return "Document{" +
"title='" + title + '\'' +
", author='" + author + '\'' +
", type=" + type +
", content='" + 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 WordDocument createWordDocument();
abstract public PdfDocument createPdfDocument();
}
public class RedFactory extends Factory {
@Override
public WordDocument createWordDocument() {
return new WordDocument("Word Czerwony", "Sebastian", DocumentType.WORD, "Zawartość dokumentu Word");
}

@Override
public PdfDocument createPdfDocument() {
return new PdfDocument("Pdf Czerwony", "Sebastian", DocumentType.PDF, "Zawartość dokumentu PDF");
}
}
public class BlueFactory extends Factory {
@Override
public WordDocument createWordDocument() {
return new WordDocument("Word Niebieski", "Sebastian", DocumentType.WORD, "Zawartość dokumentu Word");
}

@Override
public PdfDocument createPdfDocument() {
return new PdfDocument("Pdf Niebieski", "Sebastian", DocumentType.PDF, "Zawartość dokumentu PDF");
}
}
public class Main {
public static void main(String[] args) {
Factory blueFactory = new BlueFactory();
Factory redFactory = new RedFactory();

WordDocument blueWord = blueFactory.createWordDocument();
PdfDocument bluePdf = blueFactory.createPdfDocument();

WordDocument redWord = redFactory.createWordDocument();
PdfDocument redPdf = redFactory.createPdfDocument();

System.out.println(blueWord);
System.out.println(bluePdf);
System.out.println(redWord);
System.out.println(redPdf);
}
}

Podsumowanie

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 – wzorzec projektowy

Przeznaczenie

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 Car(String brand, String model, int year, String color, String fuelType, double engineSize, int horsepower, String transmission, int mileage, boolean isElectric) {
this.brand = brand;
this.model = model;
this.year = year;
this.color = color;
this.fuelType = fuelType;
this.engineSize = engineSize;
this.horsepower = horsepower;
this.transmission = transmission;
this.mileage = mileage;
this.isElectric = isElectric;
}

public Car(String brand, String model, int year) {
this.brand = brand;
this.model = model;
this.year = year;
}

public Car(String brand, String model, int year, String color, String fuelType, double engineSize, int horsepower, String transmission, int mileage) {
this.brand = brand;
this.model = model;
this.year = year;
this.color = color;
this.fuelType = fuelType;
this.engineSize = engineSize;
this.horsepower = horsepower;
this.transmission = transmission;
this.mileage = mileage;
}


public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public String getModel() {
return model;
}

public void setModel(String model) {
this.model = model;
}

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

public String getFuelType() {
return fuelType;
}

public void setFuelType(String fuelType) {
this.fuelType = fuelType;
}

public double getEngineSize() {
return engineSize;
}

public void setEngineSize(double engineSize) {
this.engineSize = engineSize;
}

public int getHorsepower() {
return horsepower;
}

public void setHorsepower(int horsepower) {
this.horsepower = horsepower;
}

public String getTransmission() {
return transmission;
}

public void setTransmission(String transmission) {
this.transmission = transmission;
}

public int getMileage() {
return mileage;
}

public void setMileage(int mileage) {
this.mileage = mileage;
}

public boolean isElectric() {
return isElectric;
}

public void setElectric(boolean electric) {
isElectric = electric;
}

@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", model='" + model + '\''+
", year=" + year +
", color='" + color + '\'' +
", fuelType='" + fuelType + '\'' +
", engineSize=" + engineSize +
", horsepower=" + horsepower +
", transmission='" + transmission + '\'' +
", mileage=" + mileage +
", isElectric=" + 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;

private Car(CarBuilder carBuilder) {
this.brand = carBuilder.brand;
this.model = carBuilder.model;
this.year = carBuilder.year;
this.color = carBuilder.color;
this.fuelType = carBuilder.fuelType;
this.engineSize = carBuilder.engineSize;
this.horsepower = carBuilder.horsepower;
this.transmission = carBuilder.transmission;
this.mileage = carBuilder.mileage;
this.isElectric = carBuilder.isElectric;
}

public String getBrand() {
return brand;
}

public String getModel() {
return model;
}

public int getYear() {
return year;
}

public String getFuelType() {
return fuelType;
}

public String getColor() {
return color;
}

public double getEngineSize() {
return engineSize;
}

public int getHorsepower() {
return horsepower;
}

public String getTransmission() {
return transmission;
}

public int getMileage() {
return mileage;
}

public boolean isElectric() {
return isElectric;
}

@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", model='" + model + '\'' +
", year=" + year +
", color='" + color + '\'' +
", fuelType='" + fuelType + '\'' +
", engineSize=" + engineSize +
", horsepower=" + horsepower +
", transmission='" + transmission + '\'' +
", mileage=" + mileage +
", isElectric=" + isElectric +
'}';
}

public static class CarBuilder {
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 CarBuilder buildBrand(String brand) {
this.brand = brand;
return this;
}

public CarBuilder buildModel(String model) {
this.model = model;
return this;
}

public CarBuilder buildYear(int year) {
this.year = year;
return this;
}

public CarBuilder buildColor(String color) {
this.color = color;
return this;
}

public CarBuilder buildFuelType(String fuelType) {
this.fuelType = fuelType;
return this;
}

public CarBuilder buildEngineSize(double engineSize) {
this.engineSize = engineSize;
return this;
}

public CarBuilder buildHorsePower(int horsepower) {
this.horsepower = horsepower;
return this;
}

public CarBuilder buildTransmission(String transmission) {
this.transmission = transmission;
return this;
}

public CarBuilder buildMileage(int mileage) {
this.mileage = mileage;
return this;
}

public CarBuilder buildIsElectric(boolean isElectric) {
this.isElectric = isElectric;
return this;
}

public Car build() {
return new Car(this);
}
}
}
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();

System.out.println(car);
}
}

Builder wersja klasyczna – schemat działania

Struktura:

Kod:

public interface CarBuilder {
void buildBrand();
void buildModel();
void buildYear();
void buildColor();
void buildFuelType();
void buildEngineSize();
void buildHorsePower();
void buildTransmission();
void buildMileage();
void buildIsElectric();

Car getCar();
}
public class CarDirector {
private CarBuilder carBuilder;

public CarDirector(CarBuilder carBuilder) {
this.carBuilder = carBuilder;
}

public void buildCar() {
carBuilder.buildBrand();
carBuilder.buildModel();
carBuilder.buildYear();
carBuilder.buildColor();
carBuilder.buildFuelType();
carBuilder.buildEngineSize();
carBuilder.buildHorsePower();
carBuilder.buildTransmission();
carBuilder.buildMileage();
carBuilder.buildIsElectric();
}

public Car getCar() {return this.carBuilder.getCar();}

}
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 String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public String getModel() {
return model;
}

public void setModel(String model) {
this.model = model;
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public String getFuelType() {
return fuelType;
}

public void setFuelType(String fuelType) {
this.fuelType = fuelType;
}

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

public int getHorsepower() {
return horsepower;
}

public void setHorsepower(int horsepower) {
this.horsepower = horsepower;
}

public double getEngineSize() {
return engineSize;
}

public void setEngineSize(double engineSize) {
this.engineSize = engineSize;
}

public String getTransmission() {
return transmission;
}

public void setTransmission(String transmission) {
this.transmission = transmission;
}

public int getMileage() {
return mileage;
}

public void setMileage(int mileage) {
this.mileage = mileage;
}

public boolean isElectric() {
return isElectric;
}

public void setElectric(boolean electric) {
isElectric = electric;
}

@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", model='" + model + '\'' +
", year=" + year +
", color='" + color + '\'' +
", fuelType='" + fuelType + '\'' +
", engineSize=" + engineSize +
", horsepower=" + horsepower +
", transmission='" + transmission + '\'' +
", mileage=" + mileage +
", isElectric=" + isElectric +
'}';
}
}
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();

System.out.println(passengerCar);
System.out.println(vanCar);
}
}

Podsumowanie


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.

SOLID – Zasada inwersji zależności

Definicja

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 Switch nie 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.

SOLID – Zasada segregacji interfejsów

Definicja

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

Przykład programu przed wdrożeniem LSP

public interface Document {
    void open();
    void save();
    void print();
    void sendViaEmail();
}
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

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)

SOLID – Zasada podstawienia Liskov

Definicja

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.

SOLID – Zasada otwarte/zamknięte

Definicja

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.

SOLID – Zasada pojedynczej odpowiedzialności

Definicja

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