Водоснабжение и отопление частного дома является важной составляющей хорошего дома.

Плавающий вес (Flyweight): слишком много объектов

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

Плавающий вес решает эту проблему путем снижения числа объектов. Чтобы сделать это вы создаете расширенные данные в объекте, так что вы можете представить, что имеете больше объектов, чем это есть на самом деле. Однако, это внесет сложность в интерфейсы, использующие такие объекты, поскольку вы должны передавать дополнительную информацию в вызываемый метод, чтобы сказать методу, как найти расширенную информацию.

В качестве очень простого примера рассмотрим объект DataPoint, который хранит int, float, и id, содержащий номер объекта. Предположим, вам нужно создать миллион таких объектов, а затем манипулировать ими подобным образом:

//: flyweight:ManyObjects.java
class DataPoint {
  
private static int count = 0;
  
private int id = count++;
  
private int i;
  
private float f;
  
  
public int getI() {
     
return i;
  
}
  
  
public void setI(int i) {
     
this.i = i;
  
}
  
  
public float getF() {
     
return f;
  
}
  
  
public void setF(float f) {
     
this.f = f;
  
}
  
  
public String toString() {
     
return "id: " + id + ", i = " + i + ", f = " + f;
  
}
}

public class ManyObjects {
  
static final int size = 1000000;
  
  
public static void main(String[] args) {
     
DataPoint[] array = new DataPoint[size];
     
for (int i = 0; i < array.length; i++)
        
array[i] = new DataPoint();
     
for (int i = 0; i < array.length; i++) {
        
DataPoint dp = array[i];
         dp.setI
(dp.getI() + 1);
         dp.setF
(47.0f);
     
}
     
System.out.println(array[size - 1]);
  
}
}
// /:~

В зависимости от вашего компьютера, эта программа может выполняться несколько секунд. Более сложные объекты и более запутанные операции могут стать причиной увеличения накладных расходов и этот метод станет неприемлемым. Для решения проблемы DataPoint можно снизить количество объектов с миллиона до одного объекта, введя хранитель расширенных данных в DataPoint:

//: flyweight:FlyWeightObjects.java
class ExternalizedData {
  
static final int size = 5000000;
  
static int[] id = new int[size];
  
static int[] i = new int[size];
  
static float[] f = new float[size];
  
static {
     
for (int i = 0; i < size; i++)
        
id[i] = i;
  
}
}

class FlyPoint {
  
private FlyPoint() {
   }
  
  
public static int getI(int obnum) {
     
return ExternalizedData.i[obnum];
  
}
  
  
public static void setI(int obnum, int i) {
     
ExternalizedData.i[obnum] = i;
  
}
  
  
public static float getF(int obnum) {
     
return ExternalizedData.f[obnum];
  
}
  
  
public static void setF(int obnum, float f) {
     
ExternalizedData.f[obnum] = f;
  
}
  
  
public static String str(int obnum) {
     
return "id: " + ExternalizedData.id[obnum] + ", i = "
           
+ ExternalizedData.i[obnum] + ", f = "
           
+ ExternalizedData.f[obnum];
  
}
}

public class FlyWeightObjects {
  
public static void main(String[] args) {
     
for (int i = 0; i < ExternalizedData.size; i++) {
        
FlyPoint.setI(i, FlyPoint.getI(i) + 1);
         FlyPoint.setF
(i, 47.0f);
     
}
     
System.out.println(FlyPoint.str(ExternalizedData.size - 1));
  
}
}
// /:~

Так как все данные теперь хранятся в ExternalizedData, каждый вызов метода FlyPoint должен включать индекс для ExternalizedData. Чтобы быть последовательным, и чтобы напомнить читателю схожесть с явным указателем this при вызове метода, "this индекс" передается в метод в качестве первого аргумента.

