— Привет, Амиго!

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

— Сегодня у нас будет не просто интересная, а прямо-таки эпическая тема.

Сегодня я расскажу тебе, что такое шаблоны проектирования (design patterns).

— Круто. Много про них слышал. Жду с нетерпением.

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

Чем чаще они решали такие вопросы, тем чаще стали понимать, что существуют некоторые удачные решения, и наоборот, неудачные.

Неудачные решения обычно создают проблем больше, чем решают. Они плохо расширяются, создают много лишних ограничений, и т.п. А удачные решения – наоборот.

— А можно какую-нибудь аналогию?

— Допустим, ты строишь дом. И думаешь — из чего же он должен состоять. Ты решил, что тебе нужны стены, пол и потолок. В результате ты построил дом без фундамента и с ровной крышей. Такой дом будет трескаться, а крыша – протекать. Это – неудачное решение.

И наоборот: дом, состоящий из фундамента, стен и двускатной крыши будет удачным решением. Ему нестрашны ни большие снегопады (снег будет скатываться с крыши), ни подвижки почвы – фундамент будет обеспечивать стабильность. Такое решение мы назовем удачным.

— Ясно. Спасибо.

— Ок. Тогда я продолжу.

Со временем сборник удачных решений объявили «паттернами (шаблонами) проектирования», а сборник неудачных – антипаттернами.

Сам шаблон проектирования — это как бы ответ на вопрос. Его сложно понять, если не слышал самого вопроса.

Первая категория паттернов – это порождающие паттерны. Такие паттерны описывают удачные решения, связанные с созданием объектов.

— А что такого сложного в создании объектов?

— А вот как раз сейчас мы это и разберем.

Паттерн Singleton – Синглетон, Одиночка.

Паттерны проектирования: Singleton, Factory, FactoryMethod, AbstractFactory - 1

Часто в программе некоторые объекты могут существовать только в единственном экземпляре. Например, консоль, логгер, сборщик мусора и т.д.

Неудачное решение: отказаться от создания объектов, просто создать класс у которого все методы статические.

Удачное решение: создать единственный объект класса и хранить его в статической переменной. Запретить создание второго объекта этого класса. Пример:

Пример
class Sun
{
 private static Sun instance;
 public static Sun getInstance()
 {
  if (instance == null)
  instance = new Sun();

  return instance;
 }

 private Sun()
 {
 }
}
Как вызвать
Sun sun = Sun.getInstance();

Все просто.

Во-первых, мы сделали конструктор private. Теперь его можно вызвать только изнутри нашего класса. Мы запретили создание объекта Sun везде кроме методов класса Sun.

Во-вторых, получить объект этого класса можно только вызвав метод getInstance(). Это не только единственный метод, который может создавать объекты класса Sun, но он еще и следит, чтобы такой объект был единственным.

— Ясно.

— Когда человек думает – как же именно это сделать? Паттерн говорит – можешь попробовать так – это одно из удачных решений.

— Спасибо. Теперь что-то начинает проясняться.

— Также про этот паттерн можно прочитать здесь.

Паттерн Factory – Фэктори, Фабрика.

Паттерны проектирования: Singleton, Factory, FactoryMethod, AbstractFactory - 2

Очень часто программисты сталкиваются вот с какой ситуацией. У тебя есть некоторый базовый класс и много подклассов. Например – персонаж игры – GamePerson и классы всех остальных персонажей игры, унаследованные от него.

Допустим, у тебя есть такие классы:

Пример
abstract class GamePerson
{
}

class Warrior extends GamePerson
{
}

class Mag extends GamePerson
{
}

class Troll extends GamePerson
{
}

class Elv extends GamePerson
{
}

Вопрос состоит в том, как гибко и удобно управлять созданием объектов этих классов.

Если проблема кажется тебе надуманной, представь, что в игре нужно создавать десятки мечей и щитов, сотни магических заклинаний, тысячи монстров. Без удобного подхода к созданию объектов тут не обойтись.

Вот какое «удачное решение» предлагает паттерн Фабрика (Factory).

Во-первых, надо завести enum, значения которого будут соответствовать различным классам.

Во-вторых, сделать специальный класс – Factory, у которого будет статический метод или методы, которые и будут заниматься созданием объекта(ов) в зависимости от enum’а.

Пример:

Пример
public enum PersonType
{
 UNKNOWN,
 WARRIOR,
 MAG,
 TROLL,
 ELV,
}

public class PersonFactory
{
 public static GamePerson createPerson(PersonType personType)
 {
  switch(personType)
  {
   WARRIOR:
   return new Warrior();
   MAG:
   return new Mag();
   TROLL:
   return new Troll();
   ELV:
   return new Elv();
   default:
   throw new GameException();
  }
 }
}
Как вызвать
GamePerson person = PersonFactory.createPerson(PersonType.MAG);

