用于将一组标记与适当的方法/构造函数匹配的OO策略

时间:2012-10-17 11:55:31

标签: java parsing tokenize

这个问题并不是专门用正则表达式执行标记化,而是关于如何匹配适当类型的对象(或对象的适当构造函数)来处理标记化器的标记输出。

为了解释一下,我的目标是将包含令牌行的文本文件解析为描述数据的适当对象。我的解析器实际上已经完成,但目前是一堆switch ... case语句,我的问题的焦点是如何使用一个漂亮的OO方法重构它。

首先,这里有一个例子来说明我在做什么。想象一个包含许多条目的文本文件,如下面两个:

cat    50    100    "abc"
dog    40    "foo"  "bar"   90

在解析文件的这两个特定行时,我需要分别创建类CatDog的实例。实际上,正在描述相当多的不同对象类型,并且有时会有不同的参数数量变化,如果值不是明确表示它们的话,通常会假设默认值(这意味着通常适合使用构建器)创建对象时的模式,或者某些类有几个构造函数。)

每行的初始标记化使用我创建的Tokenizer类来完成,该类使用匹配每种类型的可能标记的正则表达式组(整数,字符串以及与此相关的一些其他特殊标记类型)应用程序)以及PatternMatcher。此tokenizer类的最终结果是,对于它解析的每一行,它提供了Token个对象的列表,其中每个Token都有一个.type属性(指定整数,字符串,等)以及原始价值属性。

对于解析的每一行,我必须:

  • switch ... case关于对象类型(第一个标记);
  • switch关于参数的数量并选择合适的构造函数 对于那个数量的论点;
  • 检查每个标记类型是否适合构造对象所需的参数类型;
  • 如果参数类型的数量或组合不适合所调用的对象类型,则记录错误。

我目前拥有的解析器有很多switch / caseif / else来处理这个问题,尽管它有效,但是相当多的对象类型变得有点笨拙。

有人可以建议一种替代的,更清晰的,更“OO”的模式匹配令牌列表的方式到适当的方法调用吗?

2 个答案:

答案 0 :(得分:1)

我做了类似的事情,我将解析器与代码发射器分离,除了解析本身之外,我还考虑其他任何东西。我所做的是引入一个接口,解析器在它认为找到语句或类似的程序元素时使用该接口来调用方法。在您的情况下,这些可能是您在问题中的示例中显示的单独行。因此,只要您解析了一行,就可以在接口上调用一个方法,其实现将负责其余的工作。这样你就可以将程序生成与解析隔离开来,两者都可以自己完成(至少是解析器,因为程序生成将实现解析器将使用的接口)。一些代码来说明我的思路:

interface CodeGenerator
{
     void onParseCat(int a, int b, String c); ///As per your line starting with "cat..."
     void onParseDog(int a, String b, String c, int d); /// In same manner
}

class Parser
{
    final CodeGenerator cg;

    Parser(CodeGenerator cg)
    {
        this.cg = cg;
    }

    void parseCat() /// When you already know that the sequence of tokens matches a "cat" line
    {
         /// ...

         cg.onParseCat(/* variable values you have obtained during parsing/tokenizing */);
    }
}

这为您提供了几个优点,其中一个优点是您不需要复杂的switch逻辑,因为您已经确定了语句/表达式/元素的类型并调用了正确的方法。您甚至可以在onParse接口中使用类似CodeGenerator的内容,如果您想要始终使用相同的方法,则依赖于Java方法覆盖。还要记住,您可以在运行时使用Java查询方法,这可以帮助您进一步删除switch逻辑。

getClass().getMethod("onParse", Integer.class, Integer.class, String.class).invoke(this, catStmt, a, b, c);

请注意,上面使用Integer类而不是基本类型int,并且您的方法必须根据参数类型和计数覆盖 - 如果您有两个不同的语句使用相同的参数序列,上述情况可能会失败,因为至少有两种方法具有相同的签名。这当然是Java(以及许多其他语言)中方法覆盖的限制。

无论如何,你有几种方法可以达到你想要的效果。避免switch的关键是实现某种形式的虚方法调用,依赖内置的虚方法调用工具,或者使用静态绑定为特定的程序元素类型调用特定的方法。

当然,您至少需要一个switch语句,您可以根据行开头的字符串确定实际调用的方法。它或者引入一个Map<String,Method>,它为您提供了一个运行时切换工具,其中地图将字符串映射到您可以调用invoke(Java的一部分)的正确方法。我更喜欢将switch保留在没有大量案例的地方,并为更复杂的运行时方案保留Java Map

但是既然你谈到“相当多的对象类型”,我可以建议你引入运行时映射并确实使用Map类。这取决于您的语言有多复杂,以及开始您的行的字符串是关键字,还是更大的字符串中的字符串。

答案 1 :(得分:1)

答案在于问题;你想要一个策略,基本上是一个密钥所在的地图,例如“cat”和一个实例的值:

final class CatCreator implements Creator {
    final Argument<Integer> length = intArgument("length");
    final Argument<Integer> width = intArgument("width");
    final Argument<String> name = stringArgument("length");

    public List<Argument<?>> arguments() {
        return asList(length, width, name);
    }

    public Cat create(Map<Argument<?>, String> arguments) {
        return new Cat(length.get(arguments), width.get(arguments), name.get(arguments));
    }
}

支持您可以在各种对象类型之间重用的代码:

abstract class Argument<T> {
    abstract T get(Map<Argument<?>, String> arguments);
    private Argument() {
    }

    static Argument<Integer> intArgument(String name) {
        return new Argument<Integer>() {
            Integer get(Map<Argument<?>, String> arguments) {
                return Integer.parseInt(arguments.get(this));
            }
        });
    }

    static Argument<String> stringArgument(String name) {
        return new Argument<String>() {
            String get(Map<Argument<?>, String> arguments) {
                return arguments.get(this);
            }
        });
    }
}

我确信有人会发布一个需要较少代码但使用反射的版本。选择其中之一,但要记住编程错误的额外可能性,使其通过反射进行编译。