Естественно, здесь стоит повторить относительно преждевременной оптимизации. "Сначала заставьте это работать, затем сделайте это быстрее - если это заработало". Также, профайлер - это инструмент, который используется для обнаружения узких мест, а не для гадания.

Декоратор (Decorator): Слишком много классов

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

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

Все декораторы, которые являются обертками оригинального объекта, должны иметь одинаковый основной интерфейс.

Динамический прокси/суррогат?

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

Компромисс: кодирование более сложно при использовании декораторов.

Основная структура декоратора

Decorator

Кофейный пример

Рассмотрим пример похода в местный кафетерий, BeanMeUp, за кофе. Обычно там предлагаются различные варианты -- эспрессо, lattes, чаи, кофе с мороженным, горячий шоколад нескольких наименования, точно так же множество дополнительных (за дополнительные деньги) взбитых кремов или очень крепкий эспрессо. Вы также можете заказать некоторые замены в вашем напитке без дополнительной оплаты, например, попросить кофе без кофеина вместо обычного кофе.

Понятно, что если мы перейдем к моделям всех этих напитков и их комбинаций, то получим значительную диаграмму классов. Так что для ясности мы рассмотрим только подмножество кофе: Эспрессо, Эспрессо Con Panna, Café Late, Капучино и Café Mocha. Также добавим 2 дополнительных взбитых крема ("взбитых") и очень крепкий эспрессо. Также введем три варианта изменений: без кофеина, парные сливки ("мокрые") и вспененные сливки ("сухие").

Класс для каждой комбинации

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

Decorator

Вот одна из комбинаций: простая реализация капучино:

class Cappuccino {
  
private float cost = 1;
  
private String description = "Cappucino";
  
  
public float getCost() {
     
return cost;
  
}
  
  
public String getDescription() {
     
return description;
  
}
}

Ключевым моментом для использования этого метода является нахождение обычных комбинаций того, что вы хотите. Так, как только вы найдете напиток, который вы хотите, вы увидите, как его использовать, как показано в классе CoffeShop следующего примера:

//: decorator:nodecorators:CoffeeShop.java
// Кофейный пример без декоратора
package decorator.nodecorators;

import junit.framework.*;

class Espresso {
}

class DoubleEspresso {
}

class EspressoConPanna {
}

class Cappuccino {
  
private float cost = 1;
  
private String description = "Cappucino";
  
  
public float getCost() {
     
return cost;
  
}
  
  
public String getDescription() {
     
return description;
  
}
}

class CappuccinoDecaf {
}

class CappuccinoDecafWhipped {
}

class CappuccinoDry {
}

class CappuccinoDryWhipped {
}

class CappuccinoExtraEspresso {
}

class CappuccinoExtraEspressoWhipped {
}

class CappuccinoWhipped {
}

class CafeMocha {
}

class CafeMochaDecaf {
}

class CafeMochaDecafWhipped {
  
private float cost = 1.25f;
  
private String description = "Cafe Mocha decaf whipped cream";
  
  
public float getCost() {
     
return cost;
  
}
  
  
public String getDescription() {
     
return description;
  
}
}

class CafeMochaExtraEspresso {
}

class CafeMochaExtraEspressoWhipped {
}

class CafeMochaWet {
}

class CafeMochaWetWhipped {
}

class CafeMochaWhipped {
}

class CafeLatte {
}

class CafeLatteDecaf {
}

class CafeLatteDecafWhipped {
}

class CafeLatteExtraEspresso {
}

class CafeLatteExtraEspressoWhipped {
}

class CafeLatteWet {
}

class CafeLatteWetWhipped {
}

class CafeLatteWhipped {
}

