— Привет, друг!

— Привет, Билаабо!

— У нас еще осталось немного времени, поэтому я расскажу тебе про еще три паттерна.

— Еще три, а сколько их всего?

— Ну, сейчас есть несколько десятков популярных паттернов, но количество «удачных решений» не ограничено.

— Ясно. И что, мне придется учить несколько десятков паттернов?

— Пока у тебя нет опыта реального программирования, они дадут тебе не очень много.

Ты лучше поднаберись опыта, а потом, через годик, вернись к этой теме и попробуй разобраться в них более основательно. Хотя бы пару десятков самых популярных.

Грех не пользоваться чужим опытом и самому что-то изобретать в очередной 110-й раз.

— Согласен.

— Тогда начнем.

Паттерн Adapter(Wrapper) – Адаптер (Обертка)

Паттерны: Adapter, Proxy, Bridge - 1

Представь, что ты приехал в Китай, а там другой стандарт розеток. Отверстия не круглые, а плоские. Тогда тебе понадобится переходник, или другими словами – адаптер.

В программировании тоже может быть что-то подобное. Классы оперируют похожими, но различными интерфейсами. И надо сделать переходник между ними.

Вот как это может выглядеть:

Пример
interface Time
{
 int getSeconds();
 int getMinutes();
 int getHours();
}

interface TotalTime
{
 int getTotalSeconds();
}

Допустим, у нас есть два интерфейса – Time и TotalTime.

Интерфейс Time позволяет узнать текущее время с помощью методов getSeconds(), getMinutes() и getHours().

Интерфейс TotalTime позволяет получить количество секунд, которое прошло от полночи до текущего момента.

Что делать, если у нас есть объект типа TotalTime, а нужен Time и наоборот?

Для этого мы можем написать классы-адаптеры. Пример:

Пример
 class TotalTimeAdapter implements Time
{
 private TotalTime totalTime;
 public TotalTimeAdapter(TotalTime totalTime)
 {
  this.totalTime = totalTime;
 }

 public int getSeconds() 
 {
  return totalTime.getTotalSeconds() % 60; //секунды
 }

 public int getMinutes()
 {
  return (totalTime.getTotalSeconds() % (60*60)) / 60; //минуты
 }

 public int getHours()
 {
  return totalTime.getTotalSeconds() / (60*60); //часы
 }
}
 
Как пользоваться
TotalTime totalTime = ... ; // программа получает объект, который реализует интерфейс TotalTime
Time time = new TotalTimeAdapter(totalTime);
System.out.println(time.getHours()+":"+time.getMinutes()+":"+time.getSeconds());

И адаптер в другую сторону:

Пример
class TimeAdapter implements TotalTime
{
 private Time time;
 public TimeAdapter(Time time)
 {
  this.time = time;
 }

 public int getTotalSeconds()
 {
  return time.getHours()*60*60+time.getMinutes()*60 + time.getSeconds(); 
 }
}
Как пользоваться
Time time = ... ; // программа получает объект, который реализует интерфейс Time
TotalTime totalTime = new TimeAdapter(time);
System.out.println(totalTime.getTotalSeconds());

— Ага. Мне нравится. А примеры есть?

— Конечно, например, InputStreamReader – это классический адаптер. Преобразовывает тип InputStream к типу Reader.

Иногда этот паттерн еще называют обертка, потому что новый класс как бы «оборачивает» собой другой объект.

Другие интересные вещи почитать можно тут.

Паттерн Proxy — Заместитель

Паттерн прокси чем-то похож на паттерн "обертка". Но его задача – не преобразовывать интерфейсы, а контролировать доступ к оригинальному объекту, сохраненному внутри прокси-класса. При этом и оригинальный класс и прокси обычно имеют один и тот же интерфейс, что облегчает подмену объекта оригинального класса, на объект прокси.

Пример:

Интерфейс реального класса
interface Bank
{
 public void setUserMoney(User user, double money);
 public int getUserMoney(User user);
}
Реализация оригинального класса
class CitiBank implements Bank
{
 public void setUserMoney(User user, double money)
 {
  UserDAO.updateMoney(user, money);
 }

 public int getUserMoney(User user)
 {
  return UserDAO.getMoney(user);
 }
}
Реализация прокси-класса
class BankSecurityProxy implements Bank
{
 private Bank bank;
 public BankSecurityProxy(Bank bank)
 {
  this.bank = bank;
 }
 public void setUserMoney(User user, double money)
 {
  if (!SecurityManager.authorize(user, BankAccounts.Manager))
  throw new SecurityException("User can’t change money value");

  bank.setUserMoney(user, money);
 }

