在两周前的文章中,我们使用面向对象编程解决了问题:我们使用对象对问题空间进行建模。 为了使对象与另一个对象进行通信,可以使用dispatch()
方法。
这是《编程风格练习》重点系列的第 9 个帖子。其他帖子包括:
- 以编程风格介绍练习
- 以编程风格进行练习,将内容堆叠起来
- 编程风格的练习,Kwisatz Haderach风格
- 编程风格的练习,递归
- 具有高阶功能的编程风格的练习
- 以编程风格进行练习
- 以编程风格进行练习,回到面向对象的编程
- 编程风格的练习:地图也是对象
- 编程风格的练习:事件驱动的编程 (本文)
- 编程风格的练习和事件总线
- 反思编程风格的练习
- 面向方面的编程风格的练习
- 编程风格的练习:FP&I / O
- 关系数据库风格的练习
- 编程风格的练习:电子表格
- 并发编程风格的练习
- 编程风格的练习:在线程之间共享数据
- 使用Hazelcast以编程风格进行练习
- MapReduce样式的练习
- 编程风格的练习总结
事件驱动编程
还记得网络无处不在吗? 图形用户界面已经是一回事。 处理用户交互的一种很好且广泛的方法是-现在仍然是-事件驱动编程:它已被广泛用作观察者设计模式。
![观察者设计模式类图](https://i-blog.csdnimg.cn/blog_migrate/aff68f01c332c65ca9c040f9eacb1df0.png)
观察者是对dispatch()
方法的可行替代方案。 主要区别在于,通过发送消息,没有返回值。
建模解决方案
最终的课程模型如下所示:
![解决方案类图](https://i-blog.csdnimg.cn/blog_migrate/2cdff998e926ae9a45950372d57b3d48.png)
该模型以及下一个模型均来自原始的Python解决方案。 对于Kotlin,我刚刚添加了类型。
顺序图是:
![解决方案顺序图](https://i-blog.csdnimg.cn/blog_migrate/42ebafc816c839859d853e708ee87680.png)
与以前的设计最重要的区别是:不再有dispatch()
方法。 结果,也没有返回值,但是在图的末尾。
请注意,最初的设计必须不惜一切没有返回值! 这样做的原因是它只打印了单词frequency。 因此,不可能轻松进行测试。 因为我的要求之一就是确保实现正确,所以我更改了设计,以实际返回字频图作为值。
管理事件处理程序
事件处理程序只是一个高阶函数,可以像其他任何类型一样传递该函数。
在上述设计中,类将其方法注册为其他类的高阶函数, 例如 ,在WordFrequencyCounter
的init
块中:
-
dataStorage.registerForWordEvents { incrementCount(it) }
-
wfApp.registerForEndEvents { getTop25() }
关于事件处理程序,有几个问题要解决:
-
定购
-
注册事件处理程序很容易:只需提供具有正确签名的函数,然后就可以使用。 但是,事件处理程序是由事件触发的,事件的顺序可能不同于需要处理事件的顺序。 请注意,尽管对于同步消息传递而言并非如此(在这里就是这种情况),但与直接API调用相比,仍然很难推理。
为了从订购中受益,可以将事件处理程序存储在不同的“存储桶”中。 存储桶可以存储不需要订购的处理程序。 到那时,可以从这些存储桶中调用处理程序,从“存储桶1”开始,然后是“存储桶2”,依此类推。在上面的设计中,有3个存储桶:一个用于初始化处理程序,一个用于工作处理程序,以及
储存
-
在Python示例中,将处理程序存储在不同的存储桶中只是按照它们应有的顺序调用它们。 在Kotlin中,还需要考虑类型:
描述 类型 Initialization event handlers consume something
(String) → Unit
Work handlers run by changing their internal state. Neither input nor output is necessary.
() → Unit
The single end handler needs to provide the word frequencies
() → Map<String, Int>
对于当前的问题,lambda类型彼此兼容。
在现实世界中,情况可能并非如此,因为会有许多处理程序。 因此,签名将需要更加通用,并且必须进行强制转换。
调用中
-
仅仅存储处理程序并不是终点:在某些时候,它们需要被调用。 如上所述,有些返回一个值,而有些则需要一个参数或多个。 例如,读取文本样本需要文件名。 因此,以下代码:
classDataStorage( wfApp:WordFrequencyFramework, privatevalstopWordsFilter:StopWordsFilter ){ init{ wfApp.registerForLoadEvents{load(it)} } privatefunload(filename:String){ data=read(filename) .flatMap{it.split("\\W|_".toRegex())} .filter{it.isNotBlank()&&it.length>=2} .map(String::toLowerCase) } // Abridged for readability }
另一个初始化发生在StopWordsFilter
类中。 它还会加载一个文件-停用词文件-但文件名未对其进行参数化。 但是,由于需要将其存储在同一存储桶中,因此需要接受String
类型的忽略参数 :
classStopWordsFilter(wfApp:WordFrequencyFramework){
init{
wfApp.registerForLoadEvents{load(it)}
}
privatefunload(ignore:String){ (1)
stopWords=read("stop_words.txt")[0].split(",")
}
// Abridged for readability
}
- 不幸的是,这是必需的。 至少,让我们以提供提示的方式来命名参数。
事件驱动编程的主要问题
事件驱动编程遭受一个大问题:复杂性。 虽然在我们的简单练习中,它是很容易管理的,但是随着类数的增加,它可以Swift升级。
我们有两个类,并将它们描绘为节点:它们之间可以有一条边。 在三个节点的情况下,计数为3。除此之外,它还会根据此表增长:
节点数 | 边数 |
---|---|
2 | 1 |
3 | 3 |
4 | 6 |
5 | 10 |
6 | 15 |
… | … |
n | n * (n - 1) / 2 |
可见,快速进行事件处理的推理变得不可能。
结论
在某些情况下, 例如 GUI,事件驱动编程是一项重要资产。 在这种情况下,事件发送类,事件接收类的数量以及它们之间可能的关系非常有限。 一旦后面的计数增加, 观察者模式就会变得非常复杂,因为每个观察者都需要引用每个主题。 在下周的帖子中,我们将研究该问题的可能解决方案。