Skip to main content

Design Pattern: Decorator Pattern

Free2015-03-06#Design_Pattern#装饰者模式#Decorator Pattern

The Decorator Pattern is a design pattern widely used in frameworks. In the Java API, file stream operations apply this pattern, e.g., InputStream in = new BufferedInputStream(new FileInputStream(file)); Here, BufferedInputStream is used to decorate FileInputStream, thereby achieving functional extension. The Decorator Pattern satisfies the OO design principle of "Closed for modification, open for extension", making it an excellent design pattern.

no_mkd

I. What is the Decorator Pattern?

The Decorator Pattern perfectly achieves the principle of "Closed for modification, open for extension", meaning we can extend the functionality of the decoratee without modifying it. Let's look at our file operation code again:

package DecoratorPattern;

/**

  • @author ayqy
  • Define Beverage superclass, all concrete Beverages and Ingredients must extend from this class

*/ public abstract class Beverage { String desc = "Unknown Beverage";// Define beverage related description information float cost;// Define the price of the beverage

public abstract float getCost();// Define cost method to return the price of the beverage, subclasses must implement this method

public String getDesc(){
	return desc;
}

}

The InputStream object wrapped in the innermost layer is new FileInputStream(file), which is the basic file input stream. Use a BufferedInputStream object to extend its functionality, or we can even do this:

P.S. Notice the verb used above—"wrap", this is the core of the Decorator Pattern

InputStream in = new LineNumberInputStream(new BufferedInputStream(new FileInputStream(file)));

Continue adding a new decorator LineNumberInputStream to extend the line number retrieval functionality. Of course, we can extend more functionality, just keep adding new decorators. Looking back, we added layers of decoration to FileInputStream, gaining functions one by one. In this process, we achieved dynamic functional extension, but did not modify anything of the decoratee FileInputStream. This is the so-called "Closed for modification, open for extension" principle.

II. An Example

Suppose we want to open a shop to sell Milk. Optional ingredients include Mocha (chocolate flavor), Coffee, IceWater. Of course, if sales are good, we plan to introduce new drinks (Orange, Yoghurt, etc.) and new ingredients (Salt.. joke). Milk itself has a price and discounts on holidays. Different ingredients have different prices. Of course, I can also order a Milk with double IceWater.. The easiest solution to think of is:

Define a Milk class, containing many attributes, such as hasMocha, hasCoffee, hasIceWater (used to indicate added ingredients), also need discount attribute (used to indicate discount information), cost attribute (used to indicate price). Is that it? No, besides this we also need MochaNum, CoffeeNum, IceWaterNum (used to indicate the number of portions of ingredients, heavy-taste customers need double or more portions..)

The problem is solved, but is this really good?

Consider these situations: 1. Introduce a new drink Orange (we need to define an Orange, almost no reusable parts, start from zero.. Or, we can define a Beverage base class, put the common parts of drinks in there); 2. Introduce a new ingredient Salt (we must modify the Milk class, add hasSalt, SaltNum attributes to meet the Salt Milk requirement..)

Now it seems our solution is very poor, cannot adapt to any changes, to extend functionality may require modifying existing encapsulated code, and there is also a performance issue: the calculation of drink price part needs a large number of if...else... structures, making our code very bloated, and difficult to reuse (different drinks may have different ingredients, calculation methods also differ). So, it's time to try the Decorator Pattern

