Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically invoke the correct implementation in a factory

I have a library which parse URLs and extract some data. There is one class per URL. To know which class should handle the URL provided by the user, I have the code below.

public class HostExtractorFactory {

private HostExtractorFactory() {
}

public static HostExtractor getHostExtractor(URL url)
        throws URLNotSupportedException {
    String host = url.getHost();

    switch (host) {
    case HostExtractorABC.HOST_NAME:
        return HostExtractorAbc.getInstance();
    case HostExtractorDEF.HOST_NAME:
        return HostExtractorDef.getInstance();
    case HostExtractorGHI.HOST_NAME:
        return HostExtractorGhi.getInstance();
    default:
        throw new URLNotSupportedException(
                "The url provided does not have a corresponding HostExtractor: ["
                        + host + "]");
    }
}

}

The problem is users are requesting more URL to be parsed, which means my switch statement is growing. Every time someone comes up with a parser, I have to modify my code to include it.

To end this, I've decided to create a map and expose it to them, so that when their class is written, they can register themselves to the factory, by providing the host name, and the extractor to the factory. Below is the factory with this idea implemented.

public class HostExtractorFactory {

private static final Map<String, HostExtractor> EXTRACTOR_MAPPING = new HashMap<>();

private HostExtractorFactory() {
}

public static HostExtractor getHostExtractor(URL url)
        throws URLNotSupportedException {
    String host = url.getHost();

    if(EXTRACTOR_MAPPING.containsKey(host)) {
        return EXTRACTOR_MAPPING.get(host);
    } else {
        throw new URLNotSupportedException(
                "The url provided does not have a corresponding HostExtractor: ["
                        + host + "]");
    }
}

public static void register(String hostname, HostExtractor extractor) {
    if(StringUtils.isBlank(hostname) == false && extractor != null) {
        EXTRACTOR_MAPPING.put(hostname, extractor);
    }
}

}

And the user would use it that way:

public class HostExtractorABC extends HostExtractor {

public final static String HOST_NAME = "www.abc.com";

private static class HostPageExtractorLoader {
    private static final HostExtractorABC INSTANCE = new HostExtractorABC();
}

private HostExtractorABC() {
    if (HostPageExtractorLoader.INSTANCE != null) {
        throw new IllegalStateException("Already instantiated");
    }

    HostExtractorFactory.register(HOST_NAME, this);
}

public static HostExtractorABC getInstance() {
    return HostPageExtractorLoader.INSTANCE;
}
...

}

I was patting my own back when I realized this will never work: the user classes are not loaded when I receive the URL, only the factory, which means their constructor never runs, and the map is always empty. So I am back to the drawing board, but would like some ideas around getting this to work or another approach to get rid of this pesky switch statement.

S

like image 962
Sandrew Cheru Avatar asked Sep 07 '17 12:09

Sandrew Cheru


2 Answers

Another option is to use the Service Loader approach.

Having your implementers add something like the following in ./resources/META-INF/services/your.package.HostExtractor:

their.package1.HostExtractorABC
their.package2.HostExtractorDEF
their.package3.HostExtractorGHI
...

Then in your code, you can have something like:

HostExtractorFactory() {
    final ServiceLoader<HostExtractor> loader
            = ServiceLoader.load(your.package.HostExtractor.class);

    for (final HostExtractor registeredExtractor : loader) {
        // TODO - Perform pre-processing which is required.
        // Add to Map?  Extract some information and store?  Etc.
    }
}
like image 186
BeUndead Avatar answered Oct 23 '22 13:10

BeUndead


I would advice for you to learn about dependency injection (I love spring implementation). You will then be able to write an interface like

public interface HostExtractorHandler {
    public String getName();
    public HostExtractor getInstance();
}

Than your code can "ask" for all classes that implements this interface, you then would be able to build your map in the initialization phase of your class.

like image 40
Roee Gavirel Avatar answered Oct 23 '22 14:10

Roee Gavirel