public class CoffeeShop extends TestCase {
  
public void testCappuccino() {
     
// Просто убеждаемся, что отработало
      // без выбрасывания исключений.
      // Создаем обычный капучино
     
Cappuccino cappuccino = new Cappuccino();
      System.out.println
(cappuccino.getDescription() + ": $"
           
+ cappuccino.getCost());
  
}
  
  
public void testCafeMocha() {
     
// Просто убеждаемся, что отработало
      // без выбрасывания исключений.
      // Create a decaf cafe mocha with whipped
      // cream
     
CafeMochaDecafWhipped cafeMocha = new CafeMochaDecafWhipped();
      System.out.println
(cafeMocha.getDescription() + ": $"
           
+ cafeMocha.getCost());
  
}
  
  
public static void main(String[] args) {
     
junit.textui.TestRunner.run(CoffeeShop.class);
  
}
}
// /:~

А здесь то, что получаем на выходе:

Cappucino: $1.0
Cafe Mocha decaf whipped cream: $1.25

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

Подход с использованием декоратора

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

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

Метод, вызванный у Декоратора, может привести к вызову соответствующего метода компонента, и, конечно, может выполнить обработку перед или после вызова.

Так, если мы добавит методы getTotalCost() and getDescription() в интерфейс DrinkComponent, Эспрессо будет выглядеть следующим образом:

class Espresso extends Decorator {
  
private float cost = 0.75f;
  
private String description = " espresso";
  
  
public Espresso(DrinkComponent component) {
     
super(component);
  
}
  
  
public float getTotalCost() {
     
return component.getTotalCost() + cost;
  
}
  
  
public String getDescription() {
     
return component.getDescription() + description;
  
}
}

Вы комбинируете компоненты для создания напитка следующим образом:

//: decorator:alldecorators:CoffeeShop2.java
// Кофейный пример с использованием декоратора.
package decorator.alldecorators;

import junit.framework.*;

interface DrinkComponent {
  
String getDescription();
  
  
float getTotalCost();
}

class Mug implements DrinkComponent {
  
public String getDescription() {
     
return "mug";
  
}
  
  
public float getTotalCost() {
     
return 0;
  
}
}

abstract class Decorator implements DrinkComponent {
  
protected DrinkComponent component;
  
   Decorator
(DrinkComponent component) {
     
this.component = component;
  
}
  
  
public float getTotalCost() {
     
return component.getTotalCost();
  
}
  
  
public abstract String getDescription();
}

class Espresso extends Decorator {
  
private float cost = 0.75f;
  
private String description = " espresso";
  
  
public Espresso(DrinkComponent component) {
     
super(component);
  
}
  
  
public float getTotalCost() {
     
return component.getTotalCost() + cost;
  
}
  
  
public String getDescription() {
     
return component.getDescription() + description;
  
}
}

class Decaf extends Decorator {
  
private String description = " decaf";
  
  
public Decaf(DrinkComponent component) {
     
super(component);
  
}
  
  
public String getDescription() {
     
return component.getDescription() + description;
  
}
}

class FoamedMilk extends Decorator {
  
private float cost = 0.25f;
  
private String description = " foamed milk";
  
  
public FoamedMilk(DrinkComponent component) {
     
super(component);
  
}
  
  
public float getTotalCost() {
     
return component.getTotalCost() + cost;
  
}
  
  
public String getDescription() {
     
return component.getDescription() + description;
  
}
}

class SteamedMilk extends Decorator {
  
private float cost = 0.25f;
  
private String description = " steamed milk";
  
  
public SteamedMilk(DrinkComponent component) {
     
super(component);
  
}
  
  
public float getTotalCost() {
     
return component.getTotalCost() + cost;
  
}
  
  
public String getDescription() {
     
return component.getDescription() + description;
  
}
}

class Whipped extends Decorator {
  
private float cost = 0.25f;
  
private String description = " whipped cream";
  
  
public Whipped(DrinkComponent component) {
     
super(component);
  
}
  
  
public float getTotalCost() {
     
return component.getTotalCost() + cost;
  
}
  
  
public String getDescription() {
     
return component.getDescription() + description;
  
}
}

