1

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

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

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

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

在解析文件的这两个特定行时,我需要分别创建类Cat和实例Dog。实际上,有大量不同的对象类型被描述,有时参数数量的不同变化,如果值不存在以明确说明它们,则通常假定默认值(这意味着通常适合使用构建器创建对象时的模式,或者某些类有多个构造函数)。

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

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

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

我目前拥有的解析器有很多switch/caseif/else到处都可以处理这个问题,虽然它可以工作,但有相当多的对象类型,它变得有点笨拙。

有人可以建议一种替代、更清洁和更“面向对象”的模式将标记列表与适当的方法调用匹配吗?

4

2 回答 2

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,根据行开头的字符串来确定实际调用的方法。要么是这样,要么是引入 a Map<String,Method>,它为您提供了运行时切换工具,其中映射会将字符串映射到您可以调用的正确方法invoke(Java 的一部分)。我更喜欢保留switch在没有大量案例的地方,并Map为更复杂的运行时场景保留 Java。

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

于 2012-10-17T12:09:04.510 回答
1

答案就在问题中;你想要一个策略,基本上是一个映射,其中键是,例如“猫”,值是一个实例:

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

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

于 2012-10-17T12:25:49.327 回答