 public int getUserMoney(User user)
 {
  if (!SecurityManager.authorize(user, BankAccounts.Manager))
  throw new SecurityException("User can’t get money value");

  return bank.getUserMoney(user);
 }
}

В примере выше мы описали интерфейс банка – Bank, и одну его реализацию – CitiBank.

Этот интерфейс позволяет получить или изменить количество денег на счету пользователя.

А потом мы создали BankSecurityProxy, который тоже реализует интерфейс Bank и хранит в себе ссылку на другой интерфейс Bank. Методы этого класса проверяют: является ли данный пользователь владельцем счета либо менеджером банка, и если нет – то кидает исключение безопасности – SecurityException.

Вот как это работает на деле:

Код без проверки безопасности:
User user = AuthManager.authorize(login, password);
Bank bank = BankFactory.createUserBank(user);
bank.setUserMoney(user, 1000000);
Код с включённой проверкой безопасности:
User user = AuthManager.authorize(login, password);
Bank bank = BankFactory.createUserBank(user);
bank = new BankSecurityProxy(bank);
bank.setUserMoney(user, 1000000);

В первом примере мы создаем объект банк и вызываем у него метод setUserMoney.

Во втором примере мы оборачиваем оригинальный объект банк в объект BankSecurityProxy. Интерфейс у них один, так что дальнейший код как работал, так и работает. Но теперь при каждом вызове метода будет происходить проверка безопасности.

— Круто!

— Ага. Таких прокси может быть много. Например, можно добавить еще один прокси, который будет проверять – не слишком ли большая сумма. Может менеджер банка решил положить себе на счет кучу денег и сбежать с ними на Кубу.

Более того. Создание всех этих цепочек объектов можно поместить в класс BankFactory и подключать/отключать нужные из них.

По похожему принципу работает BufferedReader. Это Reader, но который делает еще дополнительную работу.

Такой подход позволяет «собирать» из «кусочков» объект нужной тебе функциональности.

Чуть не забыл. Прокси используются гораздо шире, чем я только что тебе показал. Об остальных типах использования ты можешь почитать здесь.

Паттерн Bridge – Мост

Паттерны: Adapter, Proxy, Bridge - 2

Иногда, в процессе работы программы, надо сильно поменять функциональность объекта. Например, был у тебя в игре персонаж осел, а потом маг превратил его в дракона. У дракона совсем другое поведение и свойства, но(!) это – тот же самый объект.

— А нельзя просто создать новый объект и все?

— Не всегда. Допустим, твой осел был в друзьях у кучи персонажей, или например, на нем были наложены некоторые заклинания, или он участвовал в каких-то квестах. Т.е. этот объект уже может быть задействован в куче мест и привязан к куче других объектов. Так что просто создать новый другой объект в этом случае – не вариант.

— И что же делать?

— Одним из наиболее удачных решений есть паттерн Мост.

Этот паттерн предлагает разделить объект на два объекта. На «объект интерфейса» и «объект реализации».

— А в чем отличие от интерфейса и класса, который его реализует?

— В случае с интерфейсом и классом в результате будет создан один объект, а тут — два. Смотри пример:

Пример
class User
{
 private UserImpl realUser;

 public User(UserImpl impl)
 {
  realUser = impl;
 }

 public void run() //бежать
 {
  realUser.run();
 }

 public void fly() //лететь
 {
  realUser.fly();
 }
}

class UserImpl
{
 public void run() 
 {
 }

 public void fly()
 {
 }
}

А потом можно объявить несколько классов наследников от UserImpl, например UserDonkey(осел) и UserDragon(дракон).

— Все равно не очень понял, как это будет работать.

— Ну, примерно так:

Пример
class User
{
 private UserImpl realUser;

 public User(UserImpl impl)
 {
  realUser = impl;
 }

 public void transformToDonkey()
 {
  realUser = new UserDonkeyImpl();
 }

 public void transformToDragon()
 {
  realUser = new UserDragonImpl();
 }
}
Как это работает
User user = new User(new UserDonkey()); //внутри мы – осел
user.transformToDragon(); //теперь внутри мы – дракон

— Чем-то напоминает прокси.

— Да, только в прокси главный объект мог храниться где-то отдельно, а код работал с прокси-объектами вместо него. Здесь же предлагается, что все работают именно с главным объектом, а меняются его внутренние части.

— Ага. Спасибо. А дашь ссылку почитать еще про это?

— Конечно, друг Амиго. Держи: Паттерн Bridge