class Chocolate extends Decorator {
  
private float cost = 0.25f;
  
private String description = " chocolate";
  
  
public Chocolate(DrinkComponent component) {
     
super(component);
  
}
  
  
public float getTotalCost() {
     
return component.getTotalCost() + cost;
  
}
  
  
public String getDescription() {
     
return component.getDescription() + description;
  
}
}

public class CoffeeShop2 extends TestCase {
  
public void testCappuccino() {
     
// В этом месте просто проверяем
      // что не было выброшено исключений.
      // Создаем простой капучино
     
DrinkComponent cappuccino = new Espresso(new FoamedMilk(new Mug()));
      System.out.println
(cappuccino.getDescription().trim() + ": $"
           
+ cappuccino.getTotalCost());
  
}
  
  
public void testCafeMocha() {
     
// В этом месте просто проверяем
      // что не было выброшено исключений.
      // Создаем кофе без кофеина со взбитым
      // кремом
     
DrinkComponent cafeMocha = new Espresso(new SteamedMilk(new Chocolate(
           
new Whipped(new Decaf(new Mug())))));
      System.out.println
(cafeMocha.getDescription().trim() + ": $"
           
+ cafeMocha.getTotalCost());
  
}
  
  
public static void main(String[] args) {
     
junit.textui.TestRunner.run(CoffeeShop2.class);
  
}
}
// /:~

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

Если вы хотите описать обычный капучино, вы создаете его так:

new Espresso ( new FoamedMilk ( new Mug ()))

Создание Café Mocha без кофеина со взбитым кремом требует более длинного описания.

Компромисс

Предыдущий подход был длинным в описании кофе. Существуют определенные комбинации, которые вы описываете регулярно, и было бы достаточно последовательно иметь быстрый способ их описания.

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

Ниже описано, как создать основной раздел, а так же, как создать секцию декорирования:

//: decorator:compromise:CoffeeShop3.java
// Кофейный пример с компромиссом
// основный комбинаций и декорации
package decorator.compromise;

import junit.framework.*;

interface DrinkComponent {
  
float getTotalCost();
  
   String getDescription
();
}

class Espresso implements DrinkComponent {
  
private String description = "Espresso";
  
private float cost = 0.75f;
  
  
public float getTotalCost() {
     
return cost;
  
}
  
  
public String getDescription() {
     
return description;
  
}
}

class EspressoConPanna implements DrinkComponent {
  
private String description = "EspressoConPare";
  
private float cost = 1;
  
  
public float getTotalCost() {
     
return cost;
  
}
  
  
public String getDescription() {
     
return description;
  
}
}

class Cappuccino implements DrinkComponent {
  
private float cost = 1;
  
private String description = "Cappuccino";
  
  
public float getTotalCost() {
     
return cost;
  
}
  
  
public String getDescription() {
     
return description;
  
}
}

class CafeLatte implements DrinkComponent {
  
private float cost = 1;
  
private String description = "Cafe Late";
  
  
public float getTotalCost() {
     
return cost;
  
}
  
  
public String getDescription() {
     
return description;
  
}
}

class CafeMocha implements DrinkComponent {
  
private float cost = 1.25f;
  
private String description = "Cafe Mocha";
  
  
public float getTotalCost() {
     
return cost;
  
}
  
  
public String getDescription() {
     
return description;
  
}
}

abstract class Decorator implements DrinkComponent {
  
protected DrinkComponent component;
  
  
public Decorator(DrinkComponent component) {
     
this.component = component;
  
}
  
  
public float getTotalCost() {
     
return component.getTotalCost();
  
}
  
  
public String getDescription() {
     
return component.getDescription();
  
}
}

class ExtraEspresso extends Decorator {
  
private float cost = 0.75f;
  
  
public ExtraEspresso(DrinkComponent component) {
     
super(component);
  
}
  
  
public String getDescription() {
     
return component.getDescription() + " extra espresso";
  
}
  
  
public float getTotalCost() {
     
return cost + component.getTotalCost();
  
}
}

