代理模式介绍

本文探讨了软件工程中的委托模式,特别是在处理复杂数据源的Spring Batch应用程序中。通过使用委托模式,可以简化处理器的复杂性,提高代码的可维护性。文章详细介绍了如何在读取器和写入器中实施委托模式,以及如何利用PeekableItemReader进一步优化流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 代表:被选中或当选为他人投票或代表他人的人– Merriam-Webster
  • 委托模式:在软件工程中,委托模式是面向对象编程中的一种设计模式,其中,一个对象而不是执行其陈述的任务之一,而是将该任务委托给一个关联的辅助对象Wikipedia
  • 让事情尽可能简单,但不要简单- 爱因斯坦 Albert Einstein)释义

Spring Batch是Enterprise Java工具箱中的重要工具。 它提供了开箱即用的强大功能,尤其是从不同来源读取和写入数据时。 我们在此博客中提供了几篇介绍Spring Batch的文章。 如果您不熟悉Spring Batch和Reader,Processor,Writer Tasklet,请花点时间回顾一下。

我上面使用的措辞对我来说很重要。 我尝试做的一件事就是保持我提供的代码尽可能可维护。 我希望它能正常工作,但是我今天签入的代码将在以后某个日期由某些人维护。 保持代码尽可能简单是确保代码易于维护的一种方法。

那么,当您必须处理复杂的数据源时会发生什么呢?

我们发现,经常要处理的输入文件并不像每行一个记录那么简单。 通常,文件中有多行仅描述一条记录。

例如:

HKaren Traviss
LAB00KW3VG2G
LI0345478274
LI0345511131
F00000003
HJim Butcher
LI0451457811
F00000001
HDave Duncan
LI0380791277
LI0345352912
F00000002
HRik Scarborough
LI9999999999
F00000001

在这里,我们有一个文件,其中包含十五行中的四个记录。 每条记录均以页眉行开头,包含一个或多个正文行,并以页脚结尾。 标头包含线型(标头为H)和名称。 该行还包含线型(L),查找类型(在此示例中为ISBN或Amazon代码)以及查找书本的键。 页脚再次包含线型和此块中的记录数。

使用标准的读取器,将读取每一行,然后传递给处理器,然后处理器必须确定处理的是哪种类型的行。 然后,处理程序在处理每条正文行时,处理程序将必须保留来自每个标头的信息,直到处理了页脚。 然后,编写者将必须知道处理器发送的每一行,以及是否应将其写入。 这很复杂,部分是因为多个对象必须知道如何读取文件,而不是处理器仅关心单个对象,而编写器仅关心编写给出的内容。

相反,让我们将Delegate模式引入Reader并让其处理创建整个记录的过程。 由于我们具有来自多行的信息以及用于创建每个记录的页眉和页脚,因此我们必须将记录列表传递给处理程序。 你们当中的观察者会注意到,每个记录都包含一个ISBN或Amazon图书符号,并且可以用来查找作者(也包含在标题中)。 在现实生活中,这种冗余可能也不会发生。

让我们将输出包装在另一个对象中,以使其更易于使用。

public class OrderReaderStep implements ItemReader<OrderList> {

    private static final Logger logger = LoggerFactory.getLogger(OrderReaderStep.class);
    private FlatFileItemReader
<FieldSet> delegate;
    private static final String FOOTER = "F*";
    private static final String BODY = "L*";
    private static final String HEADER = "H*";

    @BeforeStep
    public void beforeStep(StepExecution stepExecution) {
        delegate = new FlatFileItemReader<>();

        delegate.setResource(new ClassPathResource("orders.txt"));

        final DefaultLineMapper
<FieldSet> defaultLineMapper = new DefaultLineMapper<>();
        final PatternMatchingCompositeLineTokenizer orderFileTokenizer = new PatternMatchingCompositeLineTokenizer();
        final Map<String, LineTokenizer> tokenizers = new HashMap<>();
        tokenizers.put(HEADER, buildHeaderTokenizer());
        tokenizers.put(BODY, buildBodyTokenizer());
        tokenizers.put(FOOTER, buildFooterTokenizer());
        orderFileTokenizer.setTokenizers(tokenizers);
        defaultLineMapper.setLineTokenizer(orderFileTokenizer);
        defaultLineMapper.setFieldSetMapper(new PassThroughFieldSetMapper());

        delegate.setLineMapper(defaultLineMapper);

        delegate.open(stepExecution.getExecutionContext());
    }

    @AfterStep
    public void afterStep(StepExecution stepExecution) {
        delegate.close();
    }

    @Override
    public OrderList read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        logger.info("start read");

        OrderList record = null;

