Разбираем шаблоны проектирования
Разработка современных приложений процесс достаточно сложный, требующий глубокого погружения, продумывания процесса взаимодействия компонентов. При этом разрабатывать код непосредственно с нуля конечно можно, но в таком случае процесс выпуска готового решения займет значительное время, а время как известно самый дорогой ресурс.
Для ускорения проектирования и последующей разработки приложений придумали шаблоны проектирования. По сути, шаблоны проектирования это проверенные и готовые к использованию решения регулярно возникающих в повседневном программировании задач. То есть, это повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста.
В какой-то степени шаблоны проектирования это руководства по решению определенных задач в программировании. Смысл в том, что при разрешении какого-либо вопроса в разработке скорее всего, до вас с этим уже кто-то сталкивался, причем не один раз и уже какие-то рецепты решения проблемы. Так что, вместо того, чтобы изобретать свой велосипед лучше попробовать воспользоваться уже разработанными шаблонами проектирования.
При этом, как правило, шаблон не является полностью законченной конструкцией, которую можно скопировать с помощью Ctrl+C и Ctrl+V и преобразовать в код. Вместо этого, шаблон является одним из вариантов решения задачи, который можно использовать в различных ситуациях.
Представленный в статье материал предполагает знакомство читателя с основами объектно-ориентированного программирования.
Типы шаблонов
Существует три типа шаблонов:
Порождающие
Порождающие шаблоны позволяют сделать систему независимой от способа создания, композиции и представления объектов. Так шаблон, порождающий классы, должен использовать наследование, чтобы изменять наследуемый класс, а например, шаблон, порождающий объекты, делегирует инстанцирование (то есть процесс создания на основе класса экземпляра — такого объекта, который получает доступ ко всему содержимому класса, но при этом обладает и способностью хранить собственные данные) другому объекту. При этом, имея объект, всегда можно узнать, экземпляром какого класса он является. То есть, порождающие шаблоны предоставляют механизмы инициализации, позволяя создавать объекты удобным способом.
Существует несколько видов порождающих шаблонов:
abstract factory;
builder;
factory method;
lazy initialization;
object pool;
prototype;
singleton;
Multiton.
Рассмотрим каждый из них подробнее.
Абстрактная фабрика (abstract factory) — разновидность порождающих шаблонов проектирования, которые содержат интерфейс для создания взаимосвязанных объектов, но при этом не производят спецификации конкретных классов для этих объектов. То есть, абстрактная фабрика группирует индивидуальные, но связанные фабрики вместе без указания их конкретных классов.
Шаблон реализуется с помощью абстрактного класса Factory, который содержит интерфейс для создания компонентов системы, к которому далее пишутся классы, реализующие этот интерфейс. Например, шаблон для оконного интерфейса может создавать окна и кнопки для которых затем уже пишутся обработчики.
Вот пример такого класса. В нем передаются данные (круг, прямоугольник, квадрат) в AbstractFactory, чтобы получить необходимый тип объекта.
public class AbstractFactoryPatternDemo {
public static void main(String[] args) {
//get shape factory
AbstractFactory shapeFactory = FactoryProducer.getFactory(false);
//get an object of Shape Rectangle
Shape shape1 = shapeFactory.getShape("RECTANGLE");
//call draw method of Shape Rectangle
shape1.draw();
//get an object of Shape Square
Shape shape2 = shapeFactory.getShape("SQUARE");
//call draw method of Shape Square
shape2.draw();
//get shape factory
AbstractFactory shapeFactory1 = FactoryProducer.getFactory(true);
//get an object of Shape Rectangle
Shape shape3 = shapeFactory1.getShape("RECTANGLE");
//call draw method of Shape Rectangle
shape3.draw();
//get an object of Shape Square
Shape shape4 = shapeFactory1.getShape("SQUARE");
//call draw method of Shape Square
shape4.draw();
}
}
Шаблон строитель (builder) предназначен для того, чтобы отделить конструирование сложного объекта от его представления, таким образом, чтобы в результате одного и того же процесса конструирования могли получаться разные представления. По сути, данный шаблон позволяет создавать различные варианты объекта, избегая при этом «загрязнения» конструктора. Полезно, когда у объекта может быть несколько разновидностей или когда в создании объекта задействовано много шагов. Вот пример TestBuilderPattern, показывающий, как использовать класс Builder для получения объекта
public class TestBuilderPattern {
public static void main(String[] args) {
//Using builder to get the object in a single line of code and
//without any inconsistent state or arguments management issues
Computer comp = new Computer.ComputerBuilder(
"500 GB", "2 GB").setBluetoothEnabled(true)
.setGraphicsCardEnabled(true).build();
}
}
Фабричный метод (factory method) — предназначен для обеспечения возможности передачи логики создания объекта в дочерние классы. То есть, фабрика делегирует создание объектов наследникам родительского класса. Благодаря этому, мы можем использовать в коде программы не специфические классы, а манипулировать абстрактными объектами на более высоком уровне.
Шаблон ленивая инициализация (lazy initialization) как можно понять из названия предназначен для создания объекта непосредственно перед использованием. Дело в том, что многие ресурсоемкие операции, например вычислительные, достаточно требовательные к таким ресурсам, как память и создание объекта заранее может привести к тому, что при вычислениях будет задействовано меньше памяти и они будут выполняться дольше. В таком случае имеет смысл создавать объекты непосредственно перед использованием. Недостатками данного вида шаблонов является невозможность явным образом задать порядок инициализации шаблонов и возможна задержка при первом обращении к объекту.
Шаблон объектный пул (object pool) это просто набор инициализированных и готовых к использованию объектов. То есть мы можем не создавать объект, а просто взять его из пула.
В примере пул объектов управляет соединениями и предоставляет способ их повторного и совместного использования. Он также может ограничить максимальное количество объектов, которые могут быть созданы.
// Java program to illustrate
// Object Pool Design Pattern
abstract class ObjectPool {
long deadTime;
Hashtable lock, unlock;
ObjectPool()
{
deadTime = 50000; // 50 seconds
lock = new Hashtable();
unlock = new Hashtable();
}
abstract T create();
abstract boolean validate(T o);
abstract void dead(T o);
synchronized T takeOut()
{
long now = System.currentTimeMillis();
T t;
if (unlock.size() > 0) {
Enumeration e = unlock.keys();
while (e.hasMoreElements()) {
t = e.nextElement();
if ((now - unlock.get(t)) > deadTime) {
// object has dead
unlock.remove(t);
dead(t);
t = null;
}
else {
if (validate(t)) {
unlock.remove(t);
lock.put(t, now);
return (t);
}
else {
// object failed validation
unlock.remove(t);
dead(t);
t = null;
}
}
}
}
// no objects available, create a new one
t = create();
lock.put(t, now);
return (t);
}
synchronized void takeIn(T t)
{
lock.remove(t);
unlock.put(t, System.currentTimeMillis());
}
}
// Three methods are abstract
// and therefore must be implemented by the subclass
class JDBCConnectionPool extends ObjectPool {
String dsn, usr, pwd;
JDBCConnectionPool(String driver, String dsn, String usr, String pwd)
{
super();
try {
Class.forName(driver).newInstance();
}
catch (Exception e) {
e.printStackTrace();
}
this.dsn = dsn;
this.usr = usr;
this.pwd = pwd;
}
Connection create()
{
try {
return (DriverManager.getConnection(dsn, usr, pwd));
}
catch (SQLException e) {
e.printStackTrace();
return (null);
}
}
void dead(Connection o)
{
try {
((Connection)o).close();
}
catch (SQLException e) {
e.printStackTrace();
}
}
boolean validate(Connection o)
{
try {
return (!((Connection)o).isClosed());
}
catch (SQLException e) {
e.printStackTrace();
return (false);
}
}
}
class Main {
public static void main(String args[])
{
// Create the ConnectionPool:
JDBCConnectionPool pool = new JDBCConnectionPool(
"org.hsqldb.jdbcDriver", "jdbc:hsqldb: //localhost/mydb",
"sa", "password");
// Get a connection:
Connection con = pool.takeOut();
// Return the connection:
pool.takeIn(con);
}
}
Шаблон прототип (prototype) позволяет создавать объекты через клонирование других объектов вместо создания через конструктор. Такие шаблоны обычно применяют, когда требуется, чтобы система оставалась независимой как от процесса создания новых объектов, так и от типов порождаемых объектов.
Шаблон одиночка (singleton) это порождающий шаблон, который позволяет убедиться, что в процессе выполнения программы создается только один экземпляр класса с глобальным доступом. Данный шаблон можно использовать для координации с другими объектами, так как поля «Одиночки» будут одинаковы для всех, кто его вызывает.
Развить идею использования одиночек можно в шаблоне пул одиночек (Multiton). Здесь вы можете отделить логику шаблона от конкретной реализации. В примере представлен класс Nazgul
public enum NazgulName {
KHAMUL, MURAZOR, DWAR, JI_INDUR, AKHORAHIL, HOARMURATH, ADUNAPHEL, REN, UVATHA
}
public final class Nazgul {
private static final Map nazguls;
private final NazgulName name;
static {
nazguls = new ConcurrentHashMap<>();
nazguls.put(NazgulName.KHAMUL, new Nazgul(NazgulName.KHAMUL));
nazguls.put(NazgulName.MURAZOR, new Nazgul(NazgulName.MURAZOR));
nazguls.put(NazgulName.DWAR, new Nazgul(NazgulName.DWAR));
nazguls.put(NazgulName.JI_INDUR, new Nazgul(NazgulName.JI_INDUR));
nazguls.put(NazgulName.AKHORAHIL, new Nazgul(NazgulName.AKHORAHIL));
nazguls.put(NazgulName.HOARMURATH, new Nazgul(NazgulName.HOARMURATH));
nazguls.put(NazgulName.ADUNAPHEL, new Nazgul(NazgulName.ADUNAPHEL));
nazguls.put(NazgulName.REN, new Nazgul(NazgulName.REN));
nazguls.put(NazgulName.UVATHA, new Nazgul(NazgulName.UVATHA));
}
private Nazgul(NazgulName name) {
this.name = name;
}
public static Nazgul getInstance(NazgulName name) {
return nazguls.get(name);
}
public NazgulName getName() {
return name;
}
}
А обратиться к конкретному Назгулу можно с помощью следующего вызова:
LOGGER.info("DWAR={}", Nazgul.getInstance(NazgulName.DWAR));
LOGGER.info("DWAR={}", NazgulEnum.DWAR);
Структурные
Порождающие шаблоны, как и следует из названия должны в том или ином виде содействовать созданию других объектов. Структурные шаблоны имеют другую задачу — они определяют, как объекты могут использовать друг друга. Другими словами, структурные шаблоны определяют отношения между классами и объектами, позволяя им работать совместно.
Здесь тоже имеется несколько видов шаблонов. Рассмотрим кратко каждый из них.
Структурный шаблон адаптер (Adapter), предназначается для организации использования функций объекта, недоступного для модификации, через специально созданный интерфейс. Типичная проблема — имеются несовместимые объекты. С помощью шаблона адаптер их можно обернуть в адаптер, для совместимости с другим классом.
Предположим, у вас есть класс Bird с методами fly () и makeSound (). А также класс ToyDuck с методом squeak (). Давайте предположим, что у вас не хватает объектов ToyDuck, и вы хотели бы использовать объекты Bird вместо них. Класс Bird обладает некоторой схожей функциональностью, но реализует другой интерфейс, поэтому мы не можем использовать его напрямую. Так что мы будем использовать шаблон адаптера. Здесь нашим клиентом был бы ToyDuck, а адаптируемым — Bird.
interface Bird
{
// birds implement Bird interface that allows
// them to fly and make sounds adaptee interface
public void fly();
public void makeSound();
}
class Sparrow implements Bird
{
// a concrete implementation of bird
public void fly()
{
System.out.println("Flying");
}
public void makeSound()
{
System.out.println("Chirp Chirp");
}
}
interface ToyDuck
{
// target interface
// toyducks dont fly they just make
// squeaking sound
public void squeak();
}
class PlasticToyDuck implements ToyDuck
{
public void squeak()
{
System.out.println("Squeak");
}
}
class BirdAdapter implements ToyDuck
{
// You need to implement the interface your
// client expects to use.
Bird bird;
public BirdAdapter(Bird bird)
{
// we need reference to the object we
// are adapting
this.bird = bird;
}
public void squeak()
{
// translate the methods appropriately
bird.makeSound();
}
}
class Main
{
public static void main(String args[])
{
Sparrow sparrow = new Sparrow();
ToyDuck toyDuck = new PlasticToyDuck();
// Wrap a bird in a birdAdapter so that it
// behaves like toy duck
ToyDuck birdAdapter = new BirdAdapter(sparrow);
System.out.println("Sparrow...");
sparrow.fly();
sparrow.makeSound();
System.out.println("ToyDuck...");
toyDuck.squeak();
// toy duck behaving like a bird
System.out.println("BirdAdapter...");
birdAdapter.squeak();
}
}
Шаблон Мост (Bridge) используется для разделения абстракции и реализации таким образом, чтобы они могли изменяться независимо. Для разделения ответственности между классами данный шаблон использует инкапсуляцию, агрегирование и наследование.
В случае, если необходимо определить иерархию классов, состоящих одновременно из объектов, различных уровней сложности и при этом упростить процесс добавления новых видов объекта, то можно воспользоваться шаблоном компоновщик (Composite). Компоновщик объединяет объекты в древовидную структуру для представления иерархии от частного к целому.
Структурный шаблон Декоратор (Decorator). предназначен для динамического подключения дополнительного поведения к объекту, являясь альтернативой практике создания подклассов с целью расширения функциональности.
Зачастую возникает необходимость скрыть сложность системы от тех, кто будет с ней работать. Для этого существует шаблон с соответствующим названием — Фасад (Facade), который скрывает содержимое путём сведения всех возможных внешних вызовов к одному объекту, делегирующему их соответствующим объектам системы.
Поведенческие
Поведенческие шаблоны можно разделить на два вида: уровня класса (где используется наследование для определения поведения для различных классов) и уровня объекта (используется композиция). В целом, поведенческие шаблоны отвечают за то, как запустить поведение в программном компоненте и используются для того, чтобы упростить взаимодействие между сущностями. Рассмотрим несколько типов таких шаблонов.
Шаблон цепочка обязанностей (Chain of responsibility) — позволяет организовать в системе несколько уровней ответственности. Применяется, когда имеется более одного объекта, который может обработать определенный запрос или когда надо передать запрос на выполнение одному из нескольких объектов, точно не определяя, кому именно.
В примере ниже у нас есть интерфейс для обработки запроса и есть конкретные обработчики ConcreteHandler1 и ConcreteHandler2.
class Client
{
void Main()
{
Handler h1 = new ConcreteHandler1();
Handler h2 = new ConcreteHandler2();
h1.Successor = h2;
h1.HandleRequest(2);
}
}
abstract class Handler
{
public Handler Successor { get; set; }
public abstract void HandleRequest(int condition);
}
class ConcreteHandler1 : Handler
{
public override void HandleRequest(int condition)
{
// некоторая обработка запроса
if (condition==1)
{
// завершение выполнения запроса;
}
// передача запроса дальше по цепи при наличии в ней обработчиков
else if (Successor != null)
{
Successor.HandleRequest(condition);
}
}
}
class ConcreteHandler2 : Handler
{
public override void HandleRequest(int condition)
{
// некоторая обработка запроса
if (condition==2)
{
// завершение выполнения запроса;
}
// передача запроса дальше по цепи при наличии в ней обработчиков
else if (Successor != null)
{
Successor.HandleRequest(condition);
}
}
}
Команда (Command) — шаблон, представляющий действие. Объект команды заключает в себе само действие и его параметры. Он позволяет инкапсулировать действия в объекты, предоставляя средства для разделения клиента и получателя.
Посредник (Mediator) является поведенческим шаблоном, который обеспечивает взаимодействие множества объектов, позволяя объектам не ссылаться явно друг на друга.
Шаблон Итератор (Iterator) является объектом, с помощью которого можно получить последовательный доступ к элементам объекта-агрегата без использования описаний каждого из агрегированных объектов. Данный шаблон дает способ доступа к элементам объекта без показа базового представления.
В примере класс IteratorPatternDemo будет использовать NamesRepository, конкретную реализацию класса для печати имен, хранящихся в виде коллекции в NamesRepository.
abstract class Handler
public static void main(String[] args) {
NameRepository namesRepository = new NameRepository();
for(Iterator iter = namesRepository.getIterator(); iter.hasNext();){
String name = (String)iter.next();
System.out.println("Name : " + name);
}
}
}
Шаблон проектирования Хранитель (Memento) фиксирует и хранит текущее состояние объекта, чтобы оно легко восстанавливалось. То есть, мы можем, не нарушая инкапсуляцию, зафиксировать состояние объекта так, чтобы позднее восстановить его в этом состоянии.
Заключение
Тема шаблонов проектирования не ограничивается представленными в этой статье. Здесь мы рассмотрели только основные шаблоны проектирования и их виды.
Также приглашаю вас на бесплатный урок, где мы познакомимся с паттернами декомпозиции системы на микросервисы. Рассмотрим технический подход и бизнес-подход к декомпозиции.