— Т.е. мы создали специальный класс для управления созданием объектов?

— Ага.

— А какие преимущества это дает?

— Во-первых, там эти объекты можно инициализировать нужными данными.

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

В третьих, количество значений enum не обязательно должно совпадать с количеством классов. Типов персонажей может быть много, а классов – мало.

Например, для Mag & Warrior можно использовать один класс – Human, но с различными настройками силы и магии (параметрами конструктора).

Вот как это может выглядеть (для наглядности добавил еще темных эльфов):

Пример
public enum PersonType
{
 UNKNOWN,
 WARRIOR,
 MAG,
 TROLL,
 ELV,
 DARK_ELV
}

public class PersonFactory
{
 public static GamePerson createPerson(PersonType personType)
 {
  switch(personType)
  {
   WARRIOR:
   return new Human(10, 0); //сила, магия
   MAG:
   return new Human(0, 10); //сила, магия
   TROLL:
   OGR:
   return new Troll();
   ELV:
   return new Elv(true); //true – добрый, false – злой
   DARK_ELV:
   return new Elv(false); //true – добрый, false - злой
   default:
   throw new GameException();
  }
 }
}
Как вызвать
GamePerson person = PersonFactory.createPerson(PersonType.MAG);

В примере выше мы использовали всего три класса, чтобы создавать объекты шести разных типов. Это очень удобно. Более того, у нас вся эта логика сосредоточена в одном классе и в одном методе.

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

— Согласен. Удачное решение.

— А я тебе о чем говорю, шаблоны проектирования – это сборники удачных решений.

— Знать бы еще, где их применять…

— Ага. Сходу не разберёшься, согласен. Но все равно, лучше знать и не уметь, чем не знать и не уметь. Вот тебе еще полезная ссылка по этому паттерну: Паттерн Factory

— О, спасибо.

— Паттерн Abstract Factory – Абстрактная Фабрика.

Иногда, когда объектов очень много, напрашивается создание фабрики фабрик. Такую фабрику принято называть Абстрактной Фабрикой.

— Это ж где такое может понадобиться?

— Ну, например, у тебя есть несколько групп идентичных объектов. Это легче показать на примере.

Смотри, допустим, у тебя в игре есть три расы – люди, эльфы и демоны. И у каждой расы для баланса есть воин, лучник и маг. В зависимости от того, за какую сторону играет человек, он может создавать только объекты своей расы. Вот как это могло бы выглядеть в коде:

Объявление классов войск
class Warrior
{
}
class Archer
{
}
class Mag
{
}
Люди
class HumanWarrior extends Warrior
{
}

class HumanArcher extends Archer
{
}

class HumanMag extends Mag
{
}
Эльфы
class ElvWarrior extends Warrior
{
}

class ElvArcher extends Archer
{
}

class ElvMag extends Mag
{
}
Демоны
class DaemonWarrior extends Warrior
{
}

class DaemonArcher extends Archer
{
}

class DaemonMag extends Mag
{
}

А теперь создадим расы, ну или можно еще назвать их армиями.

Армии
abstract class Army
{
 public Warrior createWarrior();
 public Archer createArcher();
 public Mag createMag();
}
Армия людей
class HumanArmy extends Army
{
 public Warrior createWarrior()
 {
  return new HumanWarrior();
 }
 public Archer createArcher()
 {
  return new HumanArcher();
 }
 public Mag createMag()
 {
  return new HumanMag();
 }
}
Армия эльфов
class ElvArmy extends Army
{
 public Warrior createWarrior()
 {
  return new ElvWarrior();
 }
 public Archer createArcher()
 {
  return new ElvArcher();
 }
 public Mag createMag()
 {
  return new ElvMag();
 }
}
Армия демонов
class DaemonArmy extends Army
{
 public Warrior createWarrior()
 {
  return new DaemonWarrior();
 }
 public Archer createArcher()
 {
  return new DaemonArcher();
 }
 public Mag createMag()
 {
  return new DaemonMag();
 }
}

— А как это использовать?

— Можно везде в программе использовать классы Army, Warrior, Archer, Mag, а для создания нужных объектов – просто передавать объект нужного класса-наследника Army.

Пример:

Пример
Army humans = new HumanArmy();
Army daemons = new DaemonArmy();

Army winner = FightSimulator.simulate(humans, daemons);

В примере выше у нас есть класс, который симулирует бои между разными расами (армиями), и ему нужно просто передать два объекта Army. С их помощью он сам создает различные войска и проводит виртуальные бои между ними, с целью выявить победителя.

— Ясно. Спасибо. Действительно интересный подход.

Удачное решение, что ни говори.

— Ага.

Вот еще хорошая ссылка по этой теме: Паттерн Abstract Factory