Wzorzec projektowy – Factory

Czym jest wzorzec projektowy factory?

Najprościej omówić zasadę działania wzorca factory na podstawie przykładu, oraz zaprezentowania jakie problemy pomaga nam rozwiązać.

Załóżmy, że jesteśmy podwykonawcą dużych marek motoryzacyjnych. Składamy dla nich samochody, motory, oraz inne pojazdy.

Na potrzeby przykładu skorzystamy z języka Java, więc zaczynamy standardowo:

public class Main {
    public static void main(String[] args) {}
}

Stwórzmy klasę abstrakcyjną reprezentującą wszystkie pojazdy, po której będziemy dziedziczyć:

public abstract class Vehicle {

    private int engine;
    private int numberOfComponents;
    private int price;

    public Vehicle(int engine, int numberOfComponents, int price) {
        this.engine = engine;
        this.numberOfComponents = numberOfComponents;
        this.price = price;
    }

    public int getEngine() {
        return engine;
    }

    public int getNumberOfComponents() {
        return numberOfComponents;
    }

    public int getPrice() {
        return price;
    }
}

Stwórzmy klasę reprezentującą nasze samochody:

public class Car extends Vehicle {

    public Car(String engine, int numberOfComponents, int price) {
        super(engine, numberOfComponents, price);
    }
    
}

Analogicznie tworzymy klasę dla motocykli:

public class Motocycle extends Vehicle {

    public Motocycle(String engine, int numberOfComponents, int price) {
        super(engine, numberOfComponents, price);
    }
    
}

Końcowo w naszym Mainie powołujemy obiekty do życia:

public class Main {
    public static void main(String[] args) {

        Vehicle car = new Car("V8", 200, 50000);
        Vehicle motorcycle = new Motorcycle("V4", 150, 45000);

    }
}

Stworzyliśmy przykładowy samochód z silnikiem V8, złożony z 200 elementów który kosztuje 50 tyisięcy, oraz analogicznie poniżej przykładowy motor.

Problem

Przy dużej ilości wszelkiej maści pojazdów można zacząć się gubić podczas tworzenia, nie wiedząc dokładnie jakie parametry mają zostać przekazane, oraz samo wpisywanie parametrów za każdym razem przy tworzeniu nowego obiektu staje się strasznie czasochłonne.

Metoda Fabrykująca

Zastosujmy metodę fabrykującą do rozwiązania naszego problemu.

Stwórzmy klasę VehicleType która nam pomoże rozróżniać rodzaj pojazdów na przyszłość, wykorzystamy enum:

public enum VehicleType {
    CAR, MOTORCYCLE;
}

Stwórzmy szkielet który odziedziczy nasza fabryka pojazdów:

abstract public class Factory {
    abstract public Vehicle createVehicle(VehicleType type);
}

Tworzymy fabrykę właściwą VehicleFactory w której będzie już gotowy przepis jak nasze pojazdy mają wyglądać:

public class VehicleFactory extends Factory{

    @Override
    public Vehicle createVehicle(VehicleType vehicleType) {

        switch(vehicleType) {
            case CAR:
                return new Car("V8", 200, 50000);
            case MOTORCYCLE:
                return new Motorcycle("V4", 150, 45000);
            default:
                throw new UnsupportedOperationException("Invalid model");
        }
    }

}

Teraz użyjmy naszego wzorca tworząc te obiekty(pojazdy):

public class Main {
    public static void main(String[] args) {

        Factory factory = new VehicleFactory();
        
        //        Vehicles.Vehicle car = new Vehicles.Car("V8", 200, 50000);
        //        Vehicles.Vehicle motorcycle = new Vehicles.Motorcycle("V4", 150, 45000);

        Vehicle car = factory.createVehicle(VehicleType.CAR);
        Vehicle motorcycle = factory.createVehicle(VehicleType.MOTORCYCLE);

    }
}

Porównując zapis którego używaliśmy poprzednio do tego który mamy obecnie, widać że oddelegowaliśmy proces tworzenia obiektu do fabryki, nie przejmując się wysyłaniem parametrów, zwyczajnie powołujemy metodę createVehicle() w klasie factory wysyłając typ pojazdu który chcemy stworzyć, nic prostszego!

Mimo iż sam zapis jest niewiele krótszy od poprzedniego, należy pamiętać, że w ramach demonstracji, przykład którego użyliśmy jest bardzo prosty, choć już teraz widzimy potencjał jak łatwo rozszerzyć możliwości fabryki o kolejne, zupełnie nowe pojazdy w naszym asortymencie.

