序
好久没写设计模式了,自从写了两篇之后,就放弃治疗了,主要还是工作太忙了啊(借口,都是借口),过完年以后一直填坑,填了好几个月,总算是稳定下来了,可以打打酱油了。
为什么又重新开始写设计模式呢?学习使我快乐啊(我装逼起来我自己都害怕),其实主要是最近填坑的时候看源代码有点晕,很多代码不知道他们为什么要那么写啊,好气啊
当时第二篇写完,其实就在准备第三篇了,但是,一直也没有写,看了好几遍,但是一直掌握不到精髓(其实现在也掌握不到),感觉挺模糊的,也就一直拖啊拖,拖延症晚期患者已经不用抢救了。。。
先来举个栗子
故事背景:星巴兹咖啡,由于快速扩展,他们现在的订单系统已经跟不上他们的饮料供应需求了,先看一下当前的设计
/** * 饮料超类 * @author Skysea * */ public abstract class Beverage { protected String description;//描述 public abstract double cost();//消费金额 public Beverage(String description){ this.description = description; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
下面是各种咖啡,这里就只展示一种:
/** * 首选咖啡 * @author Skysea * */ public class HouseBlend extends Beverage{ public HouseBlend(String description) { super(description); } @Override public double cost() { return 3.5; } }
很简单的代码,咖啡继承饮料超类。
啥都不说了,先提需求:
1,购买咖啡时,要求可以在其中加入各种调料,例如:蒸奶,豆浆,摩卡...等等(产品每次给我们说的就是,具体的他们还没定好,能不能做成活的?就是随时可以扩展的那种。知道我为啥要来学设计模式了吗?会被玩死的,真的)
2,各种调料也会参与计价,而且每种调料的价格不一定相同
需求清楚了吧?不清楚?没关系,反正下周上线,不存在的,嘿嘿嘿
先来给出第一版实现(直接扩展超类):
/** * 饮料超类 * @author Skysea * */ public abstract class Beverage { protected String description;//描述 public abstract double cost();//消费金额 private boolean milk;//牛奶 private boolean soy;//豆浆 private boolean mocha;//摩卡 private boolean whip;//奶泡 //省略get set方法... }
不就是下周上线吗?不就是要扩展吗?要多少,就在超类里面加就好了啊,然后再对每一种饮料进行处理,像这样:
/** * 首选咖啡 * @author Skysea * */ public class HouseBlend extends Beverage{ public HouseBlend(String description) { super(description); } @Override public double cost() { double cost = 3.5; if(hasMilk()){ cost += 0.5; } if(hasMocha()){ cost += 0.6; } if(hasSoy()){ cost += 0.3; } if(hasWhip()){ cost += 0.4; } return cost; } }
感觉每一种都要写这么多if好麻烦啊,不存在的,这种东西,抽出来嘛:
/** * 饮料超类 * @author Skysea * */ public abstract class Beverage { /** * 调料消费 * @return */ protected double flavourCost(){ double cost = 0.0; if(hasMilk()){ cost += 0.5; } if(hasMocha()){ cost += 0.6; } if(hasSoy()){ cost += 0.3; } if(hasWhip()){ cost += 0.4; } return cost; } //... }
子类:
/** * 首选咖啡 * @author Skysea * */ public class HouseBlend extends Beverage{ public HouseBlend(String description) { super(description); } @Override public double cost() { return 3.5 + flavourCost(); } }
感觉也挺好的啊,每次要添加的时候,先去超类添加一个属性,然后在超类的 flavourCost()方法中,添加一段代码,以前的子类完全不用动,逻辑也是妥妥的,一切都是很OK的
但是,这样写至少有三处是不符合逻辑的:
1,所有的子类的cost方法都必须要加上一句 xxx + flavourCost(),不觉得写的次数太多了吗?一般一个方法写N次,那大部分最后会变成坑
2,超类的所有属性并不是每一个子类都能用上的
3,违背了开闭原则,每一次扩展虽然不用修改子类,但是却会去修改父类的属性,以及父类的flavourCost()方法
在现在的这个需求下,这些不合理都是体现不出来有什么问题的,但是,代码中不符合逻辑的东西早晚有一天会对整个模块的设计造成巨大的影响,两种情况除外:
1,你不干了(钱没给够或者我不开心毕竟程序员都是非常任性的),对模块的影响跟你没有半毛钱的关系
2,这个模块不扩展,不维护,或者扩展、维护还没有达到临界点(时间根据前期逻辑混乱程度成反比)
别问我怎么知道的,因为我TM还没走,所以就来学习来了,我擦,跑题了。。。
下面来聊聊刚学的正确姿势:
装饰者模式
先说超类 Beverage(不变,保持最最原始的样子):
/** * 饮料超类 * @author Skysea * */ public abstract class Beverage { protected String description;//描述 public abstract double cost();//消费金额 public Beverage(String description){ this.description = description; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
为什么不变?
要回答这个问题,首先要考虑,整个模块中,到底是什么东西一直变来变去?没错,就是调料啊。
一般变化的怎么处理?
抽离出来,把它和稳定的东西抽离出来,即保证了它的扩展性,又提高了代码的稳定性,遵循了开闭原则
所以为什么不变?1,它很稳定。2,它很稳定。没有3,我要逼死强迫症
接下来,贴一下它的子类代码,调整的地方:
public HouseBlend(String description) { super(description); }
把 description放在构造函数内写死,为啥?你创建一个咖啡叫啥名字,难道还要客户端帮你想好?
让他去做也是可以的,但是有两个问题你要想清楚:
1,他知道你住哪里吗?
2,你走夜路吗?
卧槽,走远了走远了...
继续:
/** * 首选咖啡 * @author Skysea * */ public class HouseBlend extends Beverage{ public HouseBlend() { super("首选咖啡"); } @Override public double cost() { return 3.5; } }
为了保证最后的测试类可以正常的跑,我把其他的也写一些出来...
/** * 浓咖啡 * @author Skysea * */ public class Espresso extends Beverage { public Espresso() { super("浓咖啡"); } @Override public double cost() { return 2.5; } }
/** * 焦炒咖啡 * @author Skysea * */ public class DarkRoast extends Beverage{ public DarkRoast() { super("焦炒咖啡"); } @Override public double cost() { return 3.2; } }
然后来到最重要的装饰超类CondimentDecorator (第一版):
/** * 调料装饰类 * @author Skysea * */ public abstract class CondimentDecorator extends Beverage { protected Beverage beverage; public CondimentDecorator(String description) { super(description); } //重写cost方法 @Override public String getDescription(){ return this.beverage.getDescription() + ", " +this.description; } //直接实现cost方法 public double cost(){ return this.beverage.cost() + condimentCost(); } protected abstract double condimentCost();//重新抽象一个调料的价格方法 }
装饰子类(Mocha ):
/** * 调料:摩卡 * @author Skysea * */ public class Mocha extends CondimentDecorator { public Mocha() { super("摩卡"); } @Override protected double condimentCost() { return 0.6; } }
是不是感觉很简单?所有的逻辑都在装饰超类里面处理好了,感觉妥妥的,没什么不对,而且其他的子类实现起来也是非常的轻松
上面的装饰类是我自己的第一版实现,写完之后,我发现,这个东西,和我们最初想要的装饰类有很大的区别:
不灵活
现在我们来看看装饰者模式的定义:
装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。(百度百科)
想要应对越复杂的变化,那么就要给子类赋予越大的权限,让它接触越多的东西,只有这样才能做灵活,当然,灵活也是相对的,这个也是必须在实际项目中取舍
所以,上面的装饰类实现,根本无法满足当前需求所需要的灵活性,比如:使用摩卡的时候,所有的饮料满20减5块怎么玩?是不是感觉玩不动了?
问题在哪里呢?
//直接实现cost方法 public double cost(){ return this.beverage.cost() + condimentCost(); }
就是在这里,这个地方把cost方法定的太死板了,也不能说这样不对,只是,这样所有继承CondimentDecorator的其实只能算装饰者模式的特例,要实现这样的东西,就必须在CondimentDecorator与Beverage中间再抽象一层。
就像这样CondimentDecorator extends Decorator,Decorator extends Beverage,其他的调料继承 Decorator (咳,别听我乱BB)
讲讲正确的装饰类及实现吧:
/** * 调料装饰类 * @author Skysea * */ public abstract class CondimentDecorator extends Beverage { protected Beverage beverage; public CondimentDecorator(Beverage beverage, String description){ super(description); this.beverage = beverage; } //根据情况来觉得是不是要抽象出来让子类实现,原因和刚才抽象condimentCost()一样,分情况,没有什么是对不对的 @Override public String getDescription(){ return this.beverage.getDescription() + ", " +this.description; } }
调料实现类:
/** * 调料:牛奶 * @author Skysea * */ public class Milk extends CondimentDecorator{ public Milk(Beverage beverage) { super(beverage, "牛奶"); } @Override public double cost() { return 0.5 + beverage.cost(); } }
/** * 调料:摩卡 * @author Skysea * */ public class Mocha extends CondimentDecorator { public Mocha(Beverage beverage) { super(beverage, "摩卡"); } @Override public double cost() { return 0.6 + beverage.cost(); } }
/** * 调料:豆浆 * @author Skysea * */ public class Soy extends CondimentDecorator{ public Soy(Beverage beverage) { super(beverage, "豆浆"); } @Override public double cost() { return 0.3 + beverage.cost(); } }
....
测试类:
/** * 装饰模式测试类 * @author Skysea * */ public class Test { public static void main(String[] args) { //初始化浓咖啡 Beverage beverage = new Espresso();//$2.5 System.out.println(beverage.getDescription() + " $"+ beverage.cost()); //初始化焦炒咖啡 Beverage beverage2 = new DarkRoast();//$3.2 //用调料来装饰它 beverage2 = new Mocha(beverage2);//$0.6 beverage2 = new Mocha(beverage2);//$0.6 beverage2 = new Soy(beverage2);//$0.3 System.out.println(beverage2.getDescription() + " $"+ beverage2.cost()); //初始化焦炒咖啡 Beverage beverage3 = new HouseBlend();//3.5 beverage3 = new Mocha(beverage3);//$0.6 beverage3 = new Milk(beverage3);//$0.5 beverage3 = new Soy(beverage3);//$0.3 System.out.println(beverage3.getDescription() + " $"+ beverage3.cost()); } }
运行结果:
为了证明我没有随便拿个假截图来骗你们,我特意把价格都标到测试类后面的
为啥后面那么长一串?这肯定不是我的锅啊,double的锅,推荐用BigDecimal来玩这个,当然也可以用long型,或者int都是可以的,实际项目也是这样
用什么主要看:
1,精度要求高不高
2,速度要求快不快
3,看产品会不会让你保留小数点2位和保留小数点3位换着来玩的情况(都是泪,不说了..)