First, because the decoratee and the decorator must have the same superclass (won't explain why for now), so, we define the following Beverage base class:

package DecoratorPattern;

/**

  • @author ayqy
  • Define Beverage superclass, all concrete Beverages and Ingredients must extend from this class

*/ public abstract class Beverage { String desc = "Unknown Beverage";// Define beverage related description information float cost;// Define the price of the beverage

public abstract float getCost();// Define cost method to return the price of the beverage, subclasses must implement this method

public String getDesc(){
	return desc;
}

}

With Beverage we can start defining the decoratee—Milk:

package DecoratorPattern;

/**

  • @author ayqy
  • Define concrete Beverage: Milk class

*/ public class Milk extends Beverage{

float discount = 1;// Define discount, holidays Milk may have discounts (default no discount)

public float getDiscount() {
	return discount;
}

public void setDiscount(float discount) {
	this.discount = discount;
}

public Milk(){
	cost = 4.5f;// Initialize Milk price
	desc = "Milk";// Initialize description information
}

 @Override
public float getCost(){
	return discount * cost;// Return discounted price
}

}

Next is the decorator, because the decorator has some characteristics different from Beverage, so we abstract it:

package DecoratorPattern;

/**

  • @author ayqy
  • Define Ingredient class, inherits from Beverage (in Decorator Pattern, decorator and decoratee must have the same superclass)

*/ public abstract class Ingredient extends Beverage{

Beverage beverage;// The beverage to add this ingredient to

 @Override
public String getDesc() {
	return "(" + desc + ")" + beverage.getDesc();// Ingredient description should include parentheses to distinguish from beverage
}

 @Override
public float getCost() {
	return cost + beverage.getCost();// Ingredients have no discount, directly return its price + beverage price
}

// Add other attributes and behaviors here where Ingredient differs from Beverage

}

Notice the getDesc and getCost methods above, we completely delegated the responsibility of calculating price and generating description information to the method call mechanism, making the code so concise..

Below define specific ingredients—IceWater, Coffee, Mocha:

package DecoratorPattern;

/**

  • @author ayqy
  • Define ingredient IceWater

*/ public class IceWater extends Ingredient{

public IceWater(Beverage bev)
{
	cost = 0.5f;
	desc = "IceWater";
	beverage = bev;
}

}

package DecoratorPattern;

/**
 * @author ayqy
 * Define ingredient Coffee
 *
 */
public class Coffee extends Ingredient{
	
	public Coffee(Beverage bev)
	{
		cost = 3;
		desc = "Coffee";
		beverage = bev;
	}
}
package DecoratorPattern;

/**
 * @author ayqy
 * Define ingredient Mocha
 *
 */
public class Mocha extends Ingredient{
	
	public Mocha(Beverage bev)
	{
		cost = 2;
		desc = "Mocha";
		beverage = bev;
	}
}

Everything is ready, our Milk shop can open..

III. Effect Example

First define a test class:

package DecoratorPattern;

public class Test { public static void main(String[] args){ Beverage bev;

	System.out.println("Make a Milk with Mocha and Coffee..");
	bev = new Milk();// First make a Milk
	bev = new Mocha(bev);// Add Mocha
	bev = new Coffee(bev);// Add Coffee
	System.out.println(bev.getDesc() + " " + bev.getCost() + "¥");
	
	System.out.println("Make a Coffee Milk with double IceWater and double Mocha..");
	bev = new Milk();// Remake a Milk
	bev = new IceWater(bev);// Add IceWater
	bev = new IceWater(bev);// Add IceWater
	bev = new Mocha(bev);// Add Mocha
	bev = new Mocha(bev);// Add Mocha
	bev = new Coffee(bev);// Add Coffee
	System.out.println(bev.getDesc() + " " + bev.getCost() + "¥");
	// Of course can also write like this:  Beverage bev = new Coffee(new Mocha(new Milk()));
	// Compare our familiar method chain: InputStream in = new BufferedInputStream(new FileInputStream(file));
}

}

Result example:

Effect is not bad, then consider the previous extension problem:

1. Introduce a new drink Orange (we need to define an Orange class inheriting from Beverage base class, can reuse existing parts in Beverage base class, if still not satisfied, of course can also abstract a "ConcreteBeverage class", let Milk and other drinks extend on this basis); 2. Introduce a new ingredient Salt (we don't need to make any modifications to the Milk class, just implement a Salt ingredient, inherit from Ingredient class is fine)

Found the advantages of the Decorator Pattern? Then it's time to pour a basin of cold water..

IV. Pros and Cons of the Decorator Pattern

The disadvantage is obvious—have you seen such long code?

XObject o = new XDecorator(new XXDecorator(new XXXDecorator(new XXXXDecorator())));

En, it just did three functional extensions to the decorated object, of course, can be more.. Also means can be longer. Moreover, we created many small objects when using, like this:

bev = new Milk();// Remake a Milk
bev = new IceWater(bev);// Add IceWater
bev = new IceWater(bev);// Add IceWater
bev = new Mocha(bev);// Add Mocha
bev = new Mocha(bev);// Add Mocha
bev = new Coffee(bev);// Add Coffee

Let someone unfamiliar with the Decorator Pattern read the code above, can they understand quickly?

Note, the code above explains the verb mentioned at the beginning—"wrap", right?

Advantages:

Besides the dynamic extension advantage mentioned above, there is another more important advantage which is the getDesc and getCost methods mentioned earlier

Correct, we can use this call mechanism to complete our operations (just add our custom operations before or after the decoration action, the example actually belongs to adding operations after the decoration action), we easily achieved effects similar to recursion

This also explains "why must the decorator and the decoratee have the same superclass?", need a bit more explanation:

There is a design principle is "Favor composition over inheritance", here we seem to violate this principle right

Actually did not violate the principle, the inheritance in the Decorator Pattern is to obtain type matching, not to use inheritance to extend class behavior, and the "Favor composition over inheritance" principle omits the premise condition "(When we need to extend class behavior) Favor composition over inheritance"

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment