How to design an easily-extensible API with the si

2019-04-07 08:20发布

Sorry for the vague title; couldn't think of how to word it more clearly. Here are the highlights of the questions:

Highlights

  • Asking an API design question about the ExifTool for Java library.
  • Here is an example of what the current API looks like.
  • As a USER, the API is super-simple to use because you just pass in Enums for the image metadata you want back.
  • As a DEV, the API somewhat sucks because you cannot easily extend the base class with more Enum types to support additional metadata that may not be supported directly in the lib.
  • Simply pre-defining and supporting "all the metadata" is non-trivial.

Question

Given that setup information, what I am after is trying to find a way to pre-define the 30 or 40 most common metadata flags that people typically want from their images; right now everything is defined as an Enum, but the class is not extensible this way.

If I go the "Class-per-Metadata-flag" route, the extensibility will be simple, but the API will be a lot less friendly to use out of the box.

I will consider making v2.0 of this library Java 8+ if closures offer a really beautiful and simple solution, but otherwise I'd obviously prefer to keep it compatible with more systems (Java 6/7) than less.

Summary

My goals for the library are "simple to use and extend" - I feel I have nailed the "simple to use" aspect with the 1.x release, but the library is not easily extensible and I'd like to correct that in the 2.x series.

I have been sitting on the 2.x release for over a year waiting for inspiration to strike and it has eluded me; I am hoping someone can spot my mistake and I can move the lib forward in a really elegant way.

Thank you for the time guys!

2条回答
对你真心纯属浪费
2楼-- · 2019-04-07 08:51

Java enums are not extensible, but they can implement interfaces.

You can often get the best of both worlds by defining an interface that providers can implement, and an enum that implements it and contains commonly used instances that the users will be able to use directly:

public interface Pet {
    public String talk();
}
public enum CommonPet implements Pet {
    CAT("Meow!"),
    DOG("Woof! Woof!");

    private final String cry;

    CommonPet(String cry) {
        this.cry = cry;
    }

    @Override
    public String talk() {
        return cry;
    }
}

The API that used to accept instances of the original enum should now take any instance of the interface.

Users can provide their own implementations using the same pattern:

public enum UncommonPet implements Pet {
    LION;

    @Override
    public String talk() {
        return "Roar!";
    }
}

Finally, there is no requirement that all implementations should be enums, so in more complex cases the user can choose to implement the interface as a full-fledged class:

public class Parrot implements Pet {
    private String phrase = "Pieces of eight!";

    @Override
    public String talk() {
        return phrase;
    }

    public void teach(String phrase) {
        this.phrase = phrase;
    }
}
查看更多
兄弟一词,经得起流年.
3楼-- · 2019-04-07 08:56

Here's a couple ideas:

  1. Create an new interface to represent a tag and retrofit your enum to implement it. Or maybe call the new interface Tag, and rename the enum to Tags or CommonTags. Then create another class that implements the interface, allowing for less common tags.

    Benefit of this approach is that it doesn't require a lot of changes on your end, but it breaks source compatibility with old versions of the library, and is a little more complicated.

    public interface Tag {
        String getName();
        Class<?> getType();
    }
    
    public enum Tags implements Tag {
        // mostly same as before
    }
    
    public class OtherTag implements Tag {
        private String name;
        private Class<?> type;
        public OtherTag(String name, Class<?> type) {
            this.name = name;
            this.type = type;
        }
        @Override
        public String getName() {
            return name;
        }
        @Override
        public Class<?> getType() {
            return type;
        }
    }
    

    In your getImageMeta method, instead of just calling Tag.forName, you'd have to construct a map of tag names to Tag objects before hand:

    ...
    Map<String, Tag> tagMap = new HashMap<String, Tag>();
    for (Tag tag: tags)
        tagMap.put(tag.getName(), tag);
    
    ...
    
    while ((line = streams.reader.readLine()) != null) {
        String[] pair = TAG_VALUE_PATTERN.split(line);
    
            if (pair != null && pair.length == 2) {
                // Determine the tag represented by this value.
                Tag tag = tagMap.get(pair[0]);
    ...
    
  2. Or convert the Tag enum to a simple class with lots of public static final fields:

    public class Tag {
        public static final Tag ISO = new Tag("ISO", Integer.class);
        public static final Tag APERTURE = new Tag("ApertureValue", Double.class);
        public static final Tag WHITE_BALANCE = new Tag("WhiteBalance", Integer.class);
        ...
    
        // almost everything else the same
        // Tag constructor should now be public
    }
    

    This will work except for the part where TAG_LOOKUP_MAP is initialized. There, you either need to list all the tags again or maybe use reflection to get all the fields on Tag:

    private static final Map<String, Tag> TAG_LOOKUP_MAP;
    static {
        for (Field field: Tag.class.getFields()) {
            if (Modifier.isPublic(field.getModifiers()) &&
                    Modifier.isStatic(field.getModifiers()) &&
                    Modifier.isFinal(field.getModifiers()) {
                Tag tag = (Tag) field.get(null);
                TAG_LOOKUP_MAP.put(tag.getName(), tag);
            }
        }
    }
    

    However, you may not even need to do this, since you still need to make the same change to getImageMeta I mentioned earlier, so your code won't actually need to call Tag.forName. Users of the library might have been using it though.

    Upside to this approach is that it maintains source compatibility, looks mostly the same from the outside (users still use Tag.ISO, for example), and users can create new tags by simply doing new Tag("ColorMode", Integer.class). Downside is it still breaks binary compatibility and it's a little bit messier to maintain on the development side.

I'm sure there are other options, but there's two that occurred to me.

查看更多
登录 后发表回答