玩转设计模式-多来源的数据结构统一
你将:
清楚多来源的数据如果做到结构统一
对模板方法模式有更多的了解
对简单工厂(虽说不是设计模式中的一种,但是用的还是挺多的)能有更深的理解
知道设计模式之间的组合使用
前言
本文主要是记录和分享我在做ETL的业务时解决多种不同来源的数据进行结构化统一的问题。本文涉及了23种设计模式中的工厂模式和模板方法模式。对这两种模式不太熟悉的同学可以看我之前写的相关文章:
????模板方法模式???? ????工厂模式????
业务说明
在消息队列中,有各种不同平台的素材数据,但是每个平台的素材数据可能都稍有不同,我们需要将这些数据处理一下,最后输出统一的数据格式供下游进行计算处理;同时也要有良好的拓展性,以供我们后续拓展素材来源。
我画了下面这张图来帮助大家理解业务场景
数据抽取
首先数据抽取转换的结果是获得一个统一的数据结构的对象,而且是多种来源。所以我们可以使用工厂模式来进行解耦。这里我就使用简单工厂模式了,虽然它不是23种设计模式中的,但是用起来比较简单。
由于虽然是不同来源的数据,但是多多少少还是会有一些相同的属性的,我们可以将这些共同的属性统一进行设置,不同的属性再单独进行处理,这也符合我们的模板方法模式的应用场景。
具体类的设计如下:
准备工作
引入maven依赖:
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.78</version> </dependency> </dependencies> 复制代码
本文主要用来讲解使用设计模式来解决实际业务,所以其他的依赖,诸如:
kafka
、flink
等都没有引入
功能实现
MediaEnum
package enums; import lombok.Getter; /** * @author 菜菜 * @date 2022/1/9 22:21 * @description 素材枚举 */ @Getter public enum MediaEnum { UNKNOWN("未知", 0,"unknown"), FIRST_TRANSFORM("来源一", 1,"first_transform"), SECOND_TRANSFORM("来源二", 2,"second_transform"), THIRD_TRANSFORM("来源三", 3,"third_transform"); private String name; private int code; private String alias; MediaEnum(String name, int code, String alias) { this.name = name; this.code = code; this.alias = alias; } public static MediaEnum create(String name) { for (MediaEnum media : values()) { if (media.name.equals(name)) { return media; } } return UNKNOWN; } } 复制代码
KafkaData
package entity; import com.alibaba.fastjson.annotation.JSONField; import lombok.Data; import java.time.LocalDateTime; import java.util.List; import java.util.Map; /** * @author 菜菜 * @date 2022/1/9 22:00 * @description kafka原始数据 */ @Data public class KafkaData { /** * 标题 */ private String title; /** * 描述 */ private String description; /** * 作者 */ private String author; /** * 分类 */ private List<String> tags; /** * 素材来源 * {@link enums.MediaEnum} */ private String mediaName; /** * 素材路径 */ private String ossPath; /** * 爬取时间 */ @JSONField(name = "spider_time", format = "yyyy-MM-dd HH:mm:ss") private LocalDateTime spiderTime; /** * 原始字段 */ @JSONField(name = "raw_data") private Map<String, Object> rawData; } 复制代码
rawData
属性记录的是素材原始的数据,而其他属性其实都是从rawData
中提取出来的,只不过各个来源的数据都拥有这些属性,所以将其提取出来。
Material
package entity; import lombok.Data; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author 菜菜 * @date 2022/1/9 22:14 * @description 统一结构数据 */ @Data public class Material { /** * 标题 */ private String title; /** * 描述 */ private String description; /** * 作者 */ private String author; /** * 标签 */ private List<String> tags; /** * 素材来源 * {@link enums.MediaEnum} */ private Integer media; /** * 素材路径 */ private String ossPath; /** * 爬取时间 */ private LocalDateTime spiderTime; /** * md5 */ private String md5; /** * 封面图路径 */ private String cover; /** * 素材时长 */ private Integer duration; /** * 素材宽度 */ private Integer width; /** * 素材长度 */ private Integer height; /** * 素材格式 */ private String format; /** * 拓展字段 */ private Map<String, Object> expand = new HashMap<>(); } 复制代码
TransformMetaData
package meta; import entity.KafkaData; import entity.Material; import enums.MediaEnum; import lombok.Data; /** * @author 菜菜 * @date 2022/1/9 22:32 * @description 转换元数据 */ @Data public abstract class TransformMetaData { /** * 获取统一数据 */ public final Material getMaterial(KafkaData source) { if (source != null) { Material material = new Material(); material.setTitle(source.getTitle()); material.setDescription(source.getDescription()); material.setAuthor(source.getAuthor()); material.setTags(source.getTags()); MediaEnum mediaEnum = MediaEnum.create(source.getMediaName()); material.setMedia(mediaEnum.getCode()); material.setSpiderTime(source.getSpiderTime()); material.setOssPath(source.getOssPath()); //填充数据 this.fill(source, material); return material; } return null; } /** * 填充数据 */ public abstract void fill(KafkaData source, Material material); } 复制代码
这里就用到了模板方法模式,将相同部分的代码放在抽象的父类的
getMaterialBo
方法中,而将不同的代码放入不同的子类的fill
方法中实现
FirstTransform
package meta; import entity.KafkaData; import entity.Material; /** * @author 菜菜 * @date 2022/1/9 22:52 * @description 来源一 */ public class FirstTransform extends TransformMetaData{ @Override public void fill(KafkaData source, Material material) { } } 复制代码
SecondTransform
package meta; import entity.KafkaData; import entity.Material; /** * @author 菜菜 * @date 2022/1/9 22:52 * @description 来源二 */ public class SecondTransform extends TransformMetaData{ @Override public void fill(KafkaData source, Material material) { } } 复制代码
ThirdTransform
package meta; import entity.KafkaData; import entity.Material; /** * @author 菜菜 * @date 2022/1/9 22:52 * @description 来源三 */ public class ThirdTransform extends TransformMetaData{ @Override public void fill(KafkaData source, Material material) { } } 复制代码
TransformFactory
package meta; import entity.KafkaData; import entity.Material; import enums.MediaEnum; import lombok.extern.slf4j.Slf4j; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.Set; /** * @author 菜菜 * @date 2022/1/9 22:56 * @description 数据转换工厂 */ @Slf4j public class TransformFactory { private static Map<String, TransformMetaData> map = new HashMap(); private final static String PROPERTIES_NAME = "transform.properties"; /** * 初始化工厂类 */ static { Properties p = new Properties(); InputStream is = TransformFactory.class.getClassLoader().getResourceAsStream(PROPERTIES_NAME); try { p.load(is); //遍历Properties集合对象 Set<Object> keys = p.keySet(); for (Object key : keys) { //根据键获取值(全类名) String className = p.getProperty((String) key); //获取字节码对象 Class clazz = Class.forName(className); TransformMetaData obj = (TransformMetaData) clazz.newInstance(); map.put((String) key, obj); } } catch (Exception e) { log.error("数据转换类加载失败", e); e.printStackTrace(); } } /** * 根据媒体名称获取转换后的对象 * * @return */ public static Material getMaterial(KafkaData source) { if (source == null || source.getMediaName() == null) { log.error("source为空或source中不包含素材来源,data=[{}]", source); return null; } MediaEnum mediaSource = MediaEnum.create(source.getMediaName()); TransformMetaData transformMetaData = map.get(mediaSource.getAlias()); if (transformMetaData == null) { log.error("无法解析的素材来源:data=[{}]", source.getMediaName()); return null; } return transformMetaData.getMaterial(source); } } 复制代码
然后我们需要在创建一个transform.properties
文件用来工厂类的初始化
first_transform=meta.FirstTransform second_transform=meta.SecondTransform third_transform=meta.ThirdTransform 复制代码
Client
import entity.KafkaData; import entity.Material; import meta.TransformFactory; /** * @author 菜菜 * @date 2022/1/9 23:23 */ public class Client { public static void main(String[] args) { //这里我就不设置值了 KafkaData kafkaData = new KafkaData(); Material material = TransformFactory.getMaterial(kafkaData); //这里得到的就是统一格式之后的数据啦 } } 复制代码
对于不同来源的素材,我们可以分别在对应的
Transform
类下的fill
方法做处理;同时后期拓展的时候也很容易,只需要在transform.properyties
文件中添加相应的对应规则,properties的key命名可以通过TransformFactory
类的60行知晓,并且再添加相应的Transform
类继承TransformMetaData
类即可
作者:是王菜菜呀
链接:https://juejin.cn/post/7051385885488578590