太多(Too many)
享元(Flyweight):太多对象(too many objects)
Flyweidht模式可能让人感到奇怪的一点是, 它(在其它模式的辅助下)是改善性能(performance hack)的一种方法。 通常来说比较好的做法就是简单的把系统里所有东西都弄成对象,但是有时候这样做会使得产生的对象数量非常巨大,这可能会导致系统过于缓慢或者内存耗尽。
Flyweight模式通过减少对象数量来解决这个问题。为了达到这个目的,需要外部化(externalize)原本属于对象一些数据,这样使得看起来对象的数量要比实际的多。但是,这同时也增加了应用这些对象时接口的复杂度,因为你必须传入额外的信息用以告诉调用方法如何找到那些外部化的信息。
作为一个非常简单的例子,考虑一个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数据成员的方法,使对象个数从一百万个减少到一个。
//: 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的索引。出于一致性的考虑,而且也是为了提醒读者注意(FlyPoint)各个方法的隐含(implicit)this指针的相似性,“this 索引”是作为第一个参数传入的。
自然,这里又要对过早优化(premature optimization)提出警告。“先让程序能够运行起来,如果确有这个必要的话,再想办法提高速度。”再说一次,用profiler工具来检测性能瓶颈,而不要靠猜测。
装饰模式(Decorator):太多类(too many classes)
Decorator模式是指:用分层的对象(layered objects)动态地和透明地给单个对象添加功能(responsibilities)。
当使用派生类方法产生过多(不灵活的)类的时候考虑应用Decorator模式。
所有外覆在原始对象上的装饰类都必须有同样的基本接口。
动态proxy/surrogate?
Decorator模式对于非常规的继承结构特别管用。
权衡:使用decoator模式通常会使代码变得比较复杂。
基本decorator结构
一个关于咖啡的例子A coffee example
考虑去当地的一家咖啡馆BeanMeUp喝杯咖啡。那里通常会提供许多种不同的饮料-蒸馏咖啡, lattes, 茶, 冰咖啡,热巧克力饮品,而且还有许多额外的添加物,比如生奶油或者额外的蒸馏咖啡粉末(当然这些是要另外收钱的)。你也可以不支付额外的费用而改变你的饮料,比如你可以要求把普通咖啡换成脱咖啡因咖啡。
有一点是非常清楚的,如果我们想给上面所有这些饮料和它们的组合建模,那将会产生一张非常大的类图。为了能够清楚的说明问题,我们只考虑所有咖啡品种的一个子集:Espresso, Espresso Con Panna, Café Late, Cappuccino and Café Mocha. 我们提供2种额外的添加物-生奶油(“whipped”)和蒸馏咖啡粉,和三种可选择的饮品-脱咖啡因咖啡,steamed milk ("wet") 和 foamed milk ("dry").
每种组合对应一个类Class for each combination
一个解决办法是给每种组合都创建一个类。用它来描述每种饮料(的组成)和价格等等。最终产生的饮料单是巨大的,整个类图的一部分可能看起来是这样的:
下面是Cappuccino这种组合的一种简单实现:
class Cappuccino {
private float cost = 1;
private String description = "Cappucino";
public float getCost() {
return cost;
}
public String getDescription() {
return description;
}
}
使用这种方法最关键的一点是找到你需要的那种特定组合。一旦找到你想要的饮品,下面CoffeeShop类的代码演示了如何使用你想要的饮品类。
//: decorator:nodecorators:CoffeeShop.java
// Coffee example with no decorators
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() {
// This just makes sure it will complete
// without throwing an exception.
// Create a plain cappuccino
Cappuccino cappuccino = new Cappuccino();
System.out.println(cappuccino.getDescription()
+ ": $" + cappuccino.getCost());
}
public void testCafeMocha() {
// This just makes sure it will complete
// without throwing an exception.
// 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
你可以看到,创建你想要的组合是非常简单的,因为你只要创建某个类的一个实例就可以了。但是,这种方法存在许多问题。首先,这些组合都是静态固定死的,客户想要的每个组合都必须预先创建好。其次,整个组合生成的菜单实在是太大了,要找到某个特定的组合非常困难而且耗时。
Decorator方法
另外可以采用把这些饮料拆成类似于esspresso和foamedmilk的组件(components),然后让顾客通过组合这些组件来描述某种特定的咖啡饮料。
我们使用Decorator模式,通过编程来实现上述想法。Decorator通过包裹(wrapping)一个组件来给它增加功能,但是Decorator兼容它所包裹的那个类的接口,这样,对component的包裹就是透明的。Decorator自身又可以被(别的Decorator)包裹而不丧失其透明性。
调用Decorator的某个方法,会依次调用到Decorator所包裹的component的方法,而component的方法可以在Decorator方法被调用之前或者之后被调用。
如果为DrinkComponent接口添加getTotalCost()和getDescription()方法,Esspresso看起来就会像下面的样子:
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
// Coffee example using decorators
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() {
// This just makes sure it will complete
// without throwing an exception.
// Create a plain cappucino
DrinkComponent cappuccino = new Espresso(
new FoamedMilk(new Mug()));
System.out.println(cappuccino.
getDescription().trim() + ": $" +
cappuccino.getTotalCost());
}
public void testCafeMocha() {
// This just makes sure it will complete
// without throwing an exception.
// Create a decaf cafe mocha with whipped
// cream
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);
}
} ///:~
这种方法当然提供了最灵活和体积最小的菜单。你只需从很少几个组件(components)里挑出你需要的,但是拼接这些“关于咖啡的描述”就变的十分费劲。
如果你想描述纯的cappussino咖啡,你可以这么写:
new Espresso(new FoamedMilk(new Mug()));
但是创建“a decaf Café Mocha with whipped cream”就需要相当冗长的描述。
折中Compromise
用上面这种方法来描述某种咖啡饮品实在是太冗长了。一些组合是会经常被用到的,如果用一种更快的方法来描述它们会方便很多。
这里的第三种方法就是前面两种方法的组合,也就是灵活性和易用性相结合。
这种折中是通过创建合适大小的供常规选择的菜单来实现的,这个菜单基本上是不怎么变化的,但是如果你想给这些基本饮品加点伴侣(比如whipped cream, decaf等),那你可以用decorators
来改变基本饮品。这其实也是大多数咖啡馆提供的那种菜单。
下面就是常规饮品(basic selection)和加了伴侣之后的饮品 (decorated selection) 的一个例子:
//: decorator:compromise:CoffeeShop3.java
// Coffee example with a compromise of basic
// combinations and decorators
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() {
// This just makes sure it will complete
// without throwing an exception.
// Create a plain cappucino
DrinkComponent cappuccino = new Cappuccino();
System.out.println(cappuccino.getDescription()
+ ": $" + cappuccino.getTotalCost());
}
public void testCafeMocha() {
// This just makes sure it will complete
// without throwing an exception.
// Create a decaf cafe mocha with whipped
// cream
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);
}
} ///:~
你可以看到,创建一个基本饮品(basic selection)是非常快且简单的,因为描述基本饮品成为一项常规工作。描述添加伴侣的饮品(decorated drink)比每种组合对应一个类那种方法要麻烦一些,但是,很明显比只使用decorator模式要简单很多。最后的结果是产生不算太多的类,同样,也不算太多的咖啡伴侣(decorators)。大多数情况下,我们碰到的都是不加伴侣的情况,这样我们就可以得益于上述两种方法带来的好处。
其它考虑Other considerations
假如说我们打算后来改变菜单,比如添加一种新类型的饮品,那会发生什么情况呢?如果我们使用的是每种组合对应一个类的那种方法,添加一种额外饮品(比如糖浆syrup)将会导致类的个数呈指数增长。但是,对于只使用decorator的方法和上述那种折中的方法,都只需要创建一个额外的类。
当牛奶涨价的时候,如果我们改变steamed milk和foamed milk的价格,会带来什么影响呢?如果是每种组合对应一个类,你必须得更改所有类的(getCost)方法,这样就得维护(maintain)非常多的类。通过使用Decorators,只要在一个地方维护(价格改变相关)的逻辑代码就可以了。
练习
1. 使用上面的decorator方法,添加一个Syrup类。然后创建一个添加了Café Latte的Syrup(你得用到steamed milk和espresso)。
2. 用“折中”的那种方法重做练习1。
3. 写一个简单的系统,用decorator模式模拟以下情况:有些鸟会飞而有一些不会,有一些鸟会有用而有一些不会,还有一些既会飞又会游泳。
4. 用decorator模式写一个Pizza餐馆,它提供可供选择的基本菜单,也允许你自己定制pizza。用“折中”的方法写一个菜单包括Margherita, Havaiian, Regina和Vegetarian pizza(反正都是pizza的品种), 和浇头(toppings,即decorator)大蒜Garlic, 橄榄Olives, 菠菜Spinach, 鳄梨?Avocade. 奶酪Feta,胡椒?Pepperdews。 写一个Hawaiian pizza, 再写一个添加了菠菜,奶酪,和胡椒的Margherita pizza.
5. 关于Decorator模式,《设计模式》一书是这么说的:“通过附加(attching)或者分离(detaching)装饰者(decorators),可以在运行时刻(run-time)添加或者去处某项功能(responsibilities)”。写一个咖啡的Decoration系统,这个系统可以从某种复杂的咖啡饮料的一系列添加物(decorators)里“简单”的去除某项功能。