Refactor the code using the factory pattern without using the if-elseif condition

2

I have the following Factory . In it I instantiate a class responsible for parsing the file in question. In order to instantiate this parser, it is first checked under the conditions if that parser is the correct one to perform the processing.

public class InvoiceParserFactory
{
    public static InvoiceParser getParser(InvoicePdfReader reader)
    {
        String invoiceText = reader.getText();

        if (isXxxParser(invoiceText)) {
            return new XXX_PARSER(invoiceText);
        }
        else if (isYyyParser(invoiceText)) {
            return new YYY_PARSER(invoiceText);
        }
        else {
            throw new InvoiceException("Can't find parser.");
        }
    }

    private static boolean is_XXX_PARSER(String invoiceText) {
        Pattern p = Pattern.compile("xxx_regex");
        Matcher m = p.matcher(invoiceText);

        //as condições aqui são diferentes, portanto não da pra simplificar esses métodos
    }

    private static boolean is_YYY_PARSER(String invoiceText) {
        Pattern p = Pattern.compile("yyy_regex");
        Matcher m = p.matcher(invoiceText);

        //as condições aqui são diferentes, portanto não da pra simplificar esses métodos
    }
}

Here is an example of a PARSER :

public final class XXX_PARSER extends InvoiceParser
{
    @Override
    protected void parseSomeText() {
        //implementação
    }
}

This logic does not scale in this design pattern, since I will have hundreds or even thousands of PARSERS . I have seen in some examples that it would be interesting to put all PARSERS into a collection and implement a static method on each PARSER to check if the PARSER in question is what needs to be instantiated. But I would have to instantiate PARSER before I even use it, because I would have to use Set<InvoiceParser> and then set.add(new XXX_PARSER) and so on, except that if I instantiate this way in a collection I I break my entire algorithm which is based on the InvoiceParser class constructor, which is the parent of all PARSERS.

What would be the best way to refactor this Factory to meet my needs?

    
asked by anonymous 08.05.2018 / 02:34

3 answers

2

You could use a enum with an element for each parser type . In it you would have the way the parser is created and the corresponding pattern . Example:

Class InvoiceParser :

private abstract class InvoiceParser {/*métodos da classe*/}

Two implementations as an example:

private class XxxParser extends InvoiceParser {
    public XxxParser(String text) {}
}

E:

private class YyyParser extends InvoiceParser {
    public YyyParser(String text) {}
}

A% with a factory method that finds the appropriate parser :

public enum Parser {
    //O primeiro parâmetro é a regex
    //o segundo é a forma como se cria o parser
    XXX_PARSER("xxx_regex", XxxParser::new),

    YYY_PARSER("yyy_regex", YyyParser::new);

    //interface com um método para criar o parser
    //permite o uso de lambda e method references
    @FunctionalInterface
    private static interface ParseCreator {
        InvoiceParser create(String text);
    }

    //cache interno
    private static final Parser[] VALUES = Parser.values(); 

    private final Pattern pattern;

    private final ParseCreator creator;

    private Parser(String regex, ParseCreator creator) {
        this.pattern = Pattern.compile(regex);
        this.creator = creator;
    }

    //factory method que encontra o parser adequado ao reader
    public static final InvoiceParser forReader(InvoicePdfReader reader) {
        for(Parser parser : VALUES) {
            if(parser.pattern.matcher(reader.getText()).find()) {
                return parser.creator.create(reader.getText());
            }
        }
        throw new IllegalArgumentException();
    }
}

If regex is not the only criterion used to identify the appropriate parser, you could add to enum an object responsible for checking those additional criteria specific to each parser , then enum would look like this:

public enum Parser {
    //o primeiro parâmetro é a regex
    //o segundo é a forma como se cria o parser
    //o terceiro são os critérios adicionais
    XXX_PARSER("xxx_regex", XxxParser::new, reader -> {
        //suas condições adicionais
        return true;
    }),

    YYY_PARSER("yyy_regex", YyyParser::new, reader-> {
        //suas condições adicionais
        return true;
    });

    //interface com um método para criar o parser
    //permite o uso de lambda e method references
    @FunctionalInterface
    private static interface ParseCreator {
        InvoiceParser create(String text);
    }

    //interface com um método para verificar se o parser é o adequado
    //permite o uso de lambda e method references
    @FunctionalInterface
    private static interface Parseable {
        boolean canParse(InvoicePdfReader reader);
    }

    //cache interno
    private static final Parser[] VALUES = Parser.values();

    private final Pattern pattern;

    private final ParseCreator creator;

    private final Parseable parseable;

    private Parser(String regex, ParseCreator creator, Parseable parseable) {
        this.pattern = Pattern.compile(regex);
        this.creator = creator;
        this.parseable = parseable;
    }

    //factory method que encontra o parser adequado ao reader
    public static final InvoiceParser forReader(InvoicePdfReader reader) {
        for(Parser parser : VALUES) {
            if(!parser.pattern.matcher(reader.getText()).find()) {
                continue;
            }
            //verifica os critérios específicos do parser
            if(parser.parseable.canParse(reader)) {
                return parser.creator.create(reader.getText());
            }
        }
        throw new IllegalArgumentException();
    }
}

To find the appropriate parser would just do:

InvoiceParser parser = Parser.forReader(reader);
    
