使用Swift使访客模式过时

访客模式是来自臭名昭著的“四人帮”的设计模式,我曾多次使用它来解决一些棘手的问题。 它主要解决了问题,但是我不能说我曾经喜欢访客设计模式。

它产生了许多自己的问题,您可以通过创建更复杂的访问者模式来解决。 这是我与模式有关的问题之一。 它很快变得复杂,我讨厌编写复杂的代码。

让我们看看一些情况,这些问题是我在C ++中使用访问者模式解决的,以及在使用Swift编写相同代码时如何完全避免使用访问者。

谓词编辑器

谓词是返回布尔值的运算符或函数。 以下是将两个表达式与比较运算符组合在一起的谓词:

  a + 3> 4-2b 

在内存中,我们可以将其表示为对象树。 这里的每种颜色代表对象或类的不同类别。

在Mac OS X上,有许多应用程序示例支持通过GUI编辑器创建谓词。 一个示例是iTunes中的智能播放列表。

想象我们想用C ++创建这样的编辑器。 因此,我们需要一个GUI来允许我们组成谓词。 一旦获得谓词,我们可能希望过滤MP3歌曲列表,以查找可以归类为90年代音乐的歌曲。

让我们考虑一些简单的事情。 我们如何表达25首播放最多的歌曲的谓词? 以下是iTunes的一种方法。

因此,让我们将其范围缩小到谓词:

 播放次数> 0 

在C ++代码中,我们可以表示为:

 表达式* plays = AttributeExpression(“ plays”); 
表达式*零= ConstantExpression(0);
谓词* pred = LargerThanPredicate(plays,zero);
playsongs = allsongs.filter(pred);

因此,要使用谓词,我们要处理Expression和Predicate对象。 但是,要使用GUI构建谓词,我们需要不同的对象来表示各个行的GUI。

我们可以有一个PredEditorRow类来代表每一行的GUI。 但是,有许多不同种类的行。 谓词可以任意嵌套,因此我们需要复合行。

让我们重新访问我们的第一个音乐智能列表。 90年代的音乐谓词看起来像这样:

  1990 <年<1999 &&(mediakind ==“音乐” || mediakind ==“音乐视频”) 

因此,年份范围可以由PredEditorLeafRow表示,并且“媒体种类”行必须具有父对象,该父对象可以是PredEditorCompositeRow。

因此,我们用复合设计模式表示谓词编辑器。 该行的最右边部分将表示某种形式的表达式,我们正在将该属性与之进行比较。 这可以简单地表示为文本字段中的数字或字符串。 但这也可以是合法值的列表,调色板中的颜色等。因此,为了处理所需的灵活性,我们可以使用某种策略模式。 在我们的图中,我们将其表示为ExpressionEditor。 因此,想象每一行都指向一个表达式编辑器,可以将其替换为另一行。 细节并不重要,因为我们主要对访客模式的实现感兴趣。 这只是一个大概的想法。

游客

面临的挑战是提供一个表示谓词的对象树,如何将其转换为谓词编辑器的GUI元素树。 如果我们例如从磁盘上读取谓词并想要显示它的编辑器,则会出现此问题。

这样的对象树将不是同质的,因为它不仅将由表达式和谓词对象组成,后者又将具有自己的子类。 表达式可以细分为例如属性表达式和常量。 由于它不是同质的,因此没有简单的方法可以以通用的方式遍历这种结构。

这就是为什么访客有帮助的原因。 我们可以设计表达式和谓词类以接受PredVisitor类型的访问者。 然后,PredEditorVisitor子类可以从谓词构造GUI,而例如PredJSONVisitor子类可以以JSON格式将谓词存储到磁盘。

在构建GUI时,PredEditorVisitor将需要两个堆栈来跟踪父节点。 对于谓词,我们首先要获得顶级父节点,因此我们必须从上至下进行构建。 一个复杂的问题是存在不同种类的父节点。 表达式节点具有比较谓词作为父代。 虽然比较谓词必须具有AND,OR,XOR等复合谓词作为父节点。 因此,我们维护了两个不同的堆栈CompositeStack和leafStack:

  PredEditorVisitor类:PredVisitor { 
虚拟虚函数访问(AttributeExpression * exp);
虚拟虚假访问(ConstantExpression * exp);
虚拟无效访问(LargerThanPredicate * pred);
私人的:
Stack CompositeStack;
Stack leafStack;
元数据* metaData; //描述我们正在过滤的数据
}

将子节点添加到哪个节点很容易,因为每次访问一个节点时,我们都会知道它是表达式,比较还是复合谓词。 这是访问复合谓词节点时的样子:

 虚空 
PredEditorVisitor :: Visit(CompoundPredicate * pred){
PredEditorCompositeRow * row =新的PredEditorCompositeRow(pred);
如果(!compositeStack.empty())
CompositeStack.top()-> addChild(row);

CompositeStack.push_back(row);
对于(谓词* child:pred){
pred-> Accept(this);
}
CompositeStack.pop_back();
}

当访问比较谓词节点时,我们利用叶子堆栈:

虚空 
PredEditorVisitor :: Visit(ComparisonPredicate * pred){
PredEditorLeafRow * row =新的PredEditorLeafRow(metaData-> attributes,OperationsForAttribute);

如果(!compositeStack.empty())
CompositeStack.top()-> addChild(row);

leafStack.push_back(行)
pred-> leftExpression-> accept(this)
row-> selectOperator(pred-> operator)
pred-> rightExpression-> accept(this)

leaf_stack_.pop_back();
}

