【问题标题】:How do I use the object oriented design paradigm when building a lexer/scanner?在构建词法分析器/扫描器时如何使用面向对象的设计范例?
【发布时间】:2018-07-06 17:20:56
【问题描述】:
我正在尝试设计一个小型正则表达式引擎(在 Smalltalk 中)来帮助提高我的面向对象技能。
我已经确定了一些其他语言(以及 Smalltalk 原生语言)的正则表达式引擎,它们似乎都有一个单一的“Lexer”或“Scanner”类。这让我很困惑,因为我认为词法分析器是一个单一的函数,它应该将模式作为输入(可能还有一个定义标记类型的“语法”对象),并返回一个标记流。我无法弄清楚接口将具有哪些额外功能,以及对象需要保持哪些额外状态。
如何将其分解为面向对象的设计?
我应该补充一点,当我阅读源代码时,我似乎经常看到这一点:一个在其名称末尾添加了动词+“er”的类。这似乎违背了“清洁代码”和“代码完整”等书籍所教授的“正确的面向对象设计”。
【问题讨论】:
标签:
oop
architecture
smalltalk
lexer
【解决方案1】:
这是一个非常广泛的问题,因此我们只能为您提供同样广泛的答案。这是我的。
在考虑面向对象时,我们试图在数据和行为之间建立明确的区别。一个对象两者兼有,一般来说,行为与数据越独立越好。
这些基本指导原则并不总能让设计看起来自然。原因是有时我们倾向于将与某些数据相关的行为附加到相同的数据上。这可能会隐藏应该拥有这种行为的实际对象。
这种现象的典型案例是算法。我们有一些数据:算法输入和一些行为:算法输出,并且认为这种行为应该附加到数据中。因此,我们很难将其实现为上述数据的函数。
但是,在大多数情况下,这种简单化的方法是有问题的。例如,许多算法可能会产生多个输出。以两个多项式的除法为例,其中输出是商和余数。即使算法产生单个输出,也可能会发生我们想问它花了多长时间,甚至告诉它取消执行并自行停止等等。
由于这些原因(以及其他同类原因),始终建议将算法视为对象而不是函数。这种对象的实例变量通常会引用
inputs - "one or more depending on the algorithm"
outputs - "idem"
auxiliary - "for holding the algorithm internal state while running"
progress - "for recording degree of advancement"
state - "various uses"
将算法实现为对象将有助于添加以下功能:
- 重复使用具有不同输入的相同实例
- 有一个存放输出的地方
- 向用户提供反馈
- 告诉算法取消并停止执行(通过将其状态设置为
#cancel)
- 计算迭代次数并注册其他内容以进行调试
如您所见,作为对象的算法比作为函数的算法要丰富得多。这并不意味着您将不得不放弃功能方法。只需具体化算法并让您的客户端对象提供一个函数,该函数将使用该算法并返回与该上下文相关的结果。
【解决方案2】:
从 Lexer/Scanner/Parser/Compiler/... 的名称可以清楚地看出我们正在具体化一个函数。但请注意,在 Smalltalk 中,没有任何功能。只是对象和消息。
想到的明显对象当然是:
- 无论您要生成的树状结构是什么节点,
- 如果你想拥有一个中间 Lexer 的标记,
- 语法本身(更高级)
但是,字符流到树状结构的转换会在哪里发生呢?
在这种情况下,Leandro 的回答反映了一些常见的做法。为诸如错误处理、交互式展示和错误纠正等事情具体化算法是很好的,但可能会导致或多或少的单片实现必须明确处理自动机的状态,这不会那么吸引人对于函数式语言爱好者...
先自己尝试,但是一旦你取得了足够的进步,我强烈建议学习 PetitParser,在这里做个介绍https://www.lukas-renggli.ch/blog/petitparser-1。你会看到语法本身,或者更确切地说是构成语法的节点,可以作为解析器,从而分解为基本、简单、优雅和可组合的对象。