Когда вы обнаруживаете, что вам нужно добавить новые типы в систему, наиболее разумным первым шагом является использование полиморфизма для создания общего интерфейса для этих новых типов. Это отделит большую часть кода вашей системы от необходимости знания типов, которые вы добавляете. Новые типы могут быть добавлены без разрушения существующего кода или его структуры. Первоначально может показаться, что при таком дизайне вам необходимо внести изменения только в тот код, где вы наследуете новые типы, но это не полная правда. Вы все еще должны создать объект нового типа и в точке создания вы должны указать точный тип используемого конструктора. Таким образом, если код, создающий объекты, разнесен по всему вашему приложению, вы получаете ту же проблему, когда добавляете новые типы - вы должны выискивать все места в вашем коде, где этот тип встречается. Это происходит при создании типа, которое, в данном случае, имеет больше значения, чем использование типа (о котором позаботится полиморфизм), но эффект тот же: добавление нового типа может вызвать проблемы.
Решение состоит в том, чтобы форсировать создание объекта, которое происходит через общую фабрику, что позволит выделить код создания из вашей системы. Если весь код в вашей программе должен проходить через эту фабрику не зависимо от того, нужно ли создавать какой-либо из ваших объектов, то все, что вам нужно сделать при добавлении создания нового объекта - это изменить фабрику.
Так как при объектно-ориентированном программировании происходит создание объектов, и так как это очень желательно, вы должны заниматься в вашей программе созданием новых типов, я подозреваю, что фабрики могут стать наиболее универсальным и полезным типом шаблонов проектирования.
Хотя Метод Простейшей Фабрики на самом деле является синглетоном, вы найдете, что любой указанный класс фабрики в более общих случаях должен иметь только один экземпляр.
Метод простейшей фабрики (Simple Factory method)
В качестве примера позвольте вернуться к системе Форм (Shape).
Один из подходов состоит в создании статического метода производства в базовом классе:
//: factory:shapefact1:ShapeFactory1.java
// Метод простейшей фабрики.
package factory.shapefact1;
import java.util.*;
import junit.framework.*;
abstract class Shape {
public abstract void draw();
public abstract void erase();
public static Shape factory(String type) {
if (type.equals("Circle"))
return new Circle();
if (type.equals("Square"))
return new Square();
throw new RuntimeException("Bad shape creation: " + type);
}
}
class Circle extends Shape {
Circle() {
} // Конструктор с пакетным уровнем доступа
public void draw() {
System.out.println("Circle.draw");
}
public void erase() {
System.out.println("Circle.erase");
}
}
class Square extends Shape {
Square() {
} // Конструктор с пакетным уровнем доступа
public void draw() {
System.out.println("Square.draw");
}
public void erase() {
System.out.println("Square.erase");
}
}
public class ShapeFactory1 extends TestCase {
String shlist[] = { "Circle", "Square", "Square", "Circle", "Circle",
"Square" };
List shapes = new ArrayList();
public void test() {
Iterator it = Arrays.asList(shlist).iterator();
while (it.hasNext())
shapes.add(Shape.factory((String) it.next()));
it = shapes.iterator();
while (it.hasNext()) {
Shape s = (Shape) it.next();
s.draw();
s.erase();
}
}
public static void main(String args[]) {
junit.textui.TestRunner.run(ShapeFactory1.class);
}
} // /:~
Метод factory( ) принимает аргумент, который позволяет определить тип создаваемой Формы (Shape); в данном случае это Строка (String), но это может быть любой набор данных. Теперь только factory( ) является частью кода, которую нужно менять при добавлении нового типа Формы (Shape) (инициализирующие данные для объекта будут, предположительно, приходить откуда-то извне системы, и не будут жестко заданным массивом, как в приведенном выше примере).
Для поощрения создания только с помощью метода factory( ), конструктор для указанных типов Формы (Shape) сделан с пакетным уровнем доступа, так что метод factory( ) имеет доступ к конструкторам, но они не доступны для классов вне этого пакета.
Полиморфные фабрики (Polymorphic factories)
Статический метод factory( ) в предыдущем примере заставляет все операции создания сфокусировать в одном месте, так что вы должны менять код только в одном месте. Это, несомненно, резонное решение, в этом случае мы получаем как бы ящик вокруг процесса создания объектов. Однако, книга Design Patterns акцентирует, что поводом для Метода Фабрики является то, что различные типы фабрик могут быть подклассами базовой фабрики (вышеприведенный дизайн упоминается, как особый случай). Но, в книге не приводится пример, а вместо этого повторяется пример, использованный для Абстрактной Фабрики (вы увидите этот пример в следующем разделе). Здесь же приводится модифицированный пример ShapeFactory1.java, так что методы-фабрики находятся в различных классах, как виртуальные функции. Обратите внимание, что определенный класс Формы (Sahpe) загружается динамически по требованию:
//: factory:shapefact2:ShapeFactory2.java
// .
package factory.shapefact2;
import java.util.*;
import junit.framework.*;
interface Shape {
void draw();
void erase();
}
abstract class ShapeFactory {
protected abstract Shape create();
private static Map factories = new HashMap();
public static void addFactory(String id, ShapeFactory f) {
factories.put(id, f);
}
// :
public static final Shape createShape(String id) {
if (!factories.containsKey(id)) {
try {
//
Class.forName("factory.shapefact2." + id);
}
catch (ClassNotFoundException e) {
throw new RuntimeException("Bad shape creation: " + id);
}
// :
if (!factories.containsKey(id))
throw new RuntimeException("Bad shape creation: " + id);
}
return ((ShapeFactory) factories.get(id)).create();
}
}
class Circle implements Shape {
private Circle() {
}
public void draw() {
System.out.println("Circle.draw");
}
public void erase() {
System.out.println("Circle.erase");
}
private static class Factory extends ShapeFactory {
protected Shape create() {
return new Circle();
}
}
static {
ShapeFactory.addFactory("Circle", new Factory());
}
}
class Square implements Shape {
private Square() {
}
public void draw() {
System.out.println("Square.draw");
}
public void erase() {
System.out.println("Square.erase");
}
private static class Factory extends ShapeFactory {
protected Shape create() {
return new Square();
}
}
static {
ShapeFactory.addFactory("Square", new Factory());
}
}
public class ShapeFactory2 extends TestCase {
String shlist[] = { "Circle", "Square", "Square", "Circle", "Circle",
"Square" };
List shapes = new ArrayList();
public void test() {
// ,
// .
Iterator it = Arrays.asList(shlist).iterator();
while (it.hasNext())
shapes.add(ShapeFactory.createShape((String) it.next()));
it = shapes.iterator();
while (it.hasNext()) {
Shape s = (Shape) it.next();
s.draw();
s.erase();
}
}
public static void main(String args[]) {
junit.textui.TestRunner.run(ShapeFactory2.class);
}
} // /:~
Теперь метод фабрики появился в своем собственном классе ShapeFactory, как метод create( ). Это метод с уровнем доступа protected, что означает, что он не может быть вызван напрямую, но он может быть перегружен. Каждый из производных классов от Формы (Shape) должен создать свой собственный подкласс ShapeFactory и перегрузить метод create( ), который создает объект своего собственного типа. Реальное создание формы выполняется путем вызова метода ShapeFactory.createShape( ), который является статическим методом, использующим класс Map в классе ShapeFactory для нахождения походящего фабричного объекта, основываясь на переданном вами идентификаторе. Фабрика немедленно используется для создания объекта формы, но вы должны представлять себе более сложный процесс, при котором возвращается соответствующий производящий объект, а затем он используется вызывателем для создания объекта более искусным способом. Однако, это выглядит так, что большую часть времени вам не нужно путаться в полиморфных методах фабрик, а можно использовать единый статический метод базового класса (как показано в ShapeFactory1.java).
Обратите внимание, что ShapeFactory должен быть проинициализирован при загрузке в Map объектом-фабрикой, что имеет место в статическом блоке каждой реализации Формы (Shape). Так что при добавлении нового типа при таком дизайне вы должны наследовать тип, создать фабрику и добавить блок статической инициализации при загрузке в Map. Эта дополнительная сложность снова возвращает нас к статическому методу фабрики, если вам не нужно создавать индивидуальных объектов фабрик.
Абстрактная фабрика (Abstract factories)
Шаблон Абстрактной Фабрики выглядит так же, как фабрика объектов, рассмотренная в предыдущем разделе, но не с одним, а с несколькими методами фабрики. Каждый из методов фабрики создает объект различного рода. Эта идея состоит в том, что в момент создания объекта фабрики вы решаете как все объекты, созданные фабрикой, будут использованы. Пример, данный в книге Design Patterns реализует вероятность использования различных графических интерфейсов пользователя (GUI). Вы создаете объект фабрики для того GUI, с которым вы работаете, а из него по запросу автоматически создаете меню, кнопку, слайдер и т. п. в соответствии с версией GUI. Таким образом вы способны изолировать в одном месте эффект изменения GUI.
Другой, предлагаемый вам пример, состоит в создании игровой среды общего назначения, в которой вы хотите иметь возможность поддерживать различные типы игр. Здесь описано, как это может выглядеть с использованием абстрактной фабрики:
//: factory:Games.java
// Пример шаблона Абстрактной фабрики.
package factory;
import junit.framework.*;
interface Obstacle {
void action();
}
interface Player {
void interactWith(Obstacle o);
}
class Kitty implements Player {
public void interactWith(Obstacle ob) {
System.out.print("Kitty has encountered a ");
ob.action();
}
}
class KungFuGuy implements Player {
public void interactWith(Obstacle ob) {
System.out.print("KungFuGuy now battles a ");
ob.action();
}
}
class Puzzle implements Obstacle {
public void action() {
System.out.println("Puzzle");
}
}
class NastyWeapon implements Obstacle {
public void action() {
System.out.println("NastyWeapon");
}
}
// Абстрактная фабрика:
interface GameElementFactory {
Player makePlayer();
Obstacle makeObstacle();
}
// Конкретный фабрики:
class KittiesAndPuzzles implements GameElementFactory {
public Player makePlayer() {
return new Kitty();
}
public Obstacle makeObstacle() {
return new Puzzle();
}
}
class KillAndDismember implements GameElementFactory {
public Player makePlayer() {
return new KungFuGuy();
}
public Obstacle makeObstacle() {
return new NastyWeapon();
}
}
class GameEnvironment {
private GameElementFactory gef;
private Player p;
private Obstacle ob;
public GameEnvironment(GameElementFactory factory) {
gef = factory;
p = factory.makePlayer();
ob = factory.makeObstacle();
}
public void play() {
p.interactWith(ob);
}
}
public class Games extends TestCase {
GameElementFactory kp = new KittiesAndPuzzles(),
kd = new KillAndDismember();
GameEnvironment g1 = new GameEnvironment(kp), g2 = new GameEnvironment(kd);
// Здесь просто убеждаемся что не было выброшено исключений:
public void test1() {
g1.play();
}
public void test2() {
g2.play();
}
public static void main(String args[]) {
junit.textui.TestRunner.run(Games.class);
}
} // /:~
В этой среде объект Player взаимодействует с объектом Obstacle, но есть разные типы игроков и препятствий в зависимости от рода игры, в которую вы играете. Вы определяете род игры путем создания определенного класса GameElementFactory, а затем GameEnvironment управляет установками и ходом игры. В этом примере установка и ход игры очень просты, но активность (начальные состояния и изменение состояния) может определять многое в ходе игры. Здесь класс GameEnvironment не предназначен для наследования, хотя нельзя исключить возможность сделать так.
Здесь также содержится примеры Двойной Диспетчеризации и Метода Фабрики (Factory method), которые будут объяснены позже.
Упражнения
- Добавьте класс треугольника в пример ShapeFactory1.java.
- Добавьте класс треугольника в пример ShapeFactory2.java.
- Добавьте новый тип GameEnvironment, называющийся GnomesAndFairies в пример Games.java.
- Измените ShapeFactory2.java таким образом, чтобы он использовал Абстрактную фабрику для создания различных наборов форм (например одна фабрика определенного типа создает "толстые формы", а другая фабрика создает "тонкие формы", но каждый объект фабрики может создавать все формы: круги, квадраты, треугольники и т. п.)
← | Выделение общности (Factoring commonality) | Специализированное создание (Specialized creation) | → |