        FieldSet line;
        List<Order> bodyList = new ArrayList<>();
        while ((line = delegate.read()) != null) {
            String prefix = line.readString("lineType");
            if (prefix.equals("H")) {
                record = new OrderList();
                record.setName(line.readString("name"));
            } else if (prefix.equals("L")) {
                Order order = new Order();
                order.setLookup(line.readString("lookupKey"));
                order.setLookupType(line.readString("keyType"));
                bodyList.add(order);
            } else if (prefix.equals("F")) {
                if (record != null) {
                    if (line.readLong("count") != bodyList.size()) {
                        throw new ValidationException("Size does not match file count");
                    }
                    record.setOrders(bodyList);
                }
                break;
            }

        }
        logger.info("end read");
        return record;
    }

    private LineTokenizer buildBodyTokenizer() {
        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();

        tokenizer.setColumns(new Range[]{ //
            new Range(1, 1), // lineType
            new Range(2, 2), // keyType
            new Range(3, 12) // lookup key
        });

        tokenizer.setNames(new String[]{ //
            "lineType",
            "keyType",
            "lookupKey"
        }); //
        tokenizer.setStrict(false);
        return tokenizer;
    }

    private LineTokenizer buildFooterTokenizer() {
        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();

        tokenizer.setColumns(new Range[]{ //
            new Range(1, 1), // lineType
            new Range(2, 9) // count
        });

        tokenizer.setNames(new String[]{ //
            "lineType",
            "count"
        }); //
        tokenizer.setStrict(false);
        return tokenizer;
    }

    private LineTokenizer buildHeaderTokenizer() {
        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();

        tokenizer.setColumns(new Range[]{ //
            new Range(1, 1), // lineType
            new Range(2, 20), // name
        });

        tokenizer.setNames(new String[]{ //
            "lineType",
            "name"
        }); //
        tokenizer.setStrict(false);
        return tokenizer;
    }

}

此Reader实现ItemReader接口。 这为我们提供了一个由作业调用的read方法,直到它返回null或发生错误时引发异常。 在我们的Reader中,我们声明另一个Reader,这是一个FlatFileItemReader。 这是我们的代表,即为我们执行功能所选择的对象。 我们的read方法将以委托的读取为循环,直到读取Footer。 然后它将整个记录捆绑到其包装器中,并将其传递给处理器。

必须先打开委托阅读器,然后才完成使用。 我必须在此处将它初始化并在此处进行设置,因此在BeforeStep中在此处打开它。 我也可以将包含的阅读器实现为ItemStreamReader,并使用Interface给我们的open,close以及update方法。

将简化的对象返回给Processor可以使我们大大简化Processor:

@Override
public List<BookList> process(OrderList orderList) throws Exception {
    logger.info("process");
    List<BookList> books = new ArrayList<>();
    for (Order order : orderList.getOrders()) {
        BookList bl = doProcessing(orderList.getName(), order);
        books.add(bl);
    }
    return books;
}

doProcessing方法可以包含此Job的业务逻辑,并且需要创建一个有效的BookList对象。 由于我们正在处理多个记录,因此该过程将创建多个可返回的BookList,并将其传递给Writer。 我将留给您填写该对象的其余部分,但这只是一个标准的ItemProcessor。 处理器不必在调用之间保留记录信息,因此程序员可以专注于业务逻辑。

我们的编写器实现ItemStreamWriter。 这为我们提供了比ItemWriter更多的方法,但是,如果您喜欢使用ItemWriter(类似于我们阅读器的方法),请确保在BeforeStep中打开Delegate,在AfterStep中将其关闭。

在Writer中使用委托使我们能够遍历Writer从Reader和Process收到的List。

public class ListWriter implements ItemStreamWriter<List<BookList>> {

    private static final Logger logger = LoggerFactory.getLogger(ListWriter.class);

    private FlatFileItemWriter<BookList> delegate;

    @BeforeStep
    public void beforeStep(StepExecution stepExecution) {
        delegate = new FlatFileItemWriter<>();
        delegate.setResource(new FileSystemResource("booklist.csv"));
        delegate.setShouldDeleteIfEmpty(true);
        delegate.setAppendAllowed(true);

        DelimitedLineAggregator<BookList> dla = new DelimitedLineAggregator<>();
        dla.setDelimiter(",");
        BeanWrapperFieldExtractor<BookList> fieldExtractor = new BeanWrapperFieldExtractor<>();
        fieldExtractor.setNames(new String[]{"bookName", "author"});
        dla.setFieldExtractor(fieldExtractor);
        delegate.setLineAggregator(dla);
    }

    @Override
    public void close() throws ItemStreamException {
        delegate.close();
    }

    @Override
    public void open(ExecutionContext ec) throws ItemStreamException {
        delegate.open(ec);
    }

    @Override
    public void update(ExecutionContext ec) throws ItemStreamException {
        delegate.update(ec);
    }

    @Override
    public void write(List<? extends List<BookList>> list) throws Exception {
        logger.info("write");
        for (List<BookList> bookList : list) {
            delegate.write(bookList);
        }
    }

}

这为我们提供了以下输出:

