在 Bash 中模拟 OOP
大家都知道 OOP 是“面向对象编程”的缩写。但OOP究竟是什么?它是类吗?还是像我们在数百个教程中学到的那样,是继承和多态?
事实上,尽管继承和多态性是补充 OOP 语言的重要特征,但它们都没有定义 OOP 的原始概念。
在这篇文章中,我将尝试演示一些基本的 OOP 概念,同时我们将一起回顾一些历史,并在Bash 脚本中对 OOP 进行一个非常简单的模拟。
OOP定义
根据维基百科:
OOP是一种基于“对象”概念的编程范式,它可以包含数据和代码。
换句话说,OOP 提供了一种结构,我们可以在其中以属性的形式对相关数据或状态进行分组;以函数或方法的形式对代码或行为进行分组。
例如,我们可以考虑一个代表银行账户的对象:
与银行账户相关的所有内容都可以归入这个单一结构,即对象。
OOP 是如何实现的?
20 世纪 60 年代早期,出现了一批类似Simula的项目,它们实现了“对象”。基本上,它们允许将状态和行为保存在一个称为“对象”的单元中。
70 年代后期,Simula 概念影响了 Alan Kay 创建Smalltalk,这是一种面向对象编程语言,它为未来几十年出现的大量基于 OO 的编程语言开辟了道路。
尽管许多教程和课程倾向于通过继承和多态来解释 OOP,但我还是想强调两个主要特征,它们是完全基本的,足以实现 OOP:
- 对象必须能够保持状态(属性)
- 对象必须能够保存行为(函数)并在运行时动态执行/分派这些函数
为什么选择 Bash?
你可能会想:为什么人们会选择 Bash 来实现 OOP?
我想使用一种设计上非面向对象的语言。而且,这种语言必须不支持词法作用域,而词法作用域对于实现第二个特性(动态调度)至关重要。
Bash是一种使用简单语法规则实现的命令 shell 和脚本语言,因此它不支持词法作用域。
别担心,我们稍后会在这篇文章中了解什么是词法作用域。现在就开始实现吧。
第一个特征:对象必须保持状态
我们可以用 Bash 实现第一个 trait 吗?我们试试看。
对象必须遵循某种模板。然后,我们将创建一个Object
表示对象模板的函数。
Object() {
}
很简单。现在,我们需要用一些参数调用这个函数。假设我们要使用以下语法创建对象:
Object account leandroAccount name=Leandro balance=500
那太棒了,不是吗?我们可以解释一下函数参数:
$1
:对象的类型,在本例中,account
$2
:对象的引用,leandroAccount
$3
+:应解析然后保存到对象内部状态的密钥对结构
好的,是时候实现这个Object
函数了。在 Bash 中,函数没有“内部状态”。虽然存在局部作用域,但它不能在我们创建的不同“对象”之间使用。
那我们该怎么办呢? 使用全局状态。
我知道这很奇怪,但这是在 Bash 中定义对象状态的唯一方法,因为它没有词法范围。
词法范围用于在内存中定义一个保留区域,用于存放可以用任意参数进行评估的结构。
但是我们可以做一个技巧,在每个属性前面加上对象的引用,例如:
leandroAccount_name
指的是 leandroAccount 的名称leandroAccount_balance
指的是 leandroAccount 的余额carlosAccount_name
指的是 carlosAccount 的名称
...等等。
Object () {
# e.g account
kind=$1
# e.g leandroAccount
self=$2
shift
shift
# iterates over the remaining args
for arg in "$@"; do
# e.g name=Leandro becomes ARG_KEY=name ARG_VALUE=Leandro
read ARG_KEY ARG_VALUE <<< $(echo "$arg" | sed -E "s/(\w+)=(.*?)/\1 \2/")
if [[ ! -z "$ARG_KEY" ]] && [[ ! -z "$ARG_VALUE" ]]; then
# declare the object's state!!!!
# e.g export leandroAccount_balance=100
export ${self}_$ARG_KEY="$ARG_VALUE"
fi
done
}
太棒了!我们来玩一下吧:
Object account leandroAccount name=Leandro balance=500
echo $leandroAccount_name # prints Leandro
echo $leandroAccount_balance # prints 500
Object account carlosAccount name=Carlitos balance=800
echo $carlosAccount_name # prints Carlitos
echo $carlosAccount_balance # prints 800
耶!我们刚刚证明了,在纯 Bash 脚本中,使用全局作用域和对象引用,可以实现第一个特性,即保存对象的状态。
到目前为止一切都很好,不是吗?
第二个特征:对象必须支持动态调度
查看维基百科:
动态分派是在运行时选择调用多态操作(方法或函数)的哪个实现的过程。它通常用于面向对象编程 (OOP) 语言和系统,并被认为是其主要特性。
我们能在 Bash 中实现这个对对象行为至关重要的第二个特性吗?我们试试看。
正如人们所猜测的,我们可以使用函数来实现行为。假设我们要像下面这样调用函数:
$leandroAccount_fn_display
Hello, Leandro. Your balance is 100
为了允许将函数名称保存到对象的作用域中,我们必须支持词法作用域,这为计算技术和结构(例如后期绑定和闭包)开辟了可能性。
遗憾的是,由于 Bash 的语法规则过于简单,它不支持词法作用域。毕竟,它是一种脚本语言。
但我们还可以尝试另一个技巧。将作用域(对象)作为参数传递给函数怎么样?例如:
$account_fn_display leandroAccount
我们来尝试一下。
首先,我们必须定义display
函数:
display() {
self=$1
name=${self}_name
balance=${self}_balance
echo "Hello, ${!name}. Your balance is ${!balance}"
}
太好了。现在,是时候使用函数作为参数来创建对象了:
## Note that we're using a different syntax for functions, by prepending a "fn_", otherwise it would conflict with function attributes
Object account leandroAccount name=Leandro balance=500 fn_display
当然,我们必须解析Object
函数中的 fn 参数,只需添加elif
子句即可:
# ... code here
## Parse argument when matching functions
## e.g fn_display -> FUNC=display
read FUNC <<< $(echo "$arg" | sed -E "s/fn_(\w+)$/\1/")
...
elif [[ ! -z "$FUNC" ]] && [[ "$FUNC" != "$self" ]]; then
## Define the function in the global scope, prepending the object kind, e.g account_fn_display, user_fn_logout etc
export ${kind}_fn_$FUNC=$FUNC
fi
# ... code here
此时我们已经一切就绪,因为我们已经可以调用将对象传递给它的函数了:
Object account leandroAccount name=Leandro balance=500 fn_display
Object account carlosAccount name=Carlitos balance=800 fn_display
$account_fn_display leandroAccount
$account_fn_display carlosAccount
#### Result ####
Hello, Leandro. Your balance is 100
Hello, Carlitos. Your balance is 800
超级棒耶!
我们刚刚证明了通过使用函数局部范围、对象引用和参数传递,在 Bash 中实现第二个特性也是完全可能的。
因此,我们可以说我们可以在 Bash 中实现 OOP。好吧,虽然非常有限,但还是有可能的。
添加更多功能
现在,我们可以通过添加更多行为来释放 Bash 中 OOP 的强大功能。
必须实现一个deposit
函数吗?没问题,现在应该非常简单:
deposit() {
self=$1
currentBalance=${self}_balance
amount=$2
export ${self}_balance=$(($currentBalance + $amount))
}
进而:
Object account leandroAccount name=Leandro balance=100 fn_display fn_deposit
$account_fn_deposit leandroAccount 50
$account_fn_display leandroAccount
## Result
Hello, Leandro. Your balance is 150
天哪,多么美好的一天!
将相关数据和行为分组
目前,我们有两个处于脚本范围的函数,没有语义含义。
我们可以创建另一个函数来包装与“帐户”相关的属性和函数。那么,如何调用这样的函数呢Account
?
Account() {
display() {
self=$1
name=${self}_name
balance=${self}_balance
echo "Hello, ${!name}. Your balance is ${!balance}"
}
deposit() {
self=$1
currentBalance=${self}_balance
amount=$2
export ${self}_balance=$(($currentBalance + $amount))
}
Object account "$@"
Object account $1 fn_display
Object account $1 fn_deposit
}
不可能,它看起来就像Class
我们在许多 Java/C++/Ruby 教程中看到的那样!
但它不是 Bash 中的类。最终,我们只是在模拟 OOP。
然而,由于 Bash 由于其脚本语言性质而依赖于全局范围,我们可以使用 OOP 来组织我们的代码,然后允许与如下对象进行交互:
Account accountA name=Leandro balance=100
Account accountB name=John balance=500
$account_fn_deposit accountA 50
$account_fn_display accountA
$account_fn_display accountB
太棒了!
结论
在本文中,我想通过使用 Bash 脚本来解释 OOP 中的两个重要特征。
这篇很棒的博文启发了我去探索在 Bash 中实现面向对象编程 (OOP)。我不是 Bash 专家,但尝试一下这件事确实很有趣也很愉快。
希望你喜欢这篇文章,欢迎留言评论。代码分享在 gist 中。
文章来源:https://dev.to/leandronsp/simulated-oop-in-bash-3mop