Мы уже прошли такую схему использования объекта класса, как 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 имеют конструктор, принимающий объекты этих же классов.