交互式系统中架构风格的选择1 简介 现代大多数web应用都是交互式的,而在软件架构中,管道-过滤器(Pipe and F
0 简介
现代大多数web应用都是交互式的,而在软件架构中,管道-过滤器(Pipe and Filter)、事件触发的隐式调用(Event-Driven Implicit Invocation) 和 解释器(Interpreter) 是三种常见的设计风格都可以不同长度实现交互式,我们在选择时可以略作比较。
1 管道-过滤器(Pipe and Filter)风格
系统由一系列数据处理组件(过滤器)组成,数据通过管道在过滤器之间传输。 每个过滤器执行特定的任务,并将处理后的数据通过管道传递给下一个过滤器。 过滤器之间是独立的,彼此之间没有依赖性,管道负责数据的流动。
适用场景:
数据流处理,如图像处理、编译器设计、数据转换等。 适合需要按顺序执行多个独立任务的场景,尤其是涉及数据的处理和转换。
package main
import (
"fmt"
"strings"
)
// Filter function type
type Filter func(data string) string
// Concrete filters
func ToUpper(data string) string {
return strings.ToUpper(data)
}
func AddSuffix(data string) string {
return data + "!!!"
}
// Pipeline function
func pipeline(data string, filters ...Filter) string {
for _, filter := range filters {
data = filter(data)
}
return data
}
func main() {
data := "hello world"
result := pipeline(data, ToUpper, AddSuffix)
fmt.Println(result)
}
管道风格实例:
// Output: HELLO WORLD!!!
该实例显示了如下在处理字符串时使用管道-过滤器的思想,在主函数中,当传入转换字符为大写的函数和添加后缀的函数后,主函数中pipeline将依次按顺序执行多个独立任务,这就是涉及数据的处理和转换。
2 事件触发的隐式调用(Event-Driven Implicit Invocation)风格
系统中的组件通过事件机制进行交互,而不是直接调用其他组件的方法。 某个事件被触发后,所有注册了该事件的组件都会被隐式调用。 松耦合系统,组件间没有直接的依赖关系,适合动态扩展或事件驱动的系统。
适用场景:
GUI应用、消息驱动系统、实时系统,如游戏引擎、IoT设备等。 适合需要响应多个不确定事件的场景,系统需要高扩展性和灵活性。
package main
import "fmt"
// EventManager holds event subscribers
type EventManager struct {
listeners map[string][]func(string)
}
// Register a listener for an event
func (e *EventManager) Register(event string, listener func(string)) {
if e.listeners == nil {
e.listeners = make(map[string][]func(string))
}
e.listeners[event] = append(e.listeners[event], listener)
}
// Trigger an event
func (e *EventManager) Trigger(event string, data string) {
if listeners, ok := e.listeners[event]; ok {
for _, listener := range listeners {
listener(data)
}
}
}
func main() {
manager := &EventManager{}
// Register event listeners
manager.Register("greet", func(data string) {
fmt.Println("Hello,", data)
})
manager.Register("greet", func(data string) {
fmt.Println("Welcome,", data)
})
// Trigger the event
manager.Trigger("greet", "John")
// Output:
// Hello, John
// Welcome, John
}
系统往往需要处理用户的输入、响应多种事件以及协调多个组件。 因此,事件触发的隐式调用风格(Event-Driven Implicit Invocation)通常很适合用于这类交互式场景。
- 具体原因:
为什么事件驱动的隐式调用风格适合交互式软件应用:
高交互性:
交互式应用通常需要根据用户的动作(如点击、输入、滑动等)触发不同的响应。事件驱动架构非常适合这种场景,因为它可以将用户动作视为事件,每个事件都可以引发特定的处理逻辑。
松耦合和扩展性:
在事件驱动架构中,组件通过事件进行通信,彼此之间没有直接的依赖。这种松耦合使得交互式应用的各个功能模块可以独立开发、维护和扩展。例如,可以很容易地增加新的事件或功能,而无需对整个系统进行大规模改动。
并行处理:
交互式软件需要处理多个并发事件,例如同时接受用户输入、网络请求和后台任务。事件驱动架构能够自然地适应这种并发处理,因为每个事件可以在独立的上下文中被处理,不同的事件处理程序不会相互干扰。
响应式设计:
事件驱动架构非常适合实现响应式设计,即根据用户的行为动态调整界面或功能。比如在 Web 应用中,可以基于用户点击某个按钮来触发后端逻辑,然后更新前端显示。
适用事件驱动架构的交互式应用
GUI应用程序:如桌面应用和移动应用,通过按钮点击、菜单选择等触发不同的事件并响应。 Web 应用程序:基于用户的输入或交互(如 AJAX 请求、按钮点击等)来动态更新页面或执行操作。 游戏引擎:游戏开发中有很多触发条件,如用户操作、物理引擎碰撞等,事件驱动架构可以很好地管理这些复杂的交互。
简单示例:交互式的事件驱动风格在 Web 应用中的使用。
以下是一个简单的 Go 例子,展示如何在交互式应用中使用事件驱动架构来处理用户事件:
package main
import (
"fmt"
"time"
)
// EventManager to manage events
type EventManager struct {
listeners map[string][]func(interface{})
}
// Register listeners to events
func (em *EventManager) Register(event string, listener func(interface{})) {
if em.listeners == nil {
em.listeners = make(map[string][]func(interface{}))
}
em.listeners[event] = append(em.listeners[event], listener)
}
// Trigger an event
func (em *EventManager) Trigger(event string, data interface{}) {
if listeners, ok := em.listeners[event]; ok {
for _, listener := range listeners {
listener(data)
}
}
}
func main() {
// Create event manager
manager := &EventManager{}
// Register listeners
manager.Register("button_click", func(data interface{}) {
fmt.Println("Button clicked:", data)
})
manager.Register("text_input", func(data interface{}) {
fmt.Println("Text input received:", data)
})
// Simulate user interactions
go func() {
time.Sleep(2 * time.Second)
manager.Trigger("button_click", "Submit")
}()
go func() {
time.Sleep(1 * time.Second)
manager.Trigger("text_input", "User typed: Hello World!")
}()
// Block main thread for demo purposes
time.Sleep(3 * time.Second)
}
在这个例子中,事件驱动架构允许多个不同的事件(如按钮点击和文本输入)被触发,并且可以相应地执行相应的处理程序。
这种风格非常适合于需要处理多个交互的应用程序,尤其是在需要处理复杂逻辑和并发操作的情况下。
3 解释器(Interpreter)风格
通过定义一种语言的解释器来执行程序,解释器逐步解释输入并执行相应的操作。 通常包含一个表达式或指令集的语法树,每个节点代表一个指令,解释器逐一执行这些指令。
适用场景:
编程语言解释器、规则引擎、脚本引擎等。 适用于需要动态执行指令或语言的场景,特别是定制语言或规则集的执行。
package main
import (
"fmt"
"strconv"
"strings"
)
// Interpreter interface
type Expression interface {
Interpret() int
}
// Terminal expression for numbers
type NumberExpression struct {
value int
}
func (n *NumberExpression) Interpret() int {
return n.value
}
// Non-terminal expression for operations
type AddExpression struct {
left, right Expression
}
func (a *AddExpression) Interpret() int {
return a.left.Interpret() + a.right.Interpret()
}
// Parse function to create expression tree
func Parse(expression string) Expression {
tokens := strings.Split(expression, " ")
leftValue, _ := strconv.Atoi(tokens[0])
rightValue, _ := strconv.Atoi(tokens[2])
left := &NumberExpression{leftValue}
right := &NumberExpression{rightValue}
return &AddExpression{left, right}
}
func main() {
expression := "4 + 2"
parsedExpression := Parse(expression)
result := parsedExpression.Interpret()
fmt.Println(result) // Output: 6
}
解释器风格可以在某些交互性强的应用软件架构中部分替代事件触发的隐式调用风格,但通常并不能完全取代它,原因在于两者的设计目标和应用场景存在显著差异。以下是详细的比较和分析:
解释器风格的特点:
逐步解释和执行指令:解释器风格的系统通常会逐步解析输入(如代码、脚本、配置文件或规则集),并根据定义的语法或规则进行解释和执行。这种风格的核心是处理一套语言或指令。
灵活性和动态性:它非常适合处理动态、可扩展的规则或语言,尤其是用户定义的逻辑或脚本。许多交互性强的系统通过嵌入式脚本(如游戏引擎、规则引擎)来实现可编程的用户行为或业务逻辑。
解释器风格在交互式应用中的应用场景:
嵌入式脚本或规则引擎:解释器风格可以在交互式应用中为用户提供编程或自定义规则的能力。例如,游戏开发者可以允许玩家编写脚本来定义游戏内的行为,或者允许用户配置复杂的业务逻辑和规则。
聊天机器人或DSL(领域特定语言):
解释器可以解析用户的输入并根据其内容动态执行相应的操作。这在聊天机器人或特定的命令行交互工具中非常有用。
但是解释器风格无法完全替代事件驱动风格:
尽管解释器风格可以处理复杂的输入和动态规则,但在许多交互式应用中,它并不能完全取代事件驱动架构。
以下是主要原因:
事件驱动适合并发和非确定性事件处理:
解释器风格通常会解析和执行一个静态的指令集或语言片段,这种执行过程往往是同步的、单线程的(除非特别设计为并发解释器)。
事件驱动架构天生擅长处理并发的、不确定的事件,例如用户的输入、网络请求、系统通知等。它能够很自然地在多个异步事件间进行切换,而不需要等待某一指令集的执行结束。
耦合度和扩展性:
事件驱动架构中的各个组件是松耦合的,基于事件触发机制,各个组件之间几乎没有依赖关系。这使得系统可以轻松扩展和修改,而不影响整体架构。
解释器风格中的指令集通常是被嵌入在应用中的,它需要对具体的语法和操作有明确的定义。如果要增加新的交互功能,通常需要修改解释器或者扩展它的指令集,这在某种程度上会增加系统的耦合性。
实时性和用户响应:
事件驱动架构能够及时响应用户的操作,而不依赖于一套解释逻辑的执行完成。这意味着用户界面的更新和反馈可以是实时的。
解释器风格虽然可以动态处理输入,但执行的速度和复杂性可能会影响用户的实时交互体验,尤其是在复杂的解释逻辑或长时间运行的任务中。
组合使用的场景:
在某些情况下,解释器风格和事件驱动架构可以结合使用,以发挥各自的优势。例如:
嵌入式脚本引擎:
在交互式应用中可以使用事件驱动架构来管理用户的交互,同时使用解释器风格来解析和执行用户定义的脚本或规则。例如,一个用户通过 GUI 触发了某个事件,这个事件启动了解释器来处理用户输入的脚本。
DSL(领域特定语言)和规则引擎:
在需要用户定义特定规则或指令的系统中,可以使用解释器风格来解析这些规则,而事件驱动架构处理用户的交互和外部事件。比如在金融系统中,用户可能定义了复杂的交易规则,这些规则通过解释器执行,但触发这些规则的仍然是事件驱动机制。
4 总结
管道-过滤器风格适合数据流处理和转换场景,系统由独立的过滤器组成,通过管道传输数据。 事件触发的隐式调用风格:适用于需要响应多个事件、组件间松耦合的场景,常见于实时系统和消息驱动的应用中。
解释器风格适用于动态执行指令或自定义语言的场景,通常用于编程语言解释器或规则引擎。 每种风格都有其独特的优势和适用场景,根据实际需求选择合适的架构风格可以显著提高系统的灵活性和可维护性。
解释器风格可以在交互性强的应用中补充事件驱动架构,但通常不能完全替代。
它适用于处理动态规则、脚本和复杂的用户输入,但在处理并发、实时交互和系统响应时,事件驱动架构仍然是更好的选择。
结合两者的优势,尤其是在需要动态行为且并发事件频繁的系统中,往往能取得更好的效果。
转载自:https://juejin.cn/post/7407635822196850739