首页 > 解决方案 > 将 Java 数据模型转换为 XLS 工作簿表的设计模式

问题描述

基本上,我想将 Java 对象模型转换为 XLS 工作簿的两个不同工作表。到目前为止,我已经解决了这个问题,但我正试图在设计模式方面找到更好的解决方案。

这是响应对象模型。

class Response {
  List<Model1> list;
}

class Model1 {
  String attr1;
  String attr2;
  List<Model2> list;

  @JsonProperty(value="attr1")
  String getAttr1() {..}
}

class Model2 {
  String attr1;
  String attr2;

  @JsonProperty(value="attr1")
  String getAttr1() {..}
}

对于转换层,我创建了一个具有不同实现的父接口,如下所示。

public interface Converter<T> {
  Sheet convert(T t);
}

public class DataConverter {

   public Workbook getWorkBook(Response res) {
      Sheet model1Sheet = new Model1SheetConverter().convert(res);
      Sheet model2Sheet = new Model2SheetConverter().convert(res);
      return new Workbook().sheets(List.of(model1Sheet, model2Sheet));
   }
}

public class Model1SheetConvert implements Converter<Response> {
   public sheet convert(Response res) {
       I do loop over here for data model 1 and then with some business logic generate sheet.
   }
}

public class Model2SheetConvert implements Converter<Response> {
   public sheet convert(Response res) {
       I do loop over here for each data model 1 then I pass data model 2 and then with some business logic generate sheet.
   }
}

现在这种方法的问题是:

  1. 我为数据模型 1 迭代了两次。
  2. 基于数据模型 1 的工作表 1 和工作表 2 将有一些动态行。我不能用这种方法在这里处理这部分。

有人可以为这种方法提出一个好的设计模式吗?或者,有没有其他方法,我可以做不同的事情。

标签: javaspring-bootdesign-patternsarchitecturesystem-design

解决方案


作为一种可能设计的基础,根据我的理解,这些是主要需求 -

  • 模型的每种类型(类)都需要一个转换器——例如,Model1 应该有它的转换器,而 Model2 应该有它自己的转换器。然后转换器生成(填充或写入)单个工作簿Sheet。我将进一步将转换器命名为 a SheetWriter,并将其多态方法命名为write(M model)method 。也就是说,接受其指定类型的模型。
  • 我们需要 2 个类 - aModel1Writer和 a Model2Writer- 每个都是这样创建的,它拥有一个 Sheet 实例并不断将数据(即行)附加到其中。每次调用其write(M model)方法时,都会返回相同的 Sheet 对象。因此,如果我们为 19 个实例和它们的 55 个“子”实例调用这两个write(M model)方法,那么我们仍然必须得到正好两张表:Sheet 包含这 19 个实例的所有信息(即 19 行),而Sheet 包含所有这 55个实例的信息(即 55 行)。Model1Model2Model1Model1Model2Model2
  • 因此,我们需要流式传输SheetWriterResponse 中的所有模型 - 包括其模型以及它们的所有子模型 - 并使用适当的实例(取决于模型的类)将它们转换(映射)到这些唯一的 2 张工作表。

考虑到上述基础 - 以下设计及其实现可能是我们的起始解决方案。它结合了工厂模式和策略。Factory 是具体 SheetWriters 的来源(给定一个具体模型),而 Strategy 是具体的 SheetWriter 例如写入逻辑,用于其相应的模型类。

在此处输入图像描述

下面是 ResponseConverter 的实现:

public class ResponseConverter {

    private final Writers factory;
    private final Map<Class<? extends WritableModel>, SheetWriter<? extends WritableModel>> writers;

    public ResponseConverter(Writers factory) {
        this.factory = factory;
        this.writers = new HashMap<>();
    }

    public Workbook toWorkbook(Response response) {
        Set<Sheet> sheets = streamAllModelsIn(response)
            .map(model -> getWriter(model).write(model))
            .collect(Collectors.toCollection(LinkedHashSet::new));

        return new Workbook(sheets.toArray(new Sheet[] {}));
    }

    private Stream<WritableModel> streamAllModelsIn(Response response) {
        List<Model1> mainModels = response.getModels();
        Stream<WritableModel> childrenStream = mainModels.stream().flatMap(this::streamChildren);

        return Stream.concat(mainModels.stream(), childrenStream);
    }

    private Stream<? extends WritableModel> streamChildren(Model1 model) {
        return model.getChildren().stream();
    }

    @SuppressWarnings("unchecked")
    private <M extends WritableModel> SheetWriter<M> getWriter(M model) {
        if (!writers.containsKey(model.getClass())) {
            writers.put(model.getClass(), factory.writerFor(model));
        }
        return (SheetWriter<M>) writers.get(model.getClass());
    }
}