Problem #2

Mimo iż posiadamy już implementację metody fabrykującej, wyobraźmy sobie, że nawiązaliśmy współpracę z dwoma firmami przykładowo Honda i Suzuki, które posiadają różne parametry dla swoich samochodów, motocykli i innych pojazdów.

Zatem, spróbujmy rozszerzyć nasz asortyment, zmieniamy nazwę klasy abstrakcyjnej Vehicles na HondaVehicles:

public abstract class HondaVehicle {

    private String engine;
    private int numberOfComponents;
    private int price;

    protected HondaVehicle(String engine, int numberOfComponents, int price) {
        this.engine = engine;
        this.numberOfComponents = numberOfComponents;
        this.price = price;
    }

    public String getEngine() {
        return engine;
    }

    public int getNumberOfComponents() {
        return numberOfComponents;
    }

    public int getPrice() {
        return price;
    }
}

Dodajmy również klasę abstrakcyjną reprezentującą pojazdy Suzuki:

public abstract class SuzukiVehicle {

    private String engine;
    private int numberOfComponents;
    private int price;

    protected SuzukiVehicle(String engine, int numberOfComponents, int price) {
        this.engine = engine;
        this.numberOfComponents = numberOfComponents;
        this.price = price;
    }

    public String getEngine() {
        return engine;
    }

    public int getNumberOfComponents() {
        return numberOfComponents;
    }

    public int getPrice() {
        return price;
    }
}

Teraz nasza klasa Factory wymaga zmian, ze względu na pojawienie się dwóch różnych rodzajów marek, więc zmieniamy nazwę Factory na HondaFactory:

abstract public class HondaFactory {
    abstract public HondaVehicle createVehicle(VehicleType type);
}

Oraz dodajemy nową klasę abstrakcyjną SuzukiFactory:

abstract public class SuzukiFactory {
    abstract public SuzukiVehicle createVehicle(VehicleType type);
}

Zmiany muszę się również pojawić w VehicleFactory jako, że mamy różne konstrukcje dla różnych marek. Zmieniamy nazwę VehicleFactory na HondaVehicleFactory:

public class HondaVehicleFactory extends HondaFactory{

    @Override
    public HondaVehicle createVehicle(VehicleType vehicleType) {

        switch(vehicleType) {
            case CAR:
                return new Car("V8", 200, 50000);
            case MOTORCYCLE:
                return new Motorcycle("V4", 150, 45000);
            default:
                throw new UnsupportedOperationException("Invalid model");
        }
    }

}

Oraz dodajemy nową klasę SuzukiVehicleFactory dodając zmiany wynikające z innej marki w parametrach samochodu i motocykla:

public class SuzukiVehicleFactory extends HondaFactory{

    @Override
    public SuzukiVehicle createVehicle(VehicleType vehicleType) {

        switch(vehicleType) {
            case CAR:
                return new Car("V6", 240, 70000);
            case MOTORCYCLE:
                return new Motorcycle("V2", 170, 55000);
            default:
                throw new UnsupportedOperationException("Invalid model");
        }
    }

}

Widzimy, że doszło nam sporo klas, a to nie koniec, bo dajmy na to obydwie firmy chcą rozszerzyć działalność i chcą, żeby dodatkowo rozpocząć dla nich produkcję rowerów, gdzie znów aby sprostać temu zadaniu trzeba wyspecjalizować te fabryki i stworzyć dużą liczbę klas. Jak temu zaradzić? Należy wykorzystać wzorzec fabryki abstrakcyjnej.

Fabryka Abstrakcyjna

Dla lepszej przejrzystości porzućmy to co robiliśmy do tej pory i zacznijmy od nowa:

Main:

public class Main {
    public static void main(String[] args) {}
}

Car i Motorcycle:

public class Car extends Vehicle {

    Car(String engine, int numberOfComponents, int price) {
        super(engine, numberOfComponents, price);
    }

}
public class Motorcycle extends Vehicle {

    Motorcycle(String engine, int numberOfComponents, int price) {
        super(engine, numberOfComponents, price);
    }

}

Dodajmy nasz rower:

public class Bicycle extends SimpleVehicle {

    Bicycle(int numberOfComponents, int price) {
        super(numberOfComponents, price);
    }

}

VehicleType:

public enum VehicleType {
    CAR, MOTORCYCLE, BICYCLE;
}

