看完本文

你将:

  • 清楚多来源的数据如果做到结构统一
  • 对模板方法模式有更多的了解
  • 对简单工厂(虽说不是设计模式中的一种,但是用的还是挺多的)能有更深的理解
  • 知道设计模式之间的组合使用

前言

本文主要是记录和分享我在做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>
复制代码

本文主要用来讲解使用设计模式来解决实际业务,所以其他的依赖,诸如:kafkaflink等都没有引入

功能实现

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

如果你觉的本文对你有帮助,麻烦点赞关注支持一下