在上面的清单中,主键是私有getWriter(M model)方法。它使用内部“缓存映射”writers来获取与当前模型的类对应的正确SheetWriter实例 - 或者,如果还没有这样的映射条目,则使用Writers factory来初始化它 - 询问适合给定模型的新实例.

第二部分是writers包的实现——意思是实际的工厂机制+具体的策略,SheetWriter将你的Model1Model2实例写入它们的输出表。这包括外部代码可以使用的“writers”包的三个“公共”组件(白色类),以及一些只在包机制内部使用且对 ResponseHandler 不可见的“隐藏”组件(灰色类)或者外面的任何东西。目标是能够在内部添加更多受包保护的 SheetWriter 类及其映射,而不会影响任何外部代码,也无需更改。

在此处输入图像描述

以下是“writers”包组件的代码清单

包的公开可用部分(API) -

public interface WritableModel { }

public interface SheetWriter<M extends WritableModel> {
    Sheet write(M model);
}

public class Writers {
    private static final Map<Class<? extends WritableModel>,
                             WriterFactory<? extends WritableModel, ? extends SheetWriter<?>>> factories;

    static {
        factories = new HashMap<>();
        factories.put(Model1.class, new Model1WriterFactory());
        factories.put(Model2.class, new Model2WriterFactory());
    }

    @SuppressWarnings("unchecked")
    public <M extends WritableModel> SheetWriter<M> writerFor(M model) {
        Class<? extends WritableModel> modelClass = model.getClass();
        if (isNull(factories.get(modelClass))) {
            throw new UnsupportedOperationException("No Writer for the provided Model's type (" + modelClass + ")");
        }
        return (SheetWriter<M>) factories.get(modelClass).get();
    }
}

以及所需工厂和策略的内部(包保护)实施 -

interface WriterFactory<M extends WritableModel, W extends SheetWriter<M>> {
    W get();
}

class Model1WriterFactory implements WriterFactory<Model1, Model1Writer> {
    @Override
    public Model1Writer get() {
        return new Model1Writer();
    }
}

class Model1Writer implements SheetWriter<Model1> {

    private final Sheet outputSheet;

    public Model1Writer() {
        this.outputSheet = new Sheet();
    }   

    @Override
    public Sheet write(Model1 model) {
        //this is your model-dependent logic of how Sheet is filled from the model
        outputSheet.addRow(model.getName() + ": has " + model.getChildCount() + " children");
        return outputSheet;
    }
}

class Model2WriterFactory implements WriterFactory<Model2, Model2Writer> {
    @Override
    public Model2Writer get() {
        return new Model2Writer();
    }
}

class Model2Writer implements SheetWriter<Model2&t; {
    private final Sheet outputSheet;

    public Model2Writer() {
        this.outputSheet = new Sheet();
    }

    @Override
    public Sheet write(Model2 model) {
        //this is your model-dependent logic of how Sheet is filled from the model
        outputSheet.addRow(model.toString());
        return outputSheet;
    }
}

上面的实现允许我们通过主要只添加新代码来扩展新(即Model3)类的工作表编写逻辑 - 并且只需稍微修改 Writers 类静态工厂映射 - 通过添加新条目。

但是,作为这种设计的一个限制 - 两个具体SheetWriter类都只接受它们相应的模型实例,以保持良好的狭窄责任焦点。但是,如果您想使用 Model1 中包含的有关 Model2 实例的信息并将其写入第一张表中 - 那么您可以这样做, Model1Writer因为您包含了那些 Model2 对象,但不能反过来 - 例如,如果在 Model2 表中,您还需要写一些来自响应的 Model1 对象的信息,那么目前这种设计是不可用的。如果确实有必要,您可以解决此问题 - 通过传递整个“上下文”,例如通过使SheetWriter接口方法也需要整个Response作为第二个参数:

public interface SheetWriter<M extends WritableModel> {
    Sheet write(M model, Response response);
}

为了前面代码的完整性,下面的清单还包括我对ResponseModel1Model2Workbook的“虚拟”实现Sheet

public class Response {
    private final List<Model1> models = new ArrayList<>();

    public Response(Model1... models) {
        this.models.addAll(asList(models));
    }

    public List<Model1> getModels() {
        return new ArrayList<>(models);
    }

    public void addModel(Model1 model) {
        models.add(model);
    }
}

@lombok.RequiredArgsConstructor
public class Model1 implements WritableModel {

    private final String name;
    private final List<Model2> children = new ArrayList<>();

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + " = " + this.name;
    }

    public void setChildren(List<Model2> children) {
        this.children.clear();
        this.children.addAll(children);
    }

    public String getName() {
        return name;
    }

    public int getChildCount() {
        return children.size();
    }

    public List<Model2> getChildren() {
        return new ArrayList<>(this.children);
    }
}

