Ми вже пройшли таку схему використання об’єкта класу, як Singleton, але ти, можливо, ще не підозрюєш, що це один із патернів проєктування, причому один із тих, що найчастіше використовується.

Насправді існує дуже багато цих самих патернів, ба більше — вони мають ієрархію, і у кожного патерну — своє призначення.

Класифікація патернів

Вид патерну Застосовність Застосовність
Породжувальні Тип, який вирішує проблему створення об’єктів
Структурні Патерни, які дозволяють побудувати правильну, доступну до розширення ієрархію класів у нашій архітектурі
Поведінкові Цей кластер патернів відповідає за безпечну та зручну взаємодію між об’єктами програми

Зазвичай патерн характеризується проблемою, яку він вирішує. Давайте розглянемо кілька патернів, з якими ми найчастіше стикаємося під час роботи з Java:

Патерн Призначення
Singleton З ним ми вже знайомі і використовуємо його для створення та звернення до класу, у якого не може бути більше ніж один екземпляр.
Iterator Ми, до речі, теж із ним знайомі та знаємо, що цей патерн дозволяє нам перебирати складовий об’єкт, не розкриваючи його внутрішнього представлення. Використовується з колекціями.
Adapter Пов’язує несумісні об’єкти для спільної роботи. Думаю, що говорячи про адаптер, кожен собі уявляє саме те, що і робить цей патерн. Простий приклад із життя — адаптер USB — вилка в розетку.
Template Method

Поведінковий патерн програмування, який вирішить проблему інтеграції та дозволяє, не змінюючи структуру алгоритму, змінити його кроки.

Уявимо, що у нас є алгоритм збирання автомобіля у вигляді послідовності збирання:

Шасі -> Кузов -> Двигун -> збірка салону

Якщо ми поставимо посилену раму, потужніший двигун або салон із додатковим підсвічуванням, ми все одно не змінимо алгоритм, і абстрактна послідовність залишиться такою самою.

Decorator Створює об’єктам обгортки, щоб додати їм корисну функціональність. Його ми й розглянемо в цій статті.

В Java.io патерни реалізуються у таких класах:

Патерн Де використовується в Java.io
Adapter
Template Method
Decorator

Патерн Декоратор

Давайте уявимо, що ми описуємо модель проєктування будинку.

Загалом схема буде такою:

Спочатку у нас є вибір із декількох видів будинків. Мінімальна комплектація — один поверх із дахом, далі — за допомогою декораторів будь-який із видів ми можемо змінити додатковими параметрами, і це впливатиме на ціну будинку.

Створюємо абстрактний клас будинку:

public abstract class House {
	String info;

	public String getInfo(){
    	return info;
	}

	public abstract int getPrice();
}

Тут у нас 2 методи:

  • getInfo() повертає інформацію про назву та комплектацію нашого будинку;
  • getPrice() — ціну будинку в поточній комплектації.

Також у нас є стандартні реалізації будинків — цегляний та дерев’яний:

public class BrickHouse extends House {

	public BrickHouse() {
    	info = "Цегляний будинок";
	}

	@Override
	public int getPrice() {
    	return 20_000;
	}
}

public class WoodenHouse extends House {

	public WoodenHouse() {
    	info = "Дерев’яний будинок";
	}

	@Override
	public int getPrice() {
    	return 25_000;
	}
}

Обидва класи успадковують від класу House та перевизначають метод ціни, встановлюючи індивідуальну ціну за стандартний будинок. В конструкторі присвоюємо ім’я.

Далі нам потрібно написати класи — декоратори. Це класи, які також успадковані від класу House. Для цього ми створюємо абстрактний клас декоратора.

У нього ми можемо покласти додаткову логіку для зміни об’єкта. В нашому випадку додаткової логіки не буде, і абстрактний клас буде порожнім.

abstract class HouseDecorator extends House {
}

Далі створюємо класи-реалізації декораторів. Ми створимо кілька класів, які дозволяють додати до будинку додаткові параметри:

public class SecondFloor extends HouseDecorator {
	House house;

	public SecondFloor(House house) {
    	this.house = house;
	}

	@Override
	public int getPrice() {
    	return house.getPrice() + 20_000;
	}

	@Override
	public String getInfo() {
    	return house.getInfo() + " + второй этаж";
	}
}
Декоратор, який додає другий поверх до нашого будинку

У конструктор декоратора приймається будинок, до якого ми застосовуємо модифікації декоратора. А методи getPrice() та getInfo() ми перевизначаємо, повертаючи інформацію про новий модернізований будинок, створений на основі старого.

public class Garage extends HouseDecorator {

	House house;
	public Garage(House house) {
    	this.house = house;
	}

	@Override
	public int getPrice() {
    	return house.getPrice() + 5_000;
	}

	@Override
	public String getInfo() {
    	return house.getInfo() + " + гараж";
	}
}
Декоратор, який додає до нашого будинку гараж

Тепер ми можемо змінити наш будинок за допомогою декораторів. Для цього потрібно створити будинок:

House brickHouse = new BrickHouse();

Далі ми привласнюємо нашій змінній house нове значення у вигляді декоратора, в який ми передаємо наш будинок:

brickHouse = new SecondFloor(brickHouse);

Наша змінна house вже має будинок із другим поверхом.

Давайте розглянемо кейси використання декораторів:

Приклад коду Виведення
House brickHouse = new BrickHouse();

  System.out.println(brickHouse.getInfo());
  System.out.println(brickHouse.getPrice());

Цегляний будинок

20000

House brickHouse = new BrickHouse();

  brickHouse = new SecondFloor(brickHouse);

  System.out.println(brickHouse.getInfo());
  System.out.println(brickHouse.getPrice());

Цегляний будинок + другий поверх

40000

House brickHouse = new BrickHouse();


  brickHouse = new SecondFloor(brickHouse);
  brickHouse = new Garage(brickHouse);

  System.out.println(brickHouse.getInfo());
  System.out.println(brickHouse.getPrice());

Цегляний будинок + другий поверх + гараж

45000

House woodenHouse = new SecondFloor(new Garage(new WoodenHouse()));

  System.out.println(woodenHouse.getInfo());
  System.out.println(woodenHouse.getPrice());

Дерев’яний будинок + гараж + другий поверх

50000

House woodenHouse = new WoodenHouse();

  House woodenHouseWithGarage = new Garage(woodenHouse);

  System.out.println(woodenHouse.getInfo());
  System.out.println(woodenHouse.getPrice());

  System.out.println(woodenHouseWithGarage.getInfo());
  System.out.println(woodenHouseWithGarage.getPrice());

Дерев’яний будинок

25000

Дерев’яний будинок + гараж

30000

У цьому прикладі ми бачимо перевагу модернізації об’єкта за допомогою декоратора. Виходить, що ми не змінили власне об’єкт woodenHouse, а створили новий об’єкт на базі нього. Але крізь цю перевагу можна розгледіти й недоліки: ми щоразу створюємо новий об’єкт у пам’яті, що дає на неї додаткове навантаження.

Розглянемо UML діаграму нашої програми:

Декоратор реалізується досить просто та застосовується для динамічної зміни об’єктів, їхньої модернізації. Декоратор можна розпізнати за конструкторами, які приймають у параметрах об’єкти того самого абстрактного типу або інтерфейсу, що й поточний клас. В Java цей патерн широко використовується в класах введення/виведення.

Наприклад, як ми вже зазначили, всі підкласи java.io.InputStream OutputStream, Reader та Writer мають конструктор, який приймає об’єкти цих самих класів.