如果我们做的只是添加新的访问者,那么这种解决方案就可以很好地工作。 但是,如果添加了新的谓词和表达式,那么将变得难以管理,因为您需要更新每个访问者。

斯威夫特的解决方案

在这种情况下,我们有访问者的原因是我们无法预先知道我们可能要为谓词创建哪种GUI或将其序列化为哪种格式。 我们也想分开关注点,不要将GUI代码与模型代码混在一起。

使用Swift,您可以使用类扩展来解决此类问题。 类扩展允许我们向现有的类添加功能。 因此,假设我们从另一家公司购买了谓词和表达式对象,我们仍然可以通过扩展为其添加功能。 现有的谓词和表达式代码将不依赖于这些扩展,因此在该方向上没有紧密的耦合。

谓词编辑器

让我们从访问者解决方案中回顾一些案例。 首先,我们研究了如何使用访问者从复合谓词创建嵌套行。

使用Swift,我们只需向每个谓词和表达式子类添加一种用于创建GUI行的方法。 首先,我们必须向表达式和谓词添加默认方法,以防我们添加新的子类而忘记实现我们的GUI行创建代码。 我们始终需要访问要为其创建谓词和表达式的数据的描述,因为例如可用于字符串数据的比较运算符对于例如数字而言是不同的。 因此,我们总是必须提供某种MetaData对象。

 扩展表达式{ 
func createExpressionEditor(meta:MetaData)-> ExpressionEditor {
var attr = self.attribute(meta)
返回ExpressionEditor.editorFor(attr,attr.defaultOperator)
}
func属性(元:MetaData)->属性{
返回meta.defaultAttribute
}
}

扩展谓词{
func createPredEditorRow(meta:MetaData)-> PredEditorRow? {
返回零
}
}

然后,对于每个子类,我们实现用于为该特定种类的谓词或表达式创建GUI行的代码。 例如,让我们看一下ComparisonPredicate:

 扩展程序ComparePredicate { 
func createPredEditorRow(meta:MetaData)-> PredEditorRow? {
让行= PredEditorLeafRow(meta.attributes,meta.operationsForAttribute)
row.selectAttribute(leftExpression.attribute(meta))
row.selectOperator(self.operator(meta))
row.expressionEditor = rightExpression.createExpressionEditor(meta)
返回行
}
}

获取细节并不重要,但是让我们大致了解一下这里发生的情况。 我们为单行创建GUI。 我们需要配置该行,以便在左侧显示一个下拉列表,其中也包含将要应用谓词的对象的可能属性。 例如,可能是歌曲,电子邮件或图像。 如果我们要过滤电子邮件,则meta将包含有关电子邮件对象的元数据,例如电子邮件可能具有的属性:

  • 发件人
  • 接受者
  • 学科
  • 优先

在确定哪些运算符可用于比较时,选择这些属性中的哪个属性很重要。

重要的是了解如何轻松添加GUI特定代码作为对非GUI类的扩展。 这不会中断关注点的分离,因为相同的类可以在以后轻松重用而无需任何GUI依赖。 扩展不能添加成员变量或更改现有方法的行为。

我们可以通过在它们的共享基类中实现扩展来处理所有复合谓词,例如AND,OR,XOR。

 扩展CompoundPredicate { 
func createPredEditorRow(meta:MetaData)-> PredEditorRow? {
让行= PredEditorCompositeRow(pred)
对于自己的孩子
row.addChild(childpred.createPredEditorRow(meta))
}
}
}

您可以看到在遍历子谓词时我们不需要任何类型的转换,因为编译器知道每个谓词都有一个createPredEditorRow,因为我们将其添加为谓词协议的扩展。

JSON格式

与重新创建GUI相比,实现JSON序列化要容易得多。 扩展允许我们将序列化代码与其余谓词代码放在完全独立的文件中。

和以前一样,我们需要在基类或协议中创建JSON创建代码,以便编译器知道createJSONRepresentation()可用于所有子类。

 扩展表达式{ 
func createJSONRepresentation()->可以吗? {
返回零
}
}

扩展谓词{
func createJSONRepresentation()->可以吗?
返回零
}
}
扩展名AttributeExpression {
func createJSONRepresentation()->可以吗? {
return [“ type”:“ AttributeExpression”,“ name”,self.name]
}
扩展名ConstantExpression {
func createJSONRepresentation()->可以吗? {
return [“ type”:“ ConstantExpression”,“ value”,self.value]
}
扩展程序ComparePredicate {
func createJSONRepresentation()->可以吗? {
return [“ type”:“ ComparisonPredicate”,
“运算符”:self.comparisonOperator,
“ leftExpression”:self.leftExpression.createJSONRepresentation(),
“ rightExpression”:self.rightExpression.createJSONRepresentation(),
]
}

结论

不可否认,谓词编辑器示例可能并不容易理解。 但这凸显了另一个事实,即解决这类问题非常复杂,当您需要依赖诸如访客模式之类的模式时,它们甚至变得更加复杂。 由于Swift支持类扩展,函数是一流的等,因此许多常见的设计模式消失或变得无关紧要。 这是一件好事。 设计模式从根本上讲是一种语言气味。 它们表示需要发明设计模式的语言所缺乏的功能。