@lombok.RequiredArgsConstructor
public class Model2 implements WritableModel {

    private final String title;
    private final int value;

    public String toString() {
        return this.getClass().getSimpleName() + ": " + this.title + " (" + this.value + ")";
    }
}

@lombok.RequiredArgsConstructor
public class Workbook {

    private final List<Sheet> sheets;

    public Workbook(Sheet... sheets) {
        this.sheets = asList(sheets);
    }

    public List<Sheet> getSheets() {
        return sheets;
    }
}

public class Sheet {

    private List<String> rows = new ArrayList<>();

    public void addRow(String value) {
        rows.add(value);
    }

    public String toString() {
        return String.join("\n", rows);
    }
}

编辑:用下面的部分更新了答案,以包括数据模型委托

这是包含整个实现的我的存储库的链接:

https://github.com/Ristox/sheetwriters

数据模型的代码已经更新以实现委托——我们自己的data包定义了自定义模型类,它们都“包装”了包中原始的、不可修改/不可扩展的模型类blackbox,如下所示:


package org.example.workbook.sheetwriters.blackbox;

import java.util.ArrayList;
import java.util.List;

/**
 *  Pretend this is a "black-box" library class eg you know the public interface, but cannot change it
 */
public final class ExternalModel1 {
  private final String name;
  private final List<ExternalModel2> children = new ArrayList<>();

  public ExternalModel1(String name) {
    this.name = name;
  }

  @Override
  public String toString() {
    return this.getClass().getSimpleName() + " = " + this.name;
  }

  public void setChildren(List<ExternalModel2> children) {
    this.children.clear();
    this.children.addAll(children);
  }

  public String getName() {
    return name;
  }

  public int getChildCount() {
    return children.size();
  }

  public List<ExternalModel2> getChildren() {
    return new ArrayList<>(this.children);
  }
}



package org.example.workbook.sheetwriters.blackbox;

/**
 *  Pretend this is a "black-box" library class eg you know the public interface, but cannot change it
 */
public final class ExternalModel2 {
  private final String title;
  private final int value;

  public ExternalModel2(String title, int value) {
    this.title = title;
    this.value = value;
  }

  public String toString() {
    return this.getClass().getSimpleName() + ": " + this.title + " (" + this.value + ")";
  }
}


package org.example.workbook.sheetwriters.data;

import java.util.List;
import org.example.workbook.sheetwriters.blackbox.ExternalModel1;
import org.example.workbook.sheetwriters.writers.WritableModel;

public class Model1 implements WritableModel {

    private final ExternalModel1 delegate;

    public Model1(String name) {
        delegate = new ExternalModel1(name);
    }

    @Override
    public String toString() {
        return this.delegate.toString();
    }

    public void setChildren(List<Model2> children) {
        this.delegate.setChildren(Model2.toDelegates(children));
    }

    public String getName() {
        return this.delegate.getName();
    }

    public int getChildCount() {
        return this.delegate.getChildCount();
    }

    public List<Model2> getChildren() {
        return Model2.wrapAll(this.delegate.getChildren());
    }
}

package org.example.workbook.sheetwriters.data;

import static java.util.stream.Collectors.toList;

import java.util.List;
import org.example.workbook.sheetwriters.blackbox.ExternalModel2;
import org.example.workbook.sheetwriters.writers.WritableModel;

public class Model2 implements WritableModel {

    public static Model2 wrap(ExternalModel2 externalModel) {
        return new Model2(externalModel);
    }

    public static List<Model2> wrapAll(List<ExternalModel2> externalModels) {
        return externalModels.stream().map(Model2::wrap).collect(toList());
    }

    public static List<ExternalModel2> toDelegates(List<Model2> model2List) {
        return model2List.stream().map(Model2::toDelegate).collect(toList());
    }

    private final ExternalModel2 delegate;

    public Model2(String title, int value) {
        this.delegate = new ExternalModel2(title, value);
    }

    private Model2(ExternalModel2 delegate) {
        this.delegate = delegate;
    }

    public String toString() {
        return this.delegate.toString();
    }

    public ExternalModel2 toDelegate() {
        return this.delegate;
    }
}

如果最初的“黑盒”模型类有一个很大的公共接口,那么手动重复 + 委托整个接口可能看起来很乏味。在这种情况下,您可能希望查看编译时预处理器并使用它们的样板注释,例如Lombok 的 @Delegate。它有其起起落落,但如果使用得当、有意识地使用,应该能够以少得多的样板代码实现整个必要的委托。


推荐阅读