1. 需求
1.1 最开始的需求
假设现在有一个老板,开了一家面店,提供各种各样的面。那么编程如下。
- 超类
package decorator;
abstract class Noodles {
private String name;
public Nooddes(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return getName() + ": " + cost();
}
public abstract double cost();
}
- 继承类
package decorator;
//红烧牛肉面
public class BraisedBeefNoodles extends Noodles{
BraisedBeefNoodles(){
super("BraisedBeefNoodles");
}
@Override
public double cost() {
return 18;
}
}
package decorator;
//葱油面
public class ScallionNoodles extends Noodles{
ScallionNoodles(){
super("ScallionNoodles");
}
@Override
public double cost() {
return 10;
}
}
package decorator;
//西红柿鸡蛋面
public class TomatoEggNoodles extends Noodles{
TomatoEggNoodles(){
super("TomatoEggNoodles");
}
@Override
public double cost() {
return 12;
}
}
还有其它的就不一 一列举了。下面是测试类,也很简单,只是简易的打印一下,假设这个步骤就相当于顾客下单付钱店家煮面端上来的整个过程。
package decorator;
public class Test1 {
public static void main(String[] args) {
//假设顾客点了一碗红烧牛肉面
Noodles braisedBeefNoodles = new BraisedBeefNoodles();
System.out.println(braisedBeefNoodles);
}
}
输出如下,这样一笔生意就算完成了。
BraisedBeefNoodles: 18.0
1.2 变化
疫情期间,生意不怎么好。老板为了提高自己店面的营业额,新推出很多种配菜供消费者选择,例如卤鸡腿、煎蛋、卤鸭头、香干、火腿肠等等一系列小食。
这个时候,这个系统应该怎么去开发呢?
2. 思考
- 能不能根据每种面加上每种配菜的搭配额外开发出类,例如煎蛋+红烧牛肉面搭配开发一个
FriedEggBraisedBeefNoodles
?
答:这种做法本身是没错的,但细想一下,店里面有几十种面,也有几十种配菜,而且顾客可以一次性点多个配菜,这个组合很难预估,如果要把所有的组合都写全的话,这个系统会变得极其庞大且难以维护。
- 在基类基础上添加若干个标志位,每个标志位代表一种配菜,当标志位被设置为特殊值的时候,表示顾客点了这个配菜,这样行不行?演示如下面代码。
//即,将下面的代码加入或修改到Noodles类中。
boolean friedEgg, ....;// 一系列标志位
//getter and setter 略
//重写价钱方法,不再是abstract方法而是具体方法,配菜的花费从父类开始统筹,提高代码重用
double cost(){
double total = 0;
if(friedEgg){
total += 2;
} //其余情况略
return total;
}
//子类中的价钱方法也要重写,比如牛肉面里面的方法需要修改为
double cost(){
return 18 + super.cost();//需要考虑面类本来的钱和配菜的钱
}
答:理论上可行,但顾客有可能点了两份煎蛋,单纯用标志位无法满足,还需要用于计数的变量。如果这种方法可行的话,那么,把配菜的基类和子类都写出来,将配菜类数组或者不定长参数作为形参传给Noodles
类及其子类的构造函数应该也是可行而且相对较好的。如SideDishes
为配菜类基类,这个类其实长得跟Noodles
类差不多。A、B、C
等继承配菜类基类。具体演示如下面代码。
//配菜类基类
abstract class SideDishes {
private String name;
public SideDishes(String name) {this.name = name;}
public String getName() {return name;}
public abstract double cost();
}
//具体配菜类,此处只以A为例
class A extends SideDishes{
public A(){
super("SideDish A");
}
public double cost(){
return 2.0;
}
}
//重写Noodles类
abstract class Noodles {
private String name;
private double sideDishesTotal;
public Noodles(String name, SideDishes...dishes) {
this.name = name;
for (SideDishes sideDishes : dishes) {
sideDishesTotal += sideDishes.cost();
this.name += (" + " + sideDishes.getName());
}
}
public double getSideDishesTotal() {
return sideDishesTotal;
}
public String getName() {
return name;
}
@Override
public String toString() {
return getName() + ": " + cost();
}
public abstract double cost();
}
//重写具体面类
public class BraisedBeefNoodles extends Noodles{
BraisedBeefNoodles(SideDishes...dishes){
super("BraisedBeefNoodles", dishes);
}
@Override
public double cost() {
return 18 + super.getSideDishesTotal();
}
}
//测试
public class Test2 {
public static void main(String[] args) {
A a = new A();
A a1 = new A();//想加多少加多少,比加标志位的方法还好一些。
Noodles braisedBeefNoodles = new BraisedBeefNoodles(a,a1);
System.out.println(braisedBeefNoodles);
}
}
输出
BraisedBeefNoodles + SideDish A + SideDish A: 22.0
可以看到,我们加了两份SideDish A
,牛肉面的价格是18
元,整体价格是22
元,完整的菜单和价钱都打印出来了,没毛病。
但是,此处所做的两种假设虽然能解决问题,其最大的问题是,修改了原有代码,违反了“开闭原则”。假如前面的类是别人开发的,你压根没有办法获取这些类的源码,那么就没法按照本条所述的两种方案来做。即使你能获取这些类的源码,甚至你就是它们的开发者,从遵守设计模式七大原则的角度上说,也不能去修改既有的代码。就想java
源码似的,如果升一次级就要去改一下原有代码,那么世界上将会有很多应用遭殃的。
那有什么办法能够既不修改原有代码又能实现需求呢?这就要说到装饰器设计模式了。
3.装饰器设计模式(Decorator Pattern)
3.1 实现
通过前面的推论,很容易知道装饰器设计模式的作用,装饰器模式(Decorator Pattern)
允许向一个现有的对象添加新的功能,同时又不改变其结构。
用前面所说的面和配菜的例子来说,新的功能就是店家新出的配菜,原有结构就是原有的面。装饰器模式使得我们能够使用区别于前一章所说的做法的方式完成这个需求。
废话少说,我们先看看它是怎么完成的吧!
- 原有的
Noodles
及其子类保持不变,这点很重要。 - 配菜类基类
abstract class SideDishes extends Noodles {
private Noodles noodles;
public SideDishes(Noodles noodles) {
super(noodles.getName());
this.noodles = noodles;
}
public Noodles getNoodles() {
return noodles;
}
public abstract double cost();
}
- 配菜类子类,以
A
为例
class A extends SideDishes{
public A(Noodles noodles){
super(noodles);
}
public double cost(){
return getNoodles().cost() + 2.0;
}
@Override
public String getName() {
return super.getName() + " + SideDishes A";
}
}
- 测试类
public class Test3 {
public static void main(String[] args) {
Noodles braisedBeefNoodles = new BraisedBeefNoodles();
A a = new A(braisedBeefNoodles);
A a1 = new A(a);
System.out.println(a1);
}
}
输出
BraisedBeefNoodles + SideDishes A + SideDishes A: 22.0
3.2 说明
上面所演示的,就是装饰器设计模型的样子了。
在不改变原有结构的基础上,继承原有结构并且关联原有结构Noodles
类。虽然我们说继承用于“is a”关系,符合里氏替换原则(任何使用父类对象的地方都可以替换成子类对象),但是在装饰器设计模式中,“is a”
关系不是语法强制。
从上面的例子可以看的出来,配菜不是面,但是配菜继承了面,而且配菜关联了面,这就是装饰器设计模式的玩儿法。
用简单的话来说,就是俄罗斯套娃。我套我自己。配菜A现在算是一种面了,他需要一种面传给他的构造参数,那就传呗,于是我们传进去了红烧牛肉面;如果还需要再加配菜或者再加面,那就在原来基础上再套呗,你有钱你套好了,哈哈。
装饰器设计模式,用于类似本文案例中的情形,即需要再原有架构上添加新功能的情况,既保证新功能与原有架构之间相互独立且易于扩展,也避免了直接继承而造成类的数量过于庞大,大大提高了拓展性并降低了耦合度。
下面是本文例子的uml
图。
4. 总结
装饰器模型,结构性设计模式之一,用于想系统中增加新功能而不改变原有结构的情况。其基本思想是,创建装饰类(如本文的SideDishes
)用来包装原有类(Noodles
),所谓包装也就是继承并且组合该类,实现无限套娃。但是,需要注意,装饰器模式有时候会违反继承里面的“is a”
关系,本例中的配菜和面就是这样,配菜不是面,但是为了使用装饰器模式,我们只能让配菜继承面。
java
类库中 诸多的流里就有装饰器设计模式的影子,如下面这张图中我们看到,FilterInputStream
既继承了InputStream
又将其作为了自己的一个属性。但是FilterInputStream
也算是一种InputStream
,所以没有违反"is a"
关系。
好了,就到这里吧,有点啰嗦了,感觉文章上下怪怪的,词不达意啊
5. 参考文献
- java 类库源码
- 蜗牛学院 “Java 设计模式”