Rozdzielamy nasze pojazdy na spalinowe i te o konstrukcji prostej (rower):

public abstract class SimpleVehicle {

    private int numberOfComponents;
    private int price;

    protected SimpleVehicle(int numberOfComponents, int price) {
        this.numberOfComponents = numberOfComponents;
        this.price = price;
    }

    public int getNumberOfComponents() {
        return numberOfComponents;
    }

    public int getPrice() {
        return price;
    }
}
public abstract class CombustionVehicle {

    private String engine;
    private int numberOfComponents;
    private int price;

    protected CombustionVehicle(String engine, int numberOfComponents, int price) {
        this.engine = engine;
        this.numberOfComponents = numberOfComponents;
        this.price = price;
    }

    public String getEngine() {
        return engine;
    }

    public int getNumberOfComponents() {
        return numberOfComponents;
    }

    public int getPrice() {
        return price;
    }
}

Tworzymy szkielet naszych fabryk:

    abstract public class Factory {
        abstract public CombustionVehicle createCombustionVehicle(VehicleType type);
        abstract public SimpleVehicle createSimpleVehicle(VehicleType type);
    }

Implementujemy fabryki oddzielnie dla marki Suzuki i dla marki Honda:

public class SuzukiFactory extends Factory {

    @Override
    public CombustionVehicle createCombustionVehicle(VehicleType type) {

        switch (type) {
            case CAR:
                return new Car("V6", 240, 70000);
            case MOTORCYCLE:
                return new Motorcycle("V2", 170, 55000);
            default:
                throw new UnsupportedOperationException("Invalid type");
        }
    }

        @Override
        public SimpleVehicle createSimpleVehicle(VehicleType type) {

        switch(type) {
                case BICYCLE:
                    return new Bicycle(70, 4000);
                default:
                    throw new UnsupportedOperationException("Invalid type");
            }
        }
}
public class HondaFactory extends Factory {

    @Override
    public CombustionVehicle createCombustionVehicle(VehicleType type) {

        switch(type) {
            case CAR:
                return new Car("V8", 200, 50000);
            case MOTORCYCLE:
                return new Motorcycle("V4", 150, 45000);
            default:
                throw new UnsupportedOperationException("Invalid type");
        }

    }

    @Override
    public SimpleVehicle createSimpleVehicle(VehicleType type) {

        switch(type) {
            case BICYCLE:
                return new Bicycle(50, 3000);
            default:
                throw new UnsupportedOperationException("Invalid type");
        }

    }
}

Możemy już przejść do stworzenia konkretnych obiektów dla konkretnej marki i zbudowania naszych pojazdów:

public class Main {
    public static void main(String[] args) {

//        Factory factory = new VehicleFactory();
//
//        Vehicle car = factory.createVehicle(VehicleType.CAR);
//        Vehicle motorcycle = factory.createVehicle(VehicleType.MOTORCYCLE);

//        Vehicles.Vehicle car = new Vehicles.Car("V8", 200, 50000);
//        Vehicles.Vehicle motorcycle = new Vehicles.Motorcycle("V4", 150, 45000);

        Factory suzukiFactory = new SuzukiFactory();
        Factory hondaFactory = new HondaFactory();

        CombustionVehicle suzukiCar = suzukiFactory.createCombustionVehicle(VehicleType.CAR);
        CombustionVehicle suzukiMotorcycle =   
        suzukiFactory.createCombustionVehicle(VehicleType.MOTORCYCLE);
        SimpleVehicle suzukiBicycle = suzukiFactory.createSimpleVehicle(VehicleType.BICYCLE);

        CombustionVehicle hondaCar = hondaFactory.createCombustionVehicle(VehicleType.CAR);
        CombustionVehicle hondaMotorcycle = 
        hondaFactory.createCombustionVehicle(VehicleType.MOTORCYCLE);
        SimpleVehicle hondaBicycle = suzukiFactory.createSimpleVehicle(VehicleType.BICYCLE);

    }
}

Jaka jest różnica między metodą fabrykującą a fabryką abstrakcyjną można zapytać?

Jak zauważyliśmy na przykładzie fabryka abstrakcyjna wykorzystuje wiele metod fabrykujących.

Zalety:

  • Łatwość testowania
  • Tworzenie obiektu zlecane jest do sub-klasy
  • Modułowa rozwojowość aplikacji

Wady:

  • Duża ilość dodatkowych klas
  • Zwiększona złożoność kodu