08.05.2018 / 16:44
1

You can use a ServiceLoader . This is the mechanism that was introduced in Java 6 to solve exactly the same problems you have. In your case, InvoiceParserFactory corresponds to a service. The implementations of this service are scattered on the classpath / modulepath in a lot of places. The function of ServiceLoader is to find all the implementations, instantiate them and give the result.

First of all, we'll give you a stir in your InvoiceParserFactory class (which has become an interface):

package com.example.invoice;

import java.util.ServiceLoader;

public interface InvoiceParserFactory {
    public boolean acceptsText(String text);

    public default boolean acceptsText(InvoicePdfReader reader) {
        return acceptsText(reader.getText());
    }

    public InvoiceParser newParser(String text);

    public default InvoiceParser newParser(InvoicePdfReader reader) {
        return newParser(reader.getText());
    }

    public static InvoiceParser getParser(InvoicePdfReader reader) {
        ServiceLoader<InvoiceParserFactory> loader =
                ServiceLoader.load(InvoiceParserFactory.class);

        for (InvoiceParserFactory factory : loader) {
            if (factory.acceptsText(reader)) {
                return factory.newParser(reader);
            }
        }

        throw new InvoiceException("Can't find parser.");
    }
}

This interface takes advantage of the fact that from Java 8, interfaces can have static methods or implementations default . From Java 9, private methods are also allowed in interfaces.

Let's suppose that this interface is inside the% JAR invoice-api.jar .

Then, in the file xxx.jar , you put this class:

package com.example.xxx.invoice;

import com.example.invoice.InvoiceParserFactory;

public class XxxInvoiceParserFactory implements InvoiceParserFactory {
    // ...

    public XxxInvoiceParserFactory() {
    }

    @Override
    public boolean acceptsText(String text) {
        Pattern p = Pattern.compile("xxx_regex");
        Matcher m = p.matcher(invoiceText);
        // ...        
    }

    @Override
    public InvoiceParser newParser(String text) {
        return new XxxParser(text);
    }
}

Within xxx.jar , you put a file with the same name as the offered service class / interface (not the implementation) within a folder named services within the META-INF folder. For example, in the case above, the file would be named META-INF/services/com.example.invoice.InvoiceParser . Within this file you put the full name of the implementing class:

com.example.xxx.invoice.XxxInvoiceParserFactory

Let's assume that% JAR% has two distinct implementations of yyy.jar , one InvoiceParser and the other YyyBlueInvoiceParser . In this case in the file YyyRedInvoiceParser within that other JAR you put this:

com.example.yyy.invoice.YyyBlueInvoiceParserFactory
com.example.yyy.invoice.YyyRedInvoiceParserFactory

That is, what happens is that each JAR has within the folder META-INF/services/com.example.invoice.InvoiceParser , files with the names of the services offered, within each of these files, is the name of the existing implementationsa these services.

An application that has META-INF/services , xxx.jar , and yyy.jar in the classpath will be able to see these three implementations. To change the implementations is easy, just that each JAR that contains some implementation (s) of your service to declare within invoice-api.jar within the JAR itself and then just add the JAR to the classpath and META-INF/services/com.example.invoice.InvoiceParser will find it.

However, there is one though. In order for ServiceLoader to find and instantiate services, they must be in public classes, have a public constructor without parameters, and the corresponding class must be an implementation or subclass of the service offered.

Ideally you should have the subclasses of ServiceLoader as a service instead of InvoiceParser implementations. However, to achieve this, you need to refactor your subclasses of InvoiceParserFactory in a way that gets rid of the need to have the PDF as a parameter in order to have a constructor without parameters.

There is another way of offering services in modular JARs (Java 9+) that are in the modulepath instead of the classpath, but this is a bit more complicated. The javadoc of class InvoiceParser describes this in detail.

I also talked a bit about ServiceLoader in that other answer of mine .

    
08.05.2018 / 06:28
0

You can use the Reflections library and then do this:

public class InvoiceParserFactory {

    private static final Set<Class<? extends InvoiceParser>> SUB_TYPES;

    static {
        Reflections reflections = new Reflections("com.example.meupacote");
        SUB_TYPES = reflections.getSubTypesOf(InvoiceParser.class);
    }

    public static InvoiceParser getParser(InvoicePdfReader reader) {
        String invoiceText = reader.getText();

        for (Class<? extends InvoiceParser> k : SUB_TYPES) {
            try {
                return k.getDeclaredConstructor(String.class).newInstance(invoiceText);
            } catch (Exception e) {
                // Engole e pula.
            }
        }
        throw new InvoiceException("Can't find parser.");
    }
}

At each implementation of InvoiceParser , you should put a constructor that throws an exception if the String passed is not compatible with it:

public class XxxInvoiceParser extends InvoiceParser {

    // implementação...

    public XxxInvoiceParser(String invoiceText) {
        if (!isXxx(invoiceText)) throw new IllegalArgumentException();
        // implementação...
    }

    @Override
    protected void parseSomeText() {
        // implementação...
    }

    private static boolean isXxx(String invoiceText) {
        Pattern p = Pattern.compile("xxx_regex");
        Matcher m = p.matcher(invoiceText);

        // implementação...
    }
}
    
08.05.2018 / 21:15