java的动态性编程(三)——如何运用反射编程
一开始,在真正进入编写实现代码的工作之前,我将首先定义要解决的问题,然后为这个库设计一个接口。不过,在开发这个库的时候,我并不是按照上述步骤进行的――我先是尽力简化一群有公共代码基础的应用程序中的现有代码,然后使之通用化。本文中使用的“定义-设计-构建”这种线性序列比起完完整整地描述开发过程要简练得多,而且,按照这种方式来组织对开发过程的描述,我可以修正我原先的一些假设,并清理掉这个库的代码中一些不必要的方面。您完全有希望发现将上述方式作为开发您自己的基于反射的应用程序时所使用的模型十分管用。
定义问题
我曾经写过许多使用命令行参数的Java应用程序。一开始,大多数应用程序都很小,但最后有些应用程序却变得大到出乎我的意料。下面是我观察到的这些应用程序的变大过程的标准模式:
1、一开始只有一个或者两个参数,按照某种特定的顺序排列。
2、考虑到这个应用程序有更多的事情要做,于是添加更多的参数。
3、厌倦了每次都输入所有的参数,于是让一些参数成为可选的参数,让这些参数带有默认的值。
4、忘记了参数的顺序,于是修改代码,允许参数以任何顺序排列。
5、将这个应用程序交给其他感兴趣的人。但是他们并不知道这些参数各自代表什么,于是又为这些参数添加更完善的错误检查和“帮助”描述。
当我进入到第5步的时候,我通常会后悔没有将整个过程都放在第一步来做。好在我很快就会忘记后面的那些阶段,不到一两个星期,我又会考虑另外一个简单的小命令行程序,我想拥有这个应用程序。有了这个想法之后,上述整个恶心的循环过程的重现只是时间的问题。
有一些库可以用来帮助进行命令行参数处理。不过,在本文中我会忽略掉这些库,而是自己动手创建一个库。这不是(或者不仅仅是)因为我有着“非此处发明(not invented here)”的态度(即不愿意用外人发明的东西,译者注),而是因为想拿参数处理作为一个实例。这样一来,反射的强项和弱点便正好体现了对参数处理库的需求。特别地,参数处理库:
需要一个灵活的接口,用以支持各种应用程序。对于每个应用程序,都必须易于配置。不要求顶级的性能,因为参数只需处理一次。不存在访问安全性问题,因为命令行应用程序运行的时候通常不带安全管理器。
这个库中实际的反射代码只代表整个实现的一小部分,因此我将主要关注与反射最相关的一些方面。如果您想找到有关这个库的更多内容(或许还想将它用到您自己的简单命令行应用程序中去),您可以在 参考资料部分找到指向Web站点的链接。
草拟出一份设计
应用程序访问参数数据最方便的方式或许是通过该应用程序的 main 对象的一些字段。例如,假设您正在编写一个用于生成业务计划的应用程序。您可能想使用一个 boolean 标记来控制业务计划是简要的还是冗长的,使用一个 int 作为第一年的收入,使用一个 String 作为对产品的描述。我将把这些会影响应用程序的运行的变量称作 形参(parameters),以便与命令行提供的 实参(arguments)――即形参的值区分开来。通过为这些形参使用字段,将使得在 需要形参的应用程序代码中的任何地方都可以方便地调用它们。而且,如果使用字段的话,在定义形参字段时为任意形参设置默认值也很方便,如清单1所示:
public class PlanGen { private boolean m_isConcise; // rarely used, default false private int m_initialRevenue = 1000; // thousands, default is 1M private float m_growthRate = 1.5; // default is 50% growth rate private String m_productDescription = // McD look out, here I come "eFood - (Really) Fast Food Online"; ... private int revenueForYear(int year) { return (int)(m_initialRevenue * Math.pow(m_growthRate, year-1)); } ...
java PlanGen -c -f2500 -g2.5 -n "iSue4U - Litigation at Internet Speed" plan.txt
private static final ParameterDef[] PARM_DEFS = { new BoolDef('c', "m_isConcise"), new IntDef('f', "m_initialRevenue", 10, 10000), new FloatDef('g', "m_growthRate", 1.0, 100.0), new StringDef('n', "m_productDescription")}
public class PlanGen{ private static final ParameterDef[] PARM_DEFS = { ... }; public static void main(String[] args) { // if no arguments are supplied, assume help is needed if (args.length > 0) { // process arguments directly to instance PlanGen inst = new PlanGen(); int next = ArgumentProcessor.processArgs (args, PARM_DEFS, inst); // next unused argument is output file name if (next >= args.length) { System.err.println("Missing required output file name"); System.exit(1); } File outf = new File(args[next++]); ... } else { System.out.println("\nUsage: java PlanGen " + "[-options] file\nOptions are:\n c concise plan\n" + "f first year revenue (K$)\n g growth rate\n" + "n product description"); } }}
public abstract class ParameterDef{ protected char m_char; // argument flag character protected String m_name; // parameter field name protected Field m_field; // actual parameter field protected ParameterDef(char chr, String name) { m_char = chr; m_name = name; } public char getFlag() { return m_char; } protected void bindToClass(Class clas) { try { // handle the field look up and accessibility m_field = clas.getDeclaredField(m_name); m_field.setAccessible(true); } catch (NoSuchFieldException ex) { throw new IllegalArgumentException("Field '" + m_name + "' not found in " + clas.getName()); } } public abstract void handle(ArgumentProcessor proc);}
public class ArgumentProcessor{ private Object m_targetObject; // parameter value object private int m_currentIndex; // current argument position ... public ArgumentProcessor(ParameterDef[] parms, Object target) { // bind all parameters to target class for (int i = 0; i < parms.length; i++) { parms[i].bindToClass(target.getClass()); } // save target object for later use m_targetObject = target; } public void setValue(Object value, Field field) { try { // set parameter field value using reflection field.set(m_targetObject, value); } catch (IllegalAccessException ex) { throw new IllegalArgumentException("Field " + field.getName() + " is not accessible in object of class " + m_targetObject.getClass().getName()); }} public void reportArgumentError(char flag, String text) { throw new ArgumentErrorException(text + " for argument '" + flag + "' in argument " + m_currentIndex); } public static int processArgs(String[] args, ParameterDef[] parms, Object target) { ArgumentProcessor inst = new ArgumentProcessor(parms, target); ... }}
public class IntDef extends ParameterDef{ private int m_min; // minimum allowed value private int m_max; // maximum allowed value public IntDef(char chr, String name, int min, int max) { super(chr, name); m_min = min; m_max = max; } protected void bindToClass(Class clas) { super.bindToClass(clas); Class type = m_field.getType(); if (type != Integer.class && type != Integer.TYPE) { throw new IllegalArgumentException("Field '" + m_name + "'in " + clas.getName() + " is not of type int"); } } public void handle(ArgumentProcessor proc) { // set up for validating boolean minus = false; boolean digits = false; int value = 0; // convert number supplied in argument list to 'value' ... // make sure we have a valid value value = minus ? -value : value; if (!digits) { proc.reportArgumentError(m_char, "Missing value"); } else if (value < m_min || value > m_max) { proc.reportArgumentError(m_char, "Value out of range"); } else { proc.setValue(new Integer(value), m_field); } }}