师父让我来炼丹
话说,很久以前,武林中各大门派林立,百家争鸣,忽然《九阴真经》再现江湖,各大门派各自暗中派出高手争抢,都想独吞《九阴真经》,习得神功,一统江湖。
闯荡江湖,当然必备各种良药防身,小帅师父在华山脚下的仙草药房,生意忽然好了很多,各种丹药销量大增。
师父苦心研制的丹药受到的武林人士的肯定,很是开心,其中的行军丹,金创药,黑玉断续膏,号称镇店三宝。
订单如雪花般飞来,师父命小帅在后院加班加点生产,但是由于各种特效药制作工艺复杂,小帅和几个伙计忙的团团转。
客官要问炼丹的场所在何处?
说出来有点不好意思,就在药房的后院。
药品的生产代码如下:
药品抽象类
abstract public class Drug {
/**
* 药品名称
*/
String name;
/**
* 功效
*/
String efficacy;
public void packing() {
System.out.println("打包药品:" + name);
}
@Override
public String toString() { // 药丸的功效
StringBuffer display = new StringBuffer();
display.append("---- " + name + " 使用说明 ----\n");
display.append("具有功效:" + efficacy + "\n");
return display.toString();
}
}
具体药品:行军丹
public class XingJunDanDrug extends Drug{
public XingJunDanDrug() {
name = "行军丹";
System.out.println("开始制作:祖传秘法秘制七天");
System.out.println("制作完成-->行军丹");
efficacy = "江湖中常见的疗伤药丸,尺寸小,便于携带,服用后见效快,恢复生命100点。";
}
}
具体药品:金疮药
public class JinChuangYaoDrug extends Drug{
public JinChuangYaoDrug() {
name = "金疮药";
System.out.println("开始制作:祖传秘法秘制七七四十九天");
System.out.println("制作完成-->金疮药");
efficacy = "闻名天下的佛门疗伤奇药,闯荡江湖必备,加速愈合,服用后立即恢复生命500点。";
}
}
具体药品:黑玉断续膏
public class HeiYuDuanXuGaoDrug extends Drug{
public HeiYuDuanXuGaoDrug() {
name = "黑玉断续膏";
System.out.println("开始制作:祖传秘法秘制九九八十一天");
System.out.println("制作完成-->黑玉断续膏");
efficacy = "传说中的稀世秘药,有再造之力,神奇无比,其价堪比黄金,服用后立即恢复生命2500点。";
}
}
仙草药房
public class Drugstore {
/**
* 药房接到订单生产药品
* @param type
* @return
*/
public Drug orderDrug(String type) {
Drug drug = null;
System.out.println("接到订单:" + type);
// 生产药品
if("行军丹".equals(type)) {
drug = new XingJunDanDrug();
} else if("金疮药".equals(type)) {
drug = new JinChuangYaoDrug();
} else if("黑玉断续膏".equals(type)) {
drug = new HeiYuDuanXuGaoDrug();
}
// 打包药品
drug.packing();
return drug;
}
}
测试类
public class DrugstoreTest {
public static void main(String[] args) {
Drugstore drugstore = new Drugstore();
System.out.println("为保证品质,本店所有药品只接受预定:");
System.out.println();
// 药房接到行军丹订单
Drug drug = drugstore.orderDrug("行军丹");
System.out.println(drug);
// 药房接到金疮药订单
drug = drugstore.orderDrug("金疮药");
System.out.println(drug);
// 药房接到黑玉断续膏订单
drug = drugstore.orderDrug("黑玉断续膏");
System.out.println(drug);
}
}
测试结果
为保证品质,本店所有药品只接受预定:
接到订单:行军丹
开始制作:祖传秘法秘制七天
制作完成-->行军丹
打包药品:行军丹
---- 行军丹 使用说明 ----
具有功效:江湖中常见的疗伤药丸,尺寸小,便于携带,服用后见效快,恢复生命100点。
接到订单:金疮药
开始制作:祖传秘法秘制七七四十九天
制作完成-->金疮药
打包药品:金疮药
---- 金疮药 使用说明 ----
具有功效:闻名天下的佛门疗伤奇药,闯荡江湖必备,加速愈合,服用后立即恢复生命500点。
接到订单:黑玉断续膏
开始制作:祖传秘法秘制九九八十一天
制作完成-->黑玉断续膏
打包药品:黑玉断续膏
---- 黑玉断续膏 使用说明 ----
具有功效:传说中的稀世秘药,有再造之力,神奇无比,其价堪比黄金,服用后立即恢复生命2500点。
所有的原材料和设备都堆在后院,杂乱不堪,而且每种药的生产工序和流程都不一样,每一种药品的生产设备都不一样。
小帅和几个伙计,终日奔波在不同的药品生产设备之间,还要理清楚各种各样的草药,一不小心就弄乱了。
仙草店的生意越来越好,小帅和伙计很本忙不过来,药品的产量却越来越低了。
“有问题啊,药房类直接依赖具体的药品类,药品类直接在药房类中创建,药房类和具体的药品类高耦合,这违反了依赖倒置原则:要依赖抽象,不要依赖具体类。
还有,药房既负责销售又负责生产,增加了复杂性,功能不够单一啊,这违反了单一职责原则”,有一天,师父忽然有感而发。
小帅在旁边听得一脸懵逼,感觉和师父不是同一个时代的人。
过了几日,黑玉断续膏的原料断货,停止生产,师父又开发了一款新药,灵葫仙丹,恢复内力很有疗效,深受各路武林人士喜爱,马上加入生产。
“还是有问题啊,每停止生产或者新生产一款药品,都要修改药店的设备和原料,对店铺影响很大啊“,有一天师父在院子里打坐,若有所思的说,接着忽然冒出一句奇怪的话来:“这违反了开闭原则,应该对扩展开发,对修改封闭”。
听得小帅一头雾水。
这一不小心就违反了三大设计原则:依赖倒置原则,单一职责原则,开闭原则。。。
这该如何是好?
小帅每日苦苦思索,始终不得要领,脸上多了几分与年龄不相符的忧愁,再想下去,头发都要掉了,只好向师父请教。
师父微微一笑,心中早有对策。
没过几日,师父忽然决定在店铺附近的山脚下建一个工厂,把原材料和设备都搬过去,专门生产药品,店里只负责销售,让销售和生产分离开来。
“师父你要建一个工厂?”
“嗯,一个简单的工厂。”
师父专门拨了一大笔银两,找了村里最能干的几位壮士,几个月后一个崭新的工厂就建好了,小帅和伙计们都高高兴兴地搬了进去。
简单工厂的类图如下:
(图片来源:https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/simple_factory.html)
简单工厂类
public class SimpleDrugFactory {
/**
* 生产药品
* @param type
* @return
*/
public static Drug createDrug(String type) {
Drug drug = null;
// 生产药品
if ("行军丹".equals(type)) {
drug = new XingJunDanDrug();
} else if ("金疮药".equals(type)) {
drug = new JinChuangYaoDrug();
} else if ("黑玉断续膏".equals(type)) {
drug = new HeiYuDuanXuGaoDrug();
} else if ("灵葫仙丹".equals(type)) {
drug = new LinHuXianDanDrug();
}
return drug;
}
}
新的仙草药房
public class Drugstore {
/**
* 药房接到订单生产药品
* @param type
* @return
*/
public Drug orderDrug(String type) {
Drug drug = null;
System.out.println("接到订单:" + type);
// 通过工厂生产药品
drug = SimpleDrugFactory.createDrug(type);
// 打包药品
drug.packing();
return drug;
}
}
药品的生产从药房搬到了工厂里,新增了一个简单工厂类,修改了药店里生产药品的代码,除此之外,其他代码都没有变。
有了新的工厂,仙草药店现在只负责销售了,工厂专门负责生产药品,这样各自负责自己的职责,效率大大提高了。这就符合了单一职责原则。
简单工厂使药房类和具体的药品类解耦,依赖了药品抽象类,符合了依赖倒置原则。
依赖倒置原则:要依赖抽象,不要依赖具体类。
也就是说,不能让高层组件依赖低层组件,不管高层还是低层组件都应该依赖于抽象。
听不懂,太抽象对不对?
小帅也听不懂啊,师父耐心的给他讲解,还画了一幅图。。。
这里的药房就是高层组件,具体的丹药就是低层组件,刚开始在药房里直接生产药品,药房(高层组件)依赖具体丹药(低层组件)。
药房(高层组件)和具体丹药(低层组件)之间高耦合,这就是传统的依赖关系。
为什么叫传统的依赖关系呢?可能是因为人们天然的认为高层的组件都应该依赖底层的组件,这样更符合人们的常识。
依赖倒置?有点反常识啊,但是在软件设计中,这样更有好处哦。
后面我们采用了简单工厂模式,药房(高层组件)依赖于药品类(抽象),具体丹药(低层组件)也依赖于(实现)药品类(抽象),这就倒置了依赖。
药房(高层组件)和具体丹药(低层组件)之间实现了解耦,这样就符合依赖倒置原则了!
原来如此,小帅恍然大悟。。。
虽然把所有的生产设备都搬到简单工厂里了,但是随着师父开发的新品越来越多,工厂也变得越来越大,越来越复杂。
简单工厂模式有一组 if 分支判断逻辑,每上一种新产品都要修改工厂的生产线,需要修改工厂类的代码,还是违反了开闭原则,没有对扩展开发,对修改封闭啊。
是不是应该用多态或其他设计模式来替代呢?实际上,如果 if 分支并不是很多,代码中有 if 分支也是完全可以接受的。
不过,小帅看得更长远,他觉得以后生产的丹药会越来越多,一个工厂根本放不下那么多生产设备。
他向师父提议:”要不,我们把大工厂拆分成一个个小工厂,每个工厂只生产一种药品,这样管理起来就简单多了。“
师父年纪虽然大了,但是心态还是很年轻的,具有持续发展的战略眼光,马上采纳了小帅的建议,建了很多小工厂,每个工厂只负责生产一种丹药。
工厂方法模式(Factory Method Pattern)又称为工厂模式,定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
工厂模式类图如下:
(图片来源:https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/factory_method.html)
工厂接口
public interface IDrugFactory {
/**
* 生产药品
* @return
*/
Drug createDrug();
}
行军丹工厂
public class XingJunDanDrugFactory implements IDrugFactory{
public Drug createDrug() {
return new XingJunDanDrug();
}
}
金疮药工厂
public class JinChuangYaoDrugFactory implements IDrugFactory {
public Drug createDrug() {
return new JinChuangYaoDrug();
}
}
黑玉断续膏工厂
public class HeiYuDuanXuGaoDrugFactory implements IDrugFactory{
public Drug createDrug() {
return new HeiYuDuanXuGaoDrug();
}
}
新的仙草药房
public class Drugstore {
/**
* 药房接到订单生产药品
* @param type
* @return
*/
public Drug orderDrug(String type) {
IDrugFactory drugFactory = null;
Drug drug = null;
System.out.println("接到订单:" + type);
// 获取对应的工厂
if ("行军丹".equals(type)) {
drugFactory = new XingJunDanDrugFactory();
} else if ("金疮药".equals(type)) {
drugFactory = new JinChuangYaoDrugFactory();
} else if ("黑玉断续膏".equals(type)) {
drugFactory = new HeiYuDuanXuGaoDrugFactory();
} else if ("灵葫仙丹".equals(type)) {
drugFactory = new LinHuXianDanDrugFactory();
}
// 生产对应的药品
drug = drugFactory.createDrug();
// 打包药品
drug.packing();
return drug;
}
}
其他代码都没有变化,新的仙草药房只要根据不同的药品去不同的工厂拿货就行了。
这样以后新增一种丹药,只要新增一个实现IDrugFactory 的工厂类就行了,不用修改工厂代码,工厂方法模式比起简单工厂模式更加符合开闭原则。
不过,应用多态或设计模式来替代 if 分支判断逻辑,也并不是没有任何缺点的,它虽然提高了代码的扩展性,更加符合开闭原则,但也增加了类的个数,牺牲了代码的可读性。
现在工厂类里的if判断逻辑消除了,但是,if逻辑判断,从简单工厂类里又移回药房了。
if逻辑判断转了一圈,又回到了药房类中,新增一个新工厂需要改动药房类的代码,这样药房类就不符合开闭原则了啊。
真是忧伤呢,改如何解决呢?
“可以再建一个生产工厂的工厂,然后用Map消除if逻辑判断”,小帅灵光一现。
生产工厂的工厂
public class DrugFactoryMap {
private static final Map<String, IDrugFactory> cachedFactories = new HashMap<String, IDrugFactory>();
static {
cachedFactories.put("行军丹", new XingJunDanDrugFactory());
cachedFactories.put("金疮药", new JinChuangYaoDrugFactory());
cachedFactories.put("黑玉断续膏", new HeiYuDuanXuGaoDrugFactory());
cachedFactories.put("灵葫仙丹", new LinHuXianDanDrugFactory());
}
public static IDrugFactory getFactory(String type) {
if(type == null || type.isEmpty()) {
return null;
}
return cachedFactories.get(type);
}
}
药房类直接从DrugFactoryMap类中取对应的工厂就行了,成功的把if逻辑判断转移到了DrugFactoryMap类的Map中了。
DrugFactoryMap类其实就上面的简单工厂的另一种实现方式。
上面的方式就是简单工厂+工厂模式啊。
我去,要建好多工厂啊。。。
抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
抽象工厂类图如下:
(图片来源:https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/abstract_factory.html)
师父真是个很有上进心的人啊,平时这么忙了,还废寝忘食地开发新产品,你看,这几个月师父又发明了新配方,在原来的行军丹,金疮药,黑玉断续膏中加入了秘制草药和矿物质,使行军丹,金疮药,黑玉断续膏不仅能恢复生命值而且还能同时恢复内力!
这种神奇的超级药丸,极其珍贵,只限量供应vip客户。
仙草药房在武林中的名号越来越响了。
但是,新品研发出来了,有一个问题,就是每种新的丹药都要建一个对应的工厂啊,随着药品越来越多,工厂也越来越多,建工厂可是个烧钱的东西,慢慢得师父也觉得有点不妥。
小帅天天在各个工厂之间奔波,对工厂的运转非常了解,他发现有些药品的制造工艺有些相似之处。
例如:普通的行军丹,金疮药和黑玉断续膏,都用到了天山雪莲和忘忧水,需要在八卦炉中提炼。
超级的行军丹,金疮药和黑玉断续膏,都采用了华山脚下的神秘矿物质,需要吸收日月精华,加上秘制草药作为药引,才具有恢复内力的功效。
”不如,我们把普通的行军丹,金疮药和黑玉断续膏都集中在一个工厂里生产,称为H系列,把同时具有恢复内力功效的行军丹,金疮药和黑玉断续膏放到另一个工厂里生产,叫做S系列,如何?“
”如此甚好!“,师父大喜,立马下令改造组合工厂,一个生产H系列产品,另一个生产S系列产品。
完整的代码如下:
药品抽象类
abstract public class Drug {
/**
* 药品名称
*/
String name;
/**
* 功效
*/
String efficacy;
public void packing() {
System.out.println("打包药品:" + name);
}
@Override
public String toString() {
// 药丸的功效
StringBuffer display = new StringBuffer();
display.append("---- " + name + " 使用说明 ----\n");
display.append("具有功效:" + efficacy + "\n");
return display.toString();
}
}
普通行军丹
public class XingJunDanDrug extends Drug {
public XingJunDanDrug() {
name = "行军丹";
System.out.println("开始制作:祖传秘法秘制七天");
System.out.println("制作完成-->行军丹");
efficacy = "江湖中常见的疗伤药丸,尺寸小,便于携带,服用后见效快,恢复生命100点。";
}
}
普通金疮药
public class JinChuangYaoDrug extends Drug {
public JinChuangYaoDrug() {
name = "金疮药";
System.out.println("开始制作:祖传秘法秘制七七四十九天");
System.out.println("制作完成-->金疮药");
efficacy = "闻名天下的佛门疗伤奇药,闯荡江湖必备,加速愈合,服用后立即恢复生命500点。";
}
}
普通黑玉断续膏
public class HeiYuDuanXuGaoDrug extends Drug {
public HeiYuDuanXuGaoDrug() {
name = "黑玉断续膏";
System.out.println("开始制作:祖传秘法秘制九九八十一天");
System.out.println("制作完成-->黑玉断续膏");
efficacy = "传说中的稀世秘药,有再造之力,神奇无比,其价堪比黄金,服用后立即恢复生命2500点。";
}
}
超级行军丹
public class SuperXingJunDanDrug extends Drug {
public SuperXingJunDanDrug() {
name = "超级行军丹";
System.out.println("开始制作:祖传秘法秘制七天");
System.out.println("制作完成-->超级行军丹");
efficacy = "江湖中常见的疗伤药丸,尺寸小,便于携带,服用后见效快,恢复生命100点,具备超级功效-->同时恢复50点内力。";
}
}
超级金疮药
public class SuperJinChuangYaoDrug extends Drug {
public SuperJinChuangYaoDrug() {
name = "超级金疮药";
System.out.println("开始制作:祖传秘法秘制七七四十九天");
System.out.println("制作完成-->超级金疮药");
efficacy = "闻名天下的佛门疗伤奇药,闯荡江湖必备,加速愈合,服用后立即恢复生命500点,具备超级功效-->同时恢复250点内力。";
}
}
超级黑玉断续膏
public class SuperHeiYuDuanXuGaoDrug extends Drug {
public SuperHeiYuDuanXuGaoDrug() {
name = "超级黑玉断续膏";
System.out.println("开始制作:祖传秘法秘制九九八十一天");
System.out.println("制作完成-->超级黑玉断续膏");
efficacy = "传说中的稀世秘药,有再造之力,神奇无比,其价堪比黄金,服用后立即恢复生命2500点,具备超级功效-->同时恢复1000点内力。";
}
}
抽象工厂
public interface IDrugFactory {
/**
* 生产药品
* @return
*/
Drug createDrug(String type);
}
生产普通丹药的工厂
public class NormalDrugFactory implements IDrugFactory {
/**
* 生产普通药品
* @param type
* @return
*/
@Override
public Drug createDrug(String type) {
Drug drug = null;
// 生产药品
if ("行军丹".equals(type)) {
drug = new XingJunDanDrug();
} else if ("金疮药".equals(type)) {
drug = new JinChuangYaoDrug();
} else if ("黑玉断续膏".equals(type)) {
drug = new HeiYuDuanXuGaoDrug();
}
return drug;
}
}
生产超级丹药的工厂
public class SuperDrugFactory implements IDrugFactory{
/**
* 生产超级药品
* @param type
* @return
*/
@Override
public Drug createDrug(String type) {
Drug drug = null;
// 生产超级药品
if ("行军丹".equals(type)) {
drug = new SuperXingJunDanDrug();
} else if ("金疮药".equals(type)) {
drug = new SuperJinChuangYaoDrug();
} else if ("黑玉断续膏".equals(type)) {
drug = new SuperHeiYuDuanXuGaoDrug();
}
return drug;
}
}
药房类
public class Drugstore {
/**
* 药房接到订单生产药品
* @param type
* @return
*/
public Drug orderDrug(String type, boolean isVip) {
IDrugFactory drugFactory = null;
if(isVip) {
drugFactory = new SuperDrugFactory();
System.out.println("接到VIP订单:" + type + " 切换超级工厂");
} else {
drugFactory = new NormalDrugFactory();
System.out.println("接到普通订单:" + type + " 切换普通工厂");
}
// 通过工厂生产药品
Drug drug = drugFactory.createDrug(type);
// 打包药品
drug.packing();
return drug;
}
}
测试类
public class DrugstoreTest {
public static void main(String[] args) {
// 是否vip客户
boolean isVip = false;
Drugstore drugstore = new Drugstore();
System.out.println("为保证品质,本店所有药品只接受预定:");
System.out.println();
// 药房接到行军丹订单
Drug drug = drugstore.orderDrug("行军丹", isVip);
System.out.println(drug);
// 药房接到金疮药订单
drug = drugstore.orderDrug("金疮药", isVip);
System.out.println(drug);
// 药房接到黑玉断续膏订单
drug = drugstore.orderDrug("黑玉断续膏", isVip);
System.out.println(drug);
}
}
测试结果
isVip = false 普通工厂
为保证品质,本店所有药品只接受预定:
接到普通订单:行军丹 切换普通工厂
开始制作:祖传秘法秘制七天
制作完成-->行军丹
打包药品:行军丹
---- 行军丹 使用说明 ----
具有功效:江湖中常见的疗伤药丸,尺寸小,便于携带,服用后见效快,恢复生命100点。
接到普通订单:金疮药 切换普通工厂
开始制作:祖传秘法秘制七七四十九天
制作完成-->金疮药
打包药品:金疮药
---- 金疮药 使用说明 ----
具有功效:闻名天下的佛门疗伤奇药,闯荡江湖必备,加速愈合,服用后立即恢复生命500点。
接到普通订单:黑玉断续膏 切换普通工厂
开始制作:祖传秘法秘制九九八十一天
制作完成-->黑玉断续膏
打包药品:黑玉断续膏
---- 黑玉断续膏 使用说明 ----
具有功效:传说中的稀世秘药,有再造之力,神奇无比,其价堪比黄金,服用后立即恢复生命2500点。
isVip = true 切换超级工厂
为保证品质,本店所有药品只接受预定:
接到VIP订单:行军丹 切换超级工厂
开始制作:祖传秘法秘制七天
制作完成-->超级行军丹
打包药品:超级行军丹
---- 超级行军丹 使用说明 ----
具有功效:江湖中常见的疗伤药丸,尺寸小,便于携带,服用后见效快,恢复生命100点,具备超级功效-->同时恢复50点内力。
接到VIP订单:金疮药 切换超级工厂
开始制作:祖传秘法秘制七七四十九天
制作完成-->超级金疮药
打包药品:超级金疮药
---- 超级金疮药 使用说明 ----
具有功效:闻名天下的佛门疗伤奇药,闯荡江湖必备,加速愈合,服用后立即恢复生命500点,具备超级功效-->同时恢复250点内力。
接到VIP订单:黑玉断续膏 切换超级工厂
开始制作:祖传秘法秘制九九八十一天
制作完成-->超级黑玉断续膏
打包药品:超级黑玉断续膏
---- 超级黑玉断续膏 使用说明 ----
具有功效:传说中的稀世秘药,有再造之力,神奇无比,其价堪比黄金,服用后立即恢复生命2500点,具备超级功效-->同时恢复1000点内力。
抽象工厂模式可以快速的切换不同的系列,实现了系列和系列之间的隔离。
如果药房接到了普通订单,就去普通工厂下单生产,如果接到了vip订单就去超级工厂下单生产,这样就能快速切换生产不同的系列产品了。
简单工厂模式:如果对象的创建逻辑都比较简单的时候,直接用new 来创建对象就可以了,推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。
虽然也会有if逻辑判断,如果不是太复杂,也是可以接受的,也可以通过map来消除if判断。
工厂方法模式:如果每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的简单工厂类,推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。
同时,创建代码抽离到独立的工厂类之后可以方便复用。
抽象工厂模式:如果需要随时切换不同的系列产品,推荐使用抽象工厂模式,实现不同系列之间的隔离,快速切换工厂。
全真教掌门王重阳「中神通」为免江湖仇杀不断,提出「华山论剑」,胜者为「天下第一高手」,并可拥有《九阴真经》。
大战在即,天下豪杰这几日都赶往华山,华山脚下好不热闹,大家都想见证天下第一高手的诞生。
近日,仙草药房忽然来了几波神秘人物,看似都很有来头,师父不敢怠慢,依次请入内屋详谈。
小帅发现好几位神秘客人的订单中都有号称“黑黄金”的超级黑玉断续膏,这可稀奇了,超级黑玉断续膏贵比黄金,产量极其稀少,不是一般的客人有资格和能力预定的,师父只供应极少数的vip客户。
小帅偷偷看了一下客户名单,光看名字就惊出一身冷汗,他们分别是:(东邪)黄药师,(西毒)欧阳锋,(南帝)段智兴,(北丐)洪七公。