- Что такое паттерны (шаблоны) проектирования?
- Зачем нужны паттерны проектирования?
- Типы шаблонов проектирования
- Порождающие шаблоны проектирования
- Простая фабрика (Simple Factory)
- Фабричный метод (Factory Method)
- Абстрактная фабрика (Abstract Factory)
- Строитель (Builder)
- Прототип (Prototype)
- Одиночка (Singleton)
- Пул объектов (Object Pool)
Первая статья из цикла статей, посвящённого паттернам проектирования. В этой статье мы рассмотрим различные порождающие паттерны (шаблоны) проектирования и их практическое применение. Мы изучим, как они помогают улучшить архитектуру программных систем, сократить издержки и обеспечить легкость поддержки и расширения.
Что такое паттерны (шаблоны) проектирования?
Паттерны проектирования представляют собой набор проверенных и готовых к использованию решений для тех типичных задач, которые возникают при создании программного обеспечения. Они не ограничиваются конкретным языком программирования и не являются библиотеками, которые можно просто подключить к проекту. Вместо этого, они представляют собой образцы, или шаблоны, которые разработчики могут адаптировать и использовать для своих конкретных задач.
Важно отметить, что выбор правильного шаблона и его правильное применение играют решающую роль. Неправильное использование шаблона или его использование в несоответствующей ситуации может создать дополнительные проблемы. Однако, когда шаблон используется правильно, он способен сделать разработку программного обеспечения более эффективной, упростить задачу и повысить качество результата.
Помимо этого, стоит отметить, что паттерны проектирования документированы и широко признаны в сообществе разработчиков, что делает их универсальным инструментом для улучшения структуры и эффективности программ.
Существует три основных типа паттерна проектирования: порождающие (creational), структурные (structural) и поведенческие (behavioral).
Зачем нужны паттерны проектирования?
Шаблоны проектирования представляют собой готовые и проверенные руководства по решению типичных задач, которые возникают при проектировании и разработке сложных систем. Паттерны проектирования обеспечивают структурированный и эффективный способ реализации архитектурных решений, облегчая разработку, сопровождение и масштабирование программ.
Одной из главных задач шаблонов проектирования является обеспечение повторного использования кода. Разработчики сталкиваются с похожими проблемами в различных проектах. Шаблоны позволяют извлекать и изолировать общие решения для таких проблем в отдельные компоненты, которые могут быть повторно использованы в разных контекстах. Это способствует сокращению времени разработки, уменьшению вероятности ошибок и повышению качества программного обеспечения.
Шаблоны проектирования содействуют созданию чистого и структурированного кода. Они предоставляют соглашения и стандарты, которые помогают разработчикам легче понимать архитектуру программы. Кроме того, шаблоны способствуют разделению ответственностей между различными компонентами, что делает код более модульным и поддерживаемым. Это особенно важно при работе в команде, где разработчики должны легко читать и изменять чужой код.
Проектирование сложных систем — это вызов даже для опытных разработчиков. Паттерны проектирования предоставляют готовые решения для управления сложностью системы. Они позволяют разбить систему на более мелкие и понятные части, что упрощает разработку и сопровождение. Шаблоны также помогают управлять взаимосвязями между компонентами, что делает систему более гибкой и адаптивной к изменениям.
Паттерны проектирования способствуют созданию гибких и расширяемых систем. Они разделяют функциональность на отдельные компоненты, которые могут быть изменены или расширены без влияния на другие части системы. Это позволяет легко адаптировать программное обеспечение к новым требованиям и сценариям использования, а также уменьшить риск внесения ошибок при внесении изменений.
Типы шаблонов проектирования
Существует три типа шаблонов проектирования:
- Порождающие паттерны — рассматриваем в этой статье
- Структурные паттерны — описаны в статье Паттерны проектирования. Часть 2. Структурные шаблоны
- Поведенческие паттерны — описаны в статье Паттерны проектирования. Часть 3. Поведенческие шаблоны
Порождающие шаблоны проектирования
Порождающие шаблоны проектирования (Creational Patterns) — это одна из трех групп шаблонов проектирования, которые используются в программировании, содержит 7 паттернов проектирования. Они ориентированы на задачи создания объектов и управлением процессом их создания. Порождающие шаблоны подразумевают использование абстракций и интерфейсов, чтобы скрыть детали конкретной реализации объектов, что упрощает систему и делает ее более гибкой.
Главная цель порождающих шаблонов проектирования — облегчить процесс создания объектов, сделать его более гибким и удобным для разработчика.
Простая фабрика (Simple Factory)
Простая фабрика — это неформальный порождающий паттерн проектирования, который предоставляет общий интерфейс для создания объектов, но скрывает сложности процесса создания от клиентского кода. Этот паттерн удобен, когда у вас есть несколько различных классов, которые могут быть созданы на основе каких-то общих параметров, и вы хотите упростить процесс создания объектов.
Суть паттерна
Простая фабрика предоставляет класс, называемый «фабрикой», который имеет метод для создания объектов. Этот метод принимает параметры и возвращает экземпляр соответствующего класса в зависимости от переданных параметров. Клиентский код общается только с фабрикой, не зная о конкретных классах, которые будут созданы.
Пример
Давайте представим, что у нас есть фабрика по созданию различных видов автомобилей. У нас есть базовый класс Car
и его подклассы Sedan
, SUV
и SportsCar
. Фабрика будет создавать объекты этих классов в зависимости от типа автомобиля.
Сначала определим базовый класс Car
и его подклассы:
class Car {
void start() {
System.out.println("Автомобиль стартовал.");
}
}
class Sedan extends Car {
@Override
void start() {
System.out.println("Седан стартовал.");
}
}
class SUV extends Car {
@Override
void start() {
System.out.println("Внедорожник стартовал.");
}
}
class SportsCar extends Car {
@Override
void start() {
System.out.println("Спорткар стартовал.");
}
}
Теперь создадим простую фабрику CarFactory, которая будет создавать объекты на основе переданного типа:
class CarFactory {
public Car createCar(String type) {
if ("sedan".equalsIgnoreCase(type)) {
return new Sedan();
} else if ("suv".equalsIgnoreCase(type)) {
return new SUV();
} else if ("sportscar".equalsIgnoreCase(type)) {
return new SportsCar();
} else {
throw new IllegalArgumentException("Неподдерживаемый тип автомобиля: " + type);
}
}
}
Теперь клиентский код может использовать CarFactory для создания автомобилей без необходимости знания о конкретных классах:
CarFactory factory = new CarFactory();
Car car1 = factory.createCar("sedan");
Car car2 = factory.createCar("suv");
Car car3 = factory.createCar("sportscar");
car1.start();
car2.start();
car3.start();
Простая фабрика упрощает создание объектов и изоляцию сложности создания от клиентского кода. Этот паттерн особенно полезен, когда у вас есть несколько классов с общим интерфейсом, и вы хотите выбирать, какой класс создавать в зависимости от внешних факторов.
Фабричный метод (Factory Method)
Фабричный метод — это порождающий паттерн проектирования, который определяет общий интерфейс для создания объектов, но позволяет подклассам выбирать классы для создания. Это делегирует процесс инстанцирования объекта подклассам, обеспечивая таким образом расширяемость и гибкость в системе.
Суть паттерна
Суть паттерна «Фабричный метод» заключается в том, что у нас есть абстрактный класс или интерфейс, который объявляет метод для создания объектов. Конкретные подклассы реализуют этот метод, чтобы создавать объекты своего типа. Клиентский код использует абстрактный класс или интерфейс для создания объектов, не зная о конкретных классах.
Пример
Давайте представим, что у нас есть фреймворк для создания документов, и у нас есть абстрактный класс DocumentCreator, который содержит фабричный метод createDocument. Подклассы TextDocumentCreator и SpreadsheetDocumentCreator реализуют этот метод и возвращают экземпляры соответствующих документов.
// Абстрактный класс, представляющий фабричный метод
abstract class DocumentCreator {
public abstract Document createDocument();
}
// Конкретный подкласс для создания текстовых документов
class TextDocumentCreator extends DocumentCreator {
@Override
public Document createDocument() {
return new TextDocument();
}
}
// Конкретный подкласс для создания таблиц
class SpreadsheetDocumentCreator extends DocumentCreator {
@Override
public Document createDocument() {
return new SpreadsheetDocument();
}
}
// Общий интерфейс для документов
interface Document {
void open();
void save();
void close();
}
Теперь, когда мы хотим создать документ, мы можем использовать фабричный метод без знания конкретных классов:
DocumentCreator textDocumentCreator = new TextDocumentCreator();
Document textDocument = textDocumentCreator.createDocument();
textDocument.open();
// Работаем с текстовым документом
textDocument.save();
textDocument.close();
DocumentCreator spreadsheetDocumentCreator = new SpreadsheetDocumentCreator();
Document spreadsheetDocument = spreadsheetDocumentCreator.createDocument();
spreadsheetDocument.open();
// Работаем с таблицей
spreadsheetDocument.save();
spreadsheetDocument.close();
Таким образом, паттерн позволяет создавать объекты, не завися от их конкретных классов. Чем обеспечивает гибкость и расширяемость системы.
Абстрактная фабрика (Abstract Factory)
Абстрактная фабрика — это порождающий паттерн проектирования, который предоставляет интерфейс для создания семейств взаимосвязанных или зависимых объектов без указания их конкретных классов. Этот паттерн ориентирован на создание объектов с определенными характеристиками, при этом абстрагирует процесс создания от клиента, что делает его код более независимым от конкретных классов.
Суть паттерна
Абстрактная фабрика опирается на абстрактные классы или интерфейсы для предоставления набора связанных фабричных методов. Каждая конкретная реализация абстрактной фабрики предоставляет свое семейство объектов, обеспечивая их совместимость. Таким образом, клиентский код работает с абстрактными интерфейсами, не зная о конкретных классах.
Пример паттерна
Рассмотрим простой пример создания виджетов для графического интерфейса. У нас есть две платформы: Windows и macOS. Для каждой платформы мы хотим создать набор виджетов: кнопку и флажок.
Сначала определим абстрактные интерфейсы для виджетов:
// Абстрактная фабрика для создания виджетов
interface WidgetFactory {
Button createButton();
Checkbox createCheckbox();
}
// Абстрактный класс для кнопки
interface Button {
void paint();
}
// Абстрактный класс для флажка
interface Checkbox {
void render();
}
Теперь создадим конкретные реализации абстрактных фабрик и их продуктов для Windows и macOS:
// Конкретная фабрика для Windows
class WindowsFactory implements WidgetFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
// Конкретная фабрика для macOS
class MacOSFactory implements WidgetFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
@Override
public Checkbox createCheckbox() {
return new MacOSCheckbox();
}
}
// Конкретная реализация кнопки для Windows
class WindowsButton implements Button {
@Override
public void paint() {
System.out.println("Отрисовка кнопки в стиле Windows");
}
}
// Конкретная реализация флажка для Windows
class WindowsCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Отрисовка флажка в стиле Windows");
}
}
// Конкретная реализация кнопки для macOS
class MacOSButton implements Button {
@Override
public void paint() {
System.out.println("Отрисовка кнопки в стиле macOS");
}
}
// Конкретная реализация флажка для macOS
class MacOSCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Отрисовка флажка в стиле macOS");
}
}
Теперь клиентский код может создавать виджеты без знания конкретных классов:
WidgetFactory factory = new WindowsFactory(); // или MacOSFactory
Button button = factory.createButton();
Checkbox checkbox = factory.createCheckbox();
button.paint();
checkbox.render();
Паттерн «Абстрактная фабрика» позволяет создавать совместимые семейства объектов, обеспечивая высокую абстракцию и независимость клиентского кода.
Строитель (Builder)
Строитель — это порождающий паттерн проектирования, который используется для пошагового создания сложных объектов с множеством параметров. Паттерн отделяет процесс создания объекта от его представления. Тем самым позволяя создавать различные варианты объекта с одним и тем же строителем.
Суть паттерна
Суть паттерна заключается в том, что он предоставляет клиентскому коду простой способ создания сложных объектов. Вместо передачи множества аргументов в конструктор объекта, клиент использует строителя для последовательного установления параметров объекта. Это делает код более читаемым и понятным.
Пример
Давайте рассмотрим создание объекта Person с различными параметрами, такими как имя, возраст, адрес и телефон. Мы будем использовать паттерн «Строитель» для создания объекта Person.
Сначала определим класс Person:
class Person {
private String name;
private int age;
private String address;
private String phoneNumber;
public Person(String name, int age, String address, String phoneNumber) {
this.name = name;
this.age = age;
this.address = address;
this.phoneNumber = phoneNumber;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", address='" + address + '\'' +
", phoneNumber='" + phoneNumber + '\'' +
'}';
}
}
Теперь создадим строитель PersonBuilder, который позволит пошагово устанавливать параметры объекта Person:
class PersonBuilder {
private String name;
private int age;
private String address;
private String phoneNumber;
public PersonBuilder setName(String name) {
this.name = name;
return this;
}
public PersonBuilder setAge(int age) {
this.age = age;
return this;
}
public PersonBuilder setAddress(String address) {
this.address = address;
return this;
}
public PersonBuilder setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
public Person build() {
return new Person(name, age, address, phoneNumber);
}
}
Теперь клиентский код может использовать PersonBuilder для создания объекта Person:
Person person = new PersonBuilder()
.setName("John")
.setAge(30)
.setAddress("123 Main St")
.setPhoneNumber("+1 555-123-4567")
.build();
System.out.println(person);
Этот подход позволяет легко создавать объекты Person
с различными комбинациями параметров, не утруждая клиентский код большим числом конструкторов или аргументов.
Прототип (Prototype)
Прототип — это порождающий паттерн проектирования, который позволяет создавать новые объекты, копируя существующие объекты, то есть прототипы. Паттерн применяется, когда создание объекта с нуля слишком затратно или сложно, и наша задача — создать объект, подобный существующему, но с некоторыми изменениями.
Суть паттерна
Суть паттерна «Прототип» заключается в создании абстрактного базового класса, который содержит метод для клонирования объекта. Конкретные подклассы реализуют этот метод, чтобы создавать копии самих себя. Клиентский код может запросить у прототипа копию объекта и внести изменения в скопированном объекте.
Пример
Давайте рассмотрим пример с созданием копии геометрических фигур. У нас есть абстрактный класс Shape
, который определяет метод clone
для копирования фигур. Затем у нас есть конкретные классы Circle
и Rectangle
, которые реализуют метод clone
для создания копий самих себя.
Сначала определим абстрактный класс Shape
:
abstract class Shape {
protected int x;
protected int y;
public Shape(int x, int y) {
this.x = x;
this.y = y;
}
public abstract Shape clone();
public abstract void draw();
}
Затем создадим конкретные классы Circle
и Rectangle
, которые реализуют метод clone
:
class Circle extends Shape {
private int radius;
public Circle(int x, int y, int radius) {
super(x, y);
this.radius = radius;
}
@Override
public Shape clone() {
return new Circle(this.x, this.y, this.radius);
}
@Override
public void draw() {
System.out.println("Рисуем круг в точке (" + x + ", " + y + ") с радиусом " + radius);
}
}
class Rectangle extends Shape {
private int width;
private int height;
public Rectangle(int x, int y, int width, int height) {
super(x, y);
this.width = width;
this.height = height;
}
@Override
public Shape clone() {
return new Rectangle(this.x, this.y, this.width, this.height);
}
@Override
public void draw() {
System.out.println("Рисуем прямоугольник в точке (" + x + ", " + y + ") с шириной " + width + " и высотой " + height);
}
}
Теперь клиентский код может создавать копии фигур, не зная о конкретных классах:
Shape originalCircle = new Circle(0, 0, 5);
Shape clonedCircle = originalCircle.clone();
Shape originalRectangle = new Rectangle(1, 1, 10, 5);
Shape clonedRectangle = originalRectangle.clone();
originalCircle.draw();
clonedCircle.draw();
originalRectangle.draw();
clonedRectangle.draw();
Паттерн «Прототип» позволяет создавать копии объектов с минимальными усилиями и позволяет легко изменять характеристики копии.
Одиночка (Singleton)
Одиночка — это порождающий паттерн проектирования, который гарантирует, что класс имеет только один экземпляр и предоставляет глобальную точку доступа к этому экземпляру. Этот паттерн используется, когда нужно убедиться, что у класса есть только один объект, который управляет определенным ресурсом или служит определенной цели.
Суть паттерна
Суть паттерна «Одиночка» заключается в создании закрытого конструктора для класса и статического метода, который возвращает единственный экземпляр класса. Это гарантирует, что только один экземпляр будет создан, и все последующие запросы на создание объекта будут возвращать этот существующий экземпляр.
Пример
Давайте рассмотрим пример с классом Configuration
, который хранит настройки приложения. Мы хотим, чтобы у нас был только один объект Configuration
, который хранит все настройки приложения.
Создадим класс Configuration
с закрытым конструктором и статическим методом getInstance
, который будет возвращать единственный экземпляр:
public class Configuration {
private static Configuration instance;
private String databaseUrl;
private String apiKey;
private Configuration() {
// Приватный конструктор
}
public static Configuration getInstance() {
if (instance == null) {
instance = new Configuration();
}
return instance;
}
public String getDatabaseUrl() {
return databaseUrl;
}
public void setDatabaseUrl(String databaseUrl) {
this.databaseUrl = databaseUrl;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
}
Теперь клиентский код может получить доступ к единственному экземпляру Configuration
следующим образом:
Configuration config = Configuration.getInstance();
config.setDatabaseUrl("jdbc:mysql://localhost:3306/mydb");
config.setApiKey("my-api-key");
// Где угодно в коде
Configuration anotherConfig = Configuration.getInstance();
System.out.println(anotherConfig.getDatabaseUrl()); // Выведет "jdbc:mysql://localhost:3306/mydb"
Обратите внимание, что даже при множестве запросов на создание объекта Configuration
, всегда будет возвращаться один и тот же экземпляр.
Паттерн «Одиночка» обеспечивает глобальную точку доступа к объекту. Он гарантирует, что у класса будет только один экземпляр. Это полезно, когда нужно разделить общие ресурсы между различными частями приложения. Также он полезен когда нужно иметь единственный объект для управления определенным состоянием.
Пул объектов (Object Pool)
Пул объектов — это порождающий паттерн проектирования, который позволяет создавать набор объектов заранее и управлять ими, чтобы избежать накладных расходов на создание и уничтожение объектов во время выполнения программы. Этот паттерн особенно полезен, когда создание объектов является ресурсоемкой операцией, и объекты могут быть многократно использованы.
Суть паттерна
Суть паттерна «Пул объектов» заключается в создании пула, который содержит заранее созданные объекты. Когда клиентский код нуждается в объекте, он запрашивает его из пула. Если объект свободен, он арендуется клиенту. После использования объект возвращается в пул, а не уничтожается. Это позволяет избежать накладных расходов на создание и уничтожение объектов, а также управлять их переиспользованием.
Пример
Давайте рассмотрим пример с пулом объектов для подключений к базе данных. Предположим, у нас есть класс DatabaseConnection, представляющий подключение к базе данных.
Создадим класс ConnectionPool, который будет хранить пул объектов DatabaseConnection:
import java.util.ArrayList;
import java.util.List;
public class ConnectionPool {
private static final int POOL_SIZE = 10;
private List connections;
public ConnectionPool() {
connections = new ArrayList<>();
for (int i = 0; i < POOL_SIZE; i++) {
connections.add(new DatabaseConnection());
}
}
public DatabaseConnection acquireConnection() {
if (connections.isEmpty()) {
System.out.println("Все подключения заняты. Пожалуйста, подождите.");
return null;
}
DatabaseConnection connection = connections.remove(0);
System.out.println("Подключение арендовано.");
return connection;
}
public void releaseConnection(DatabaseConnection connection) {
if (connection != null) {
connections.add(connection);
System.out.println("Подключение возвращено в пул.");
}
}
}
Теперь клиентский код может использовать пул объектов для аренды и возврата подключений к базе данных:
public class Client {
public static void main(String[] args) {
ConnectionPool connectionPool = new ConnectionPool();
// Арендуем подключение
DatabaseConnection connection1 = connectionPool.acquireConnection();
// Выполняем работу с подключением
// Возвращаем подключение в пул
connectionPool.releaseConnection(connection1);
// Арендуем ещё одно подключение
DatabaseConnection connection2 = connectionPool.acquireConnection();
// Выполняем работу с новым подключением
// Возвращаем подключение в пул
connectionPool.releaseConnection(connection2);
}
}
Паттерн «Пул объектов» позволяет эффективно управлять ресурсами, которые дорого создавать. Также он позволяет повторно использовать объекты, сокращая затраты на создание и уничтожение. Это особенно полезно в случаях, когда требуется частое создание и уничтожение объектов, таких как подключения к базе данных, потоки или сетевые соединения.