大家好,欢迎关注极客架构师,极客架构师,专注架构师成长,我是码农老吴。
本期是《架构师基本功之设计模式》的第9期,我将基于工厂系列模式(简单工厂,工厂方法,抽象工厂)打造可扩展的,支持多语言的电商平台店铺商品销售数据导出模块。
说起创建对象,所有java程序员,脑子里面的第一反应,差不多应该都是“new”一个什么,什么类,这是码农的条件反射。如果你还没这个条件反射,大概说明你才刚刚入门。我们大多数人,刚开始学习编程,案例代码里面,常常充满了创建对象“new 类名称”这些代码片段。
但是,上班后,在大型的生产项目开发中,通过“new”创建对象反而越来越少。在互联网的后端开发中,比较常见的基于spring的三层架构,不论是表示层,服务层,或者是逐渐消失的数据访问层,在这里层的类里面,我们要访问依赖的对象,往往是直接使用被定义为属性的类对象。而很少我们自己去new一个对象。这是因为spring框架的DI机制或者叫IOC机制,幕后帮助我们干了很多事情。让我们专注于业务代码开发,在需要某个对象时,往往是信手拿来即可,不需要自己亲力亲为,所以现在的程序员是幸福的。
很多年前,在spring框架广泛使用之前,有谁能帮我们干这些事情呢,那就是我今天要给大家分享的创建型设计模式。前面我们已经分享了一个使用率非常高,结构非常简单的创建型设计模式,那就是单例模式。本讲,为了连贯性,我们要一口气分享完,简单工厂模式,工厂方法模式,抽象工厂模式,这几个工厂系列模式。这些模式,在spring框架兴盛之前,门庭若市,程序员必备,现在则有点像没落的豪门,日渐衰落,门可罗雀了,除了面试的时候,大家还能想起它们。但是它们的精神依然存在,在特定的业务场景,依然有它们的用武之地,掌握它们的基本原理,对于我们理解sping相关的机制,也有很大的帮助。
本讲涉及三个设计模式,结论太多,在此就不一一列出,否则有骗稿费的嫌疑。
案例介绍-电商平台可扩展,支持多国语言的数据导出平台
系统挑战
第1版代码:未使用设计模式
第2版代码:基于简单工厂模式
简单工厂(Simple factory)模式定义
简单工厂模式的痛点
第3版代码:基于工厂方法模式
工厂方法(factory method)模式定义
工厂模式-自产自销形态
完善案例-导出优美的excel文件
第4版代码:基于工厂方法模式的自产自销形态
使用REIS模型分析工厂方法模式
工厂方法模式通用代码
完善案例-导出SQL语句并支持国际化
第5版代码:基于抽象工厂模式
抽象工厂(Abstract factory)模式定义
使用REIS模型分析抽象工厂模式
抽象工厂模式通用代码
总结
对于电商平台,普通人只关注自己是否可以轻松便捷地购买到物美价廉的商品,以及免费快捷的配送服务。这也是为什么,我的老东家,就是以“多快好省”作为品牌主张的。而另外一方面,作为电商平台上为数众多的店铺,商家,则需要时时刻刻关注商品的销售情况,对商品的销售数据进行多角度的统计,和深入的分析。作为电商平台一般也都会提供丰富的销售数据统计分析功能,毕竟现在已经进入大数据时代。
但是,统计的需求是千变万化的,在线的统计功能往往仍然不能满足商家的需求,以及商家自己公司的管理流程的需要,平台提供商品销售数据的导出功能,让商家用自己熟悉的方式,进行数据统计,就是电商平台一个必不可少的功能模块。
对于商品销售数据的导出的功能,不仅要考虑订单量的问题(因为不少店铺,日订单量都可以达到万,十万级别),还需要考虑导出的格式,比如常见的excel,CSV(Comma-Separated Values,CSV,字符分隔值),还有可能需要直接导出为SQL(mysql,oracle等)文件,另外,作为国际化电商平台,对应外企,还需要支持多语言的导出功能。这些都是导出功能需要考虑的问题。对于大一点的商家,往往会寻求ISV,进行定制开发,通过电商平台提供的API开发接口,进行自身的销售平台进行交互。
为了讲解下面的模式,我们的项目案例,先从一个简单的场景开始,逐渐完善,最终形成一个可扩展的,可支持多个语言的商品销售信息导出模块。这个简单的场景,就从导出excel开始,在java中导出excel,有不少类库,而使用率比较高的,就是apche的POI项目。excel文件格式一直在升级,POI项目也跟着相应升级,比如它里面的HSSF,可操作EXCEL2003版本,而SXSSF和XSSF可操作EXCEL2007以上版本,电商平台的导出功能模块,也需要根据这些进行扩展和升级。
因此,我们开发的excel导出功能,也需要有一定的扩展性,才能适应后面的变化。
接口及类说明:
SKU:商品信息
FileType:文件类型枚举类
IExcelExport:excel文件导出接口
Excel2003Export:excel2003版本的导出类
Excel2007Export:excel2007版本的导出类
FileExportService:文件导出服务类,可以导出各种文件
AbstractTest:测试类基类
TestFileExport:测试类
package com.geekarchitect.patterns.simplefactory.demo03;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
@Data
public class SKU implements Serializable {
private Long Id;
private String name;
private Long firstCategoryId;
private String firstCategoryName;
private Long secondCategoryId;
private String secondCategoryName;
private Long thirdCategoryId;
private String thirdCategoryName;
private BigDecimal price;
private Long shopId;
private String shopName;
private int sales;//销售量
private int stock;//库存量
}
package com.geekarchitect.patterns.simplefactory.demo03;
public enum FileType {
EXCEL_2003,EXCEL_2007;
}
package com.geekarchitect.patterns.simplefactory.demo03;
import java.util.List;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
public interface IExcelExport {
void exportExcel(List skuList);
}
package com.geekarchitect.patterns.simplefactory.demo03;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
public class Excel2003Export implements IExcelExport{
private static final Logger LOG = LoggerFactory.getLogger(Excel2003Export.class);
@Override
public void exportExcel(List skuList) {
LOG.info("导出商品:excel2003");
}
}
package com.geekarchitect.patterns.simplefactory.demo03;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
public class Excel2007Export implements IExcelExport{
private static final Logger LOG = LoggerFactory.getLogger(Excel2007Export.class);
@Override
public void exportExcel(List skuList) {
LOG.info("导出商品:excel2007");
}
}
package com.geekarchitect.patterns.simplefactory.demo03;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
public class FileExportService {
private static final Logger LOG = LoggerFactory.getLogger(Excel2003Export.class);
public void exportFile(List skuList,FileType fileType){
IExcelExport excelExport=null;
switch (fileType){
case EXCEL_2003:
LOG.info("文件导出服务:excel2003");
excelExport=new Excel2003Export();
break;
case EXCEL_2007:
LOG.info("文件导出服务:excel2007");
excelExport=new Excel2007Export();
break;
}
if(null !=excelExport){
excelExport.exportExcel(skuList);
}else{
LOG.info("不支持的文件类型");
}
}
}
package com.geekarchitect.patterns.simplefactory.demo03;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
public abstract class AbstractTest {
/**
* 生成测试数据
* @param max
* @return
*/
public List generateSku(int max){
List skuList=new ArrayList<>();
for(int i=0;i
上面的代码FileExportService的exportFile方法是关键代码,可以看到,在这版代码中,我们的文件导出类FileExportService,需要根据用户选择的excel版本,自己创建相应的excel导出类对象,也就是Excel2003Export,Excel2007Export。如果后面我们要支持很多格式的导出对象,这里面的代码,就会越来越复杂。
从文件导出的角度看,FileExportService类,本身是导出对象服务的使用者,也就是客户。而Excel2003Export,Excel2007Export类,则是服务方,提供Excel文件导出服务的。
客户方FileExportService,需要享受服务方的导出服务,首先需要自己创建服务方对象,怎么能让客户这么麻烦,这么辛苦呢,毕竟客户就是上帝,麻烦客户就是麻烦上帝,罪不可赦啊,完全可以找临时工干吗,这部分工作是否可以抽离出去,由专人负责呢。而这个临时工,或者说专门负责创建对象的类,就是我们后面的工厂系列模式里面的工厂。
在这版代码中,当我们新增一种导出文件格式时,需要做的除了增加一个导出类之外,就是需要修改这里的switch语句,这违反了系统设计中的开闭原则。
下面我们看第2版代码,基于简单工厂模式。
新增接口及类:
SimpleFactory:简单工厂类
FileExportServiceV2:文件导出服务第二版
package com.geekarchitect.patterns.simplefactory.demo04;
import com.geekarchitect.patterns.simplefactory.demo03.Excel2003Export;
import com.geekarchitect.patterns.simplefactory.demo03.Excel2007Export;
import com.geekarchitect.patterns.simplefactory.demo03.FileType;
import com.geekarchitect.patterns.simplefactory.demo03.IExcelExport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
public class SimpleFactory {
private static final Logger LOG = LoggerFactory.getLogger(SimpleFactory.class);
public static IExcelExport createExcelExport(FileType fileType){
LOG.info("简单工厂");
IExcelExport excelExport=null;
switch (fileType){
case EXCEL_2003:
excelExport=new Excel2003Export();
break;
case EXCEL_2007:
excelExport=new Excel2007Export();
}
return excelExport;
}
}
package com.geekarchitect.patterns.simplefactory.demo04;
import com.geekarchitect.patterns.simplefactory.demo03.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
public class FileExportServiceV2 {
private static final Logger LOG = LoggerFactory.getLogger(FileExportServiceV2.class);
public void exportFile(List skuList,FileType fileType){
LOG.info("文件导出服务");
IExcelExport excelExport=SimpleFactory.createExcelExport(fileType);
excelExport.exportExcel(skuList);
}
}
在这版代码中,我们的导出客户方FileExportServiceV2,不再自己创建导出服务方对象,而是通过一个新增的工厂类SimpleFactory,根据客户方传递的参数,创建相应的服务方对象。这就是我们要讲的第一个使用率非常高,结构非常简单,但是却没有名分,没有工作编制的设计模式,有点像我们国家的临时工一样。它就是简单工厂模式。SimpleFactory类就是简单工厂模式里面的工厂类,而Excel2003Export,Excel2007Export就是简单工厂模式里面的产品。我们看看这个设计模式的定义。
简单工厂模式,是从哪里起源的,是哪位前辈首先提出来的,我查阅了很多国外的资料,很遗憾,至今没找到,国外的那几个知名的网址,比如google,Wikipedia等,众所周知的原因,国内无法访问,看来要查到源头,需要翻墙才行,如果有哪位朋友知道简单工厂的起源,请评论区留言或者私信我一下,在此先谢谢啦。
引用codeproject网站,一个印度美女工程师(Snesh Prajapati)的原话:
According to definition from wikipedia,
Factory Pattern is "A factory is an object for creating other objects".
Simple Factory Pattern is a Factory class in its simplest form (In comparison to Factory Method Pattern or Abstract Factory Pattern).
In another way, we can say: In simple factory pattern, we have a factory class which has a method that returns different types of object based on given input.
国外网站分享简单工厂模式的很多,为什么引用这个工程师的话,是因为她引用了wikipedia的定义,所以有一定参考价值,这里面,简单工厂模式的定义,是字体加粗的那句话,中文解释如下:
在简单工厂模式中有一个类,类里面有一个方法,这个方法,根据传入的参数,返回不同类型的对象。
这个定义里面,有三个关键词,方法,入参,返回值。
方法
一个类的方法,这个类就是所谓的简单工厂模式里面的工厂,而这个方法,就是工厂类里面负责创建对象的方法,可以叫工厂方法(注意,这里的“工厂方法”四个字,有别于我们后面要讲的工厂方法设计模式名字里面的“工厂方法”)。关于工厂方法要注意几点:
1,在简单工厂模式中,为了简单,往往将这个方法,定义为static,方便我们快速使用。
2,一个工厂类中可以包含多个这样的方法,但是不要太多,可以一个模块,使用一个工厂类。
入参
简单工厂模式里面的工厂方法,往往需要一个入参,它根据这个入参,决定自己到底应该返回哪个类型的对象。对于这个入参,要注意的事项:
1,常常是常量或者枚举,便于方法里面的switch语句或者其他分支语句进行判断。
2,也有人喜欢直接传递一个Class类(也就是要实例化的对象),这样做简化了方法的逻辑代码,而且容易扩展,但是违背了工厂模式的宗旨,我个人不建议使用。
返回值
简单工厂模式,虽然很简单,它也是一个工厂模式,麻雀虽小五脏俱全,它的工厂类里面的工厂方法,职责就是要生成产品的,或者叫创建对象的。
而对于工厂类里面的工厂方法,生产的都应该是一类产品或者叫对象。所以工厂方法的返回值类型,常常是一个接口,也可以是抽象父类,而不会是一个具体的类,那样的话,这个工厂方法只能生成一个具体产品,失去了它的价值。而工厂方法,需要返回一类产品中的哪一个具体产品,则需要根据入参,进行确定。
最后,由于简单工厂模式简单,而且不是一个正规的设计模式,在GOF的原著里面没有编制,这里我就不使用RAIS分析模型,对它进行详细解析了。
但是,我们还是要看看它的痛点在哪里。
从上面的代码,我们可以体会到简单工厂的便捷之处,这是它为什么这么流行的原因;当然,我们也应该可以体会到它的不足知足,那就是扩展性,明显不符合大名鼎鼎的开闭原则,和我们的第一版代码,没有使用设计模式的缺陷一样。关于开闭原则及其他设计原则,我后面有专门的分享,欢迎大家关注。
当新增一个具体产品时,除了增加一个新的产品类之外,还需要修改工厂方法里面的条件语句。虽然增加一个分支语句就几行代码,但是就是因为这么一点点瑕疵,引起了质变,导致它不符合开闭原则中的对扩展开放,对修改关闭的原则。
即使这样,根据实用第一的原则,业务场景中,对于工厂系列模式,首选方案还应该是简单工厂模式,如果确实不够用,满足不了系统需求,再考虑后面我即将分享的,工厂模式里面的正式工,工厂方法模式,抽象工厂模式,甚至有可能是构造器模式等等。
下面我们开始第3版代码,基于工厂方法模式的代码实现。
接口及类:
IExcelExportFactory:抽象工厂接口,创建IExcelExport对象
AbstractExcelExportFactory:抽象工厂抽象类,创建IExcelExport对象
Excel2003ExportFactory:具体工厂,创建Excel2003Export对象
Excel2007ExportFactory:具体工厂,创建Excel2007Export对象
IExcelExport:抽象产品
Excel2003Export:具体产品,导出excel2003文件
Excel2007Export:具体产品,导出excel2007文件
FileExportService:工厂客户方
package com.geekarchitect.patterns.factorymethod.demo03;
/**
* 抽象工厂:接口
* @author 极客架构师@吴念
* @createTime 2022/6/15
*/
public interface IExcelExportFactory {
IExcelExport createExcelExport();
}
package com.geekarchitect.patterns.factorymethod.demo03;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/15
*/
public abstract class AbstractExcelExportFactory implements IExcelExportFactory{
private static final Logger LOG = LoggerFactory.getLogger(Excel2003Export.class);
@Override
public IExcelExport createExcelExport() {
LOG.info("抽象工厂类,提供一个默认操作");
return null;
}
}
package com.geekarchitect.patterns.factorymethod.demo03;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/15
*/
public class Excel2003ExportFactory extends AbstractExcelExportFactory {
private static final Logger LOG = LoggerFactory.getLogger(Excel2003ExportFactory.class);
@Override
public IExcelExport createExcelExport() {
LOG.info("具体工厂:创建Excel2003Export对象");
return new Excel2003Export();
}
}
package com.geekarchitect.patterns.factorymethod.demo03;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/15
*/
public class Excel2007ExportFactory extends AbstractExcelExportFactory {
private static final Logger LOG = LoggerFactory.getLogger(Excel2007ExportFactory.class);
@Override
public IExcelExport createExcelExport() {
LOG.info("具体工厂:创建Excel2007Export对象");
return new Excel2007Export();
}
}
package com.geekarchitect.patterns.factorymethod.demo03;
import java.util.List;
/**
* 抽象产品
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
public interface IExcelExport {
void exportExcel(List skuList);
}
package com.geekarchitect.patterns.factorymethod.demo03;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
public class Excel2003Export implements IExcelExport{
private static final Logger LOG = LoggerFactory.getLogger(Excel2003Export.class);
@Override
public void exportExcel(List skuList) {
LOG.info("具体产品:导出excel2003");
}
}
package com.geekarchitect.patterns.factorymethod.demo03;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* @author 极客架构师@吴念
* @createTime 2022/6/13
*/
public class Excel2007Export implements IExcelExport {
private static final Logger LOG = LoggerFactory.getLogger(Excel2007Export.class);
@Override
public void exportExcel(List skuList) {
LOG.info("具体产品:导出excel2007");
}
}
这版代码,我们基于工厂方法模式实现的,具体做了以下事情。
1,新增了一个抽象工厂接口IExcelExportFactory,里面包含了一个工厂方法的
createExcelExport,返回的对象是抽象产品对象IExcelExport。
2,新增了一个抽象工厂抽象类AbstractExcelExportFactory,里面对createExcelExport,提供了一个默认实现,这个类在当前的情景下可以没有,但是一般为了以后的扩展,都会创建出来。
3,新增两个具体的工厂类Excel2003ExportFactory,Excel2007ExportFactory,分别实现了创建Excel2003Export和Excel2007Export具体产品的对象。
而对于这个工厂方法模式里面生产的产品。
抽象产品接口是IExcelExport,两个具体的产品类是Excel2003Export和Excel2007Export,这些我们都比较熟悉。
我们的导出服务类FileExportService,根据用户选中的excel版本,实例化相应的具体工厂类。说到这里,大家有没有一种似曾相识的感觉。这里的代码,似曾相识,与君初相识,恰是故人归。
是的,相似的代码,在我们的第一版代码,未使用设计模式的代码中出现过。大家对比一下。
在第一版代码中,没有使用设计模式,导出服务类FileExportService,根据用户选中的excel版本,选择实例化哪个导出类。而在第三版代码中,我们使用了工厂方法模式,增加了一个接口,一个抽象类,两个工厂类,阵容不可谓不强大,但是,结果好像差强人意,导出服务类FileExportService,根据用户选中的excel版本,选择实例化实例化哪个具体工厂类。然后根据工厂类,获取相应的具体导出类对象。
这不是多此一举吗?
我想起了郭德纲的一句话“要是法律不管我,我早打死他了”。
但是,但是,但是。
从团队分工合作的角度看,这确实没毛病,每个角色都自己的职责。我们已经成功的将创建对象的职责,交给了具体的工厂类,虽然客户方又有了新的任务,需要自己建立工厂类的对象。但是此创建,非彼创建,创建一个工厂类,一般是很简单的,而创建一个业务对象,往往比较复杂,复杂到需要把这项工作交由专人负责的地步。
如果工厂方法模式,就只能简简单单,干这点事情,我,码农老吴,第一个不答应。不够折腾人的,有点得不偿失啊,还不如直接使用简单工厂模式。其实,这种情形,只是工厂方法模式的一种形态,一种被大部分讲解工厂方法模式的作者,高频分享的一种形态,但不是它的首选形态。
我认真研读了GOF的《Design Patterns: Elements of Reusable Object-Oriented Software》原著,以及国内热门的几本设计模式书籍,确认了工厂方法模式,它的原创作者,对于工厂方法模式,首先推荐的是它的另外一种形态,或者叫应用方式,落地方式等。这种形态,名字我还没想好,暂时叫它自产自销形态。
目前我参考的国内的几本设计模式书籍中,只有一本讲了这种形态,这本书我认为是目前国内,我所看过的设计模式书籍里面,理论水平比较接近原著的书籍,名字我就不说的,以免有广告之嫌。我们先看工厂方法的定义,再根据它的自产自销形态,重构我们的案例。
未完待续。。。
留言与评论(共有 0 条评论) “” |