6

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

Sandrew Cheru
  • 121
  • 1
  • 9
  • 1
    You can use **reflection** or you have to list all the known or "wanted to be available" classes somewhere, e.g. a config file... E.g. `org.reflections` is a nice lightweight lib for scanning the class path. – dedek Sep 07 '17 at 12:32
  • 1
    List the class names in a simple text file that they can maintain themselves, move the factory construction to a static constructor and use `Class.forname` (https://stackoverflow.com/q/8100376/13075) on all the entries of the file in a factory initialization method. – Henrik Aasted Sørensen Sep 07 '17 at 12:32
  • 1
    Alternatively: Introduce an annotation that they apply to their classes, and scan the classpath for that in the factory. – Henrik Aasted Sørensen Sep 07 '17 at 12:33
  • Just don't end up with own Spring realization) – Izbassar Tolegen Sep 07 '17 at 12:34
  • Why can't they just register the extractors before your code runs? – Bill O'Neil Sep 07 '17 at 12:55

3 Answers3

2

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.
    }
}
dedek
  • 7,981
  • 3
  • 38
  • 68
BeUndead
  • 3,463
  • 2
  • 17
  • 21
  • Wau, didn't know that such a thing is available in plain java! – dedek Sep 07 '17 at 13:23
  • If you combine that by asking whether some HostExtractor deals with `host`, either be interface method, or simply by turning `host` in a name part, your app only needs to know one SPI interface. – Joop Eggen Sep 07 '17 at 13:37
  • But you have to list all the classes in the `META-INF/services/your.package.HostExtractor` file... – dedek Sep 07 '17 at 13:39
  • It depends on how the project is configured. If your project combines multiple jars, each one is free to define a subset of them in that jar's local resources. This is typically an easier approach than trying to list them all in the one file (easier to swap out/track what comes from where). – BeUndead Sep 07 '17 at 14:36
1

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.

dedek
  • 7,981
  • 3
  • 38
  • 68
Roee Gavirel
  • 18,955
  • 12
  • 67
  • 94
  • Can you maybe add some example code, how to *"ask" for all classes that implements this interface* in spring...? – dedek Sep 07 '17 at 13:46
1

I would use the Reflections library to locate the parsers. They all appear to derive from the HostExtractor class, so use the library to locate all subtypes:

Reflections reflections = new Reflections("base.package");    
Set<Class<? extends HostExtractor>> extractorTypes =
    reflections.getSubTypesOf(HostExtractor.class);

Use the results to create the instances in your factory:

for (Class<? extends HostExtractor> c : extractorTypes) {
    HostExtractor he = c.newInstance();
    EXTRACTOR_MAPPING.put(he.getHostName(), he);
}

I made up the getHostName method, but it should be trivial to add to the HostExtractor base class.

Henrik Aasted Sørensen
  • 6,966
  • 11
  • 51
  • 60