Going Grey,Karen Traviss
Hard Contact,Karen Traviss
501st,Karen Traviss
Storm Front,Jim Butcher
Lord of the Fire Lands,Dave Duncan
The Reluctant Swordsman,Dave Duncan
Wolfbrander Series Unpublished,Rik Scarborough

那么,如果稍微复杂一些并且输入文件不包含页脚,会发生什么呢?

逻辑记录仍然从标题行开始,但在下一个标题之前的行结束。 在我们之前的示例中,系统必须先读取下一行,然后才能知道已完成,然后具有一些复杂的逻辑来保留该信息以进行下一个遍历。

HKaren Traviss
LAB00KW3VG2G
LI0345478274
LI0345511131
HJim Butcher
LI0451457811
HDave Duncan
LI0380791277
LI0345352912
HRik Scarborough
LI9999999999

要求我们当前的作者在下一次电话会议之前先阅读并保留该记录是不必要的复杂操作,这会导致维护麻烦。 但是,我们可以使用PeekableItemReader简化此过程:

class OrderReaderStep2 implements ItemStreamReader<OrderList> {

    private static final String BODY = "L*";
    private static final String HEADER = "H*";
    private static final Logger logger = LoggerFactory.getLogger(OrderReaderStep2.class);
    private SingleItemPeekableItemReader
<FieldSet> delegate;

    @BeforeStep
    public void beforeStep(StepExecution stepExecution) {
        FlatFileItemReader fileReader = new FlatFileItemReader<>();

        fileReader.setResource(new ClassPathResource("orders2.txt"));

        final DefaultLineMapper
<FieldSet> defaultLineMapper = new DefaultLineMapper<>();
        final PatternMatchingCompositeLineTokenizer orderFileTokenizer = new PatternMatchingCompositeLineTokenizer();
        final Map<String, LineTokenizer> tokenizers = new HashMap<>();
        tokenizers.put(HEADER, buildHeaderTokenizer());
        tokenizers.put(BODY, buildBodyTokenizer());
        orderFileTokenizer.setTokenizers(tokenizers);
        defaultLineMapper.setLineTokenizer(orderFileTokenizer);
        defaultLineMapper.setFieldSetMapper(new PassThroughFieldSetMapper());

        fileReader.setLineMapper(defaultLineMapper);

        delegate = new SingleItemPeekableItemReader<>();
        delegate.setDelegate(fileReader);
    }

    @Override
    public void close() throws ItemStreamException {
        delegate.close();
    }

    @Override
    public void open(ExecutionContext ec) throws ItemStreamException {
        delegate.open(ec);
    }

    @Override
    public OrderList read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        logger.info("start read");

        OrderList record = null;

        FieldSet line;
        List<Order> bodyList = new ArrayList<>();
        while ((line = delegate.read()) != null) {
            String prefix = line.readString("lineType");
            if (prefix.equals("H")) {
                record = new OrderList();
                record.setName(line.readString("name"));
            } else if (prefix.equals("L")) {
                Order order = new Order();
                order.setLookup(line.readString("lookupKey"));
                order.setLookupType(line.readString("keyType"));
                bodyList.add(order);
            }

            FieldSet nextLine = delegate.peek();
            if (nextLine == null || nextLine.readString("lineType").equals("H")) {
                record.setOrders(bodyList);
                break;
            }

        }
        logger.info("end read");
        return record;
    }

    @Override
    public void update(ExecutionContext ec) throws ItemStreamException {
        delegate.update(ec);
    }

    private LineTokenizer buildBodyTokenizer() {
        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();

        tokenizer.setColumns(new Range[]{ //
            new Range(1, 1), // lineType
            new Range(2, 2), // keyType
            new Range(3, 12) // lookup key
        });

        tokenizer.setNames(new String[]{ //
            "lineType",
            "keyType",
            "lookupKey"
        }); //
        tokenizer.setStrict(false);
        return tokenizer;
    }

    private LineTokenizer buildHeaderTokenizer() {
        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();

        tokenizer.setColumns(new Range[]{ //
            new Range(1, 1), // lineType
            new Range(2, 20), // name
        });

        tokenizer.setNames(new String[]{ //
            "lineType",
            "name"
        }); //
        tokenizer.setStrict(false);
        return tokenizer;
    }

}

这次,我确实将包含的Reader实现为ItemStreamReader,以向您展示它们之间的区别。 可以像上一个一样将其实现为ItemReader。

PeekableItemReader允许我们向前查看下一条记录,以查看是否到达记录的末尾或文件的末尾。 然后可以使用相同的处理器和写入器来产生与以前相同的输出。

最后的想法

乍一看,委托模式似乎不像使用单个读取器或写入器那么简单。 这两个对象都有更多的配置。 但是我最喜欢的释义是说要尽可能简单,而且再简单不过。 稍微复杂一点的Reader和Writer将使您的Processor更加简单,并有助于后续维护。

代码很好,我的朋友。

翻译自: https://www.javacodegeeks.com/2016/03/introducing-delegate-pattern.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值