class Whipped extends Decorator {
  
private float cost = 0.50f;
  
  
public Whipped(DrinkComponent component) {
     
super(component);
  
}
  
  
public float getTotalCost() {
     
return cost + component.getTotalCost();
  
}
  
  
public String getDescription() {
     
return component.getDescription() + " whipped cream";
  
}
}

class Decaf extends Decorator {
  
public Decaf(DrinkComponent component) {
     
super(component);
  
}
  
  
public String getDescription() {
     
return component.getDescription() + " decaf";
  
}
}

class Dry extends Decorator {
  
public Dry(DrinkComponent component) {
     
super(component);
  
}
  
  
public String getDescription() {
     
return component.getDescription() + " extra foamed milk";
  
}
}

class Wet extends Decorator {
  
public Wet(DrinkComponent component) {
     
super(component);
  
}
  
  
public String getDescription() {
     
return component.getDescription() + " extra steamed milk";
  
}
}

public class CoffeeShop3 extends TestCase {
  
public void testCappuccino() {
     
// Здесь просто убеждаемся, что
      // не было выброшено исключений.
      // Создаем обычный капучино.
     
DrinkComponent cappuccino = new Cappuccino();
      System.out.println
(cappuccino.getDescription() + ": $"
           
+ cappuccino.getTotalCost());
  
}
  
  
public void testCafeMocha() {
     
// Здесь просто убеждаемся, что
      // не было выброшено исключений.
      // Создаем cafe mocha без кофеина
      // со взбитым кремом
     
DrinkComponent cafeMocha = new Whipped(new Decaf(new CafeMocha()));
      System.out.println
(cafeMocha.getDescription() + ": $"
           
+ cafeMocha.getTotalCost());
  
}
  
  
public static void main(String[] args) {
     
junit.textui.TestRunner.run(CoffeeShop3.class);
  
}
}
// /:~

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

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

Другие соображения

Что случится, если мы решим изменить меню предыдущего этапа путем добавления новых типов напитков? Если мы используем подход один класс на каждую комбинацию, то в результате введения таких добавок, как сироп, получим экспоненциальное увеличение количество классов. Однако привлечение декораторов или компромиссного подхода даст одинаковый результат - создание одного дополнительного класса.

А как насчет эффекта изменения стоимости сливок, когда изменится стоимость молока? Существование класса для каждой комбинации означает, что вам необходимо изменить метод в каждом классе, а это затронет многие классы. При использовании декораторов изменения локализуются в одном месте.

Упражнения

  1. Добавьте класс сиропа в подход с использованием декоратора, описанный выше. Затем создайте Café Latte (вам нужно использовать парные сливки с эспрессо) с сиропом.
  2. Повторите Упражнение 1 для комбинированного подхода.
  3. Создайте простую систему декоратора, которая моделирует тот факт, что некоторые птицы умеют летать, а некоторые нет, некоторые плавают, а некоторые нет, а некоторые и летаю и плавают.
  4. Реализуйте шаблон декоратора, чтобы создать пиццерию, в которой есть меню выбора, точно так же, как и дизайн вашей собственной пиццы. Применяя компромиссный подход для создания меню, состоящего из Маргариты, Гаваев, Регины и Вегетарианской пиццы с начинкой (декораторы) из чеснока, Оливок, Шпината, Авокадо, Feta и Свежего перца. Создайте пиццу Гаваи так же как и пиццу Маргарита, декорировав ее Шпинатом, Feta, Свежим перцем и Оливками.
  5. Относительно Декоратора книга Design Patterns заключает: "С помощью декораторов возможность отклика системы может быть добавлена и удалена во время выполнения простым присоединением и отсоединением к декоратору". Реализуйте кофейный пример, чтобы позволить такое "простое" отсоединение отклика из середины списка декораторов сложного кофейного напитка.