如何使用Enums的简单性设计易于扩展的API?

抱歉标题模糊; 我想不出如何更清楚地说出来。 以下是问题的重点:

强调

  • 询问有关ExifTool for Java库的API设计问题。
  • 以下是当前API 的示例 。
  • 作为USER ,API使用起来非常简单,因为您只需传入要返回的图像元数据的枚举。
  • 作为DEV ,API有点糟糕,因为您无法使用更多Enum类型轻松扩展基类以支持lib中可能不直接支持的其他元数据。
  • 简单地预定义和支持“ 所有元数据 ”并非易事 。

鉴于设置信息,我所追求的是试图找到一种方法来预先定义人们通常想要从他们的图像中获得的30或40个最常见的元数据标记; 现在一切都被定义为枚举 ,但这种类不可扩展。

如果我使用“Class-per-Metadata-flag”路由,那么可扩展性将很简单,但是开箱即用的API将不那么友好。

如果闭包提供了一个非常漂亮和简单的解决方案,我会考虑制作这个库Java 8+的v2.0,但是否则我显然更愿意让它与更多系统(Java 6/7)兼容而不是更少。

概要

我对该库的目标是“易于使用和扩展” – 我觉得我已经在1.x版本中使用了“简单易用”的方面,但是该库不易扩展,我想在2.x系列。

我已经坐在2.x版本上超过一年等待灵感罢工,它已经躲过了我; 我希望有人可以发现我的错误,我可以以一种非常优雅的方式向前移动lib。

谢谢你们的时间!

Java枚举不可扩展,但它们可以实现接口。

通过定义提供程序可以实现的接口,以及实现它的枚举并包含用户可以直接使用的常用实例,您通常可以获得两全其美:

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; } } 

用于接受原始枚举实例的API现在应该接受任何接口实例。

用户可以使用相同的模式提供自己的实现:

 public enum UncommonPet implements Pet { LION; @Override public String talk() { return "Roar!"; } } 

最后,并不要求所有实现都应该是枚举,因此在更复杂的情况下,用户可以选择将接口实现为完整的类:

 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; } } 

这里有几个想法:

  1. 创建一个新界面来表示标签并改进您的枚举以实现它。 或者可以调用新接口Tag ,并将枚举重命名为TagsCommonTags 。 然后创建另一个实现接口的类,允许不太常见的标记。

    这种方法的好处在于它不需要对您进行大量更改,但它会破坏与旧版本库的源兼容性,并且稍微复杂一些。

     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; } } 

    在你的getImageMeta方法中,你不必只是调用Tag.forName ,而是必须在之前为Tag对象构建一个标记名称的地图:

     ... Map tagMap = new HashMap(); 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. 或者将Tag枚举转换为具有大量public static final字段的简单类:

     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 } 

    除了初始化TAG_LOOKUP_MAP的部分之外,这将起作用。 在那里,您需要再次列出所有标签,或者使用reflection来获取Tag上的所有字段:

     private static final Map 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); } } } 

    但是,您甚至可能不需要这样做,因为您仍然需要对我之前提到的getImageMeta进行相同的更改,因此您的代码实际上不需要调用Tag.forName 。 该库的用户可能一直在使用它。

    这种方法的Tag.ISO是它保持了源兼容性,从外部看起来大致相同(例如,用户仍然使用Tag.ISO ),用户可以通过简单地创建新标签new Tag("ColorMode", Integer.class)来创建新标签。 。 缺点是它仍然打破了二进制兼容性,并且在开发方面维护起来有点麻烦。

我确定还有其他选择,但我发现了两个。