在 Bash 中模拟 OOP

2025-06-07

在 Bash 中模拟 OOP

大家都知道 OOP 是“面向对象编程”的缩写。但OOP究竟是什么?它是吗?还是像我们在数百个教程中学到的那样,继承和多态?

事实上,尽管继承和多态性是补充 OOP 语言的重要特征,但它们都没有定义 OOP 的原始概念。

在这篇文章中,我将尝试演示一些基本的 OOP 概念,同时我们将一起回顾一些历史,并在Bash 脚本中对 OOP 进行一个非常简单的模拟。


OOP定义

根据维基百科

OOP是一种基于“对象”概念的编程范式,它可以包含数据和代码。

换句话说,OOP 提供了一种结构,我们可以在其中以属性的形式对相关数据或状态进行分组;以函数或方法的形式对代码或行为进行分组。

例如,我们可以考虑一个代表银行账户的对象:

目的

与银行账户相关的所有内容都可以归入这个单一结构,即对象

OOP 是如何实现的?

20 世纪 60 年代早期,出现了一批类似Simula的项目,它们实现了“对象”。基本上,它们允许将状态和行为保存在一个称为“对象”的单元中。

70 年代后期,Simula 概念影响了 Alan Kay 创建Smalltalk,这是一种面向对象编程语言,它为未来几十年出现的大量基于 OO 的编程语言开辟了道路。

尽管许多教程和课程倾向于通过继承和多态来解释 OOP,但我还是想强调两个主要特征,它们是完全基本的,足以实现 OOP:

  1. 对象必须能够保持状态(属性)
  2. 对象必须能够保存行为(函数)并在运行时动态执行/分派这些函数

为什么选择 Bash?

你可能会想:为什么人们会选择 Bash 来实现 OOP

我想使用一种设计上非面向对象的语言。而且,这种语言必须不支持词法作用域,而词法作用域对于实现第二个特性(动态调度)至关重要

Bash是一种使用简单语法规则实现的命令 shell 和脚本语言,因此它不支持词法作用域

别担心,我们稍后会在这篇文章中了解什么是词法作用域。现在就开始实现吧。


第一个特征:对象必须保持状态

我们可以用 Bash 实现第一个 trait 吗?我们试试看。

对象必须遵循某种模板。然后,我们将创建一个Object表示对象模板的函数。



Object() {
}


Enter fullscreen mode Exit fullscreen mode

很简单。现在,我们需要用一些参数调用这个函数。假设我们要使用以下语法创建对象:



Object account leandroAccount name=Leandro balance=500


Enter fullscreen mode Exit fullscreen mode

那太棒了,不是吗?我们可以解释一下函数参数:

  • $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
}


Enter fullscreen mode Exit fullscreen mode

太棒了!我们来玩一下吧:



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


Enter fullscreen mode Exit fullscreen mode

耶!我们刚刚证明了,在纯 Bash 脚本中,使用全局作用域和对象引用,可以实现第一个特性,即保存对象的状态

到目前为止一切都很好,不是吗?

第二个特征:对象必须支持动态调度

查看维基百科

动态分派是在运行时选择调用多态操作(方法或函数)的哪个实现的过程。它通常用于面向对象编程 (OOP) 语言和系统,并被认为是其主要特性。

我们能在 Bash 中实现这个对对象行为至关重要的第二个特性吗?我们试试看

正如人们所猜测的,我们可以使用函数来实现行为。假设我们要像下面这样调用函数:



$leandroAccount_fn_display

Hello, Leandro. Your balance is 100


Enter fullscreen mode Exit fullscreen mode

为了允许将函数名称保存到对象的作用域中,我们必须支持词法作用域,这为计算技术和结构(例如后期绑定和闭包)开辟了可能性。

遗憾的是,由于 Bash 的语法规则过于简单,它不支持词法作用域。毕竟,它是一种脚本语言

但我们还可以尝试另一个技巧。将作用域(对象)作为参数传递给函数怎么样?例如:



$account_fn_display leandroAccount


Enter fullscreen mode Exit fullscreen mode

我们来尝试一下。

首先,我们必须定义display函数:



display() {
  self=$1

  name=${self}_name
  balance=${self}_balance

  echo "Hello, ${!name}. Your balance is ${!balance}"
}


Enter fullscreen mode Exit fullscreen mode

太好了。现在,是时候使用函数作为参数来创建对象了:



## 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


Enter fullscreen mode Exit fullscreen mode

当然,我们必须解析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


Enter fullscreen mode Exit fullscreen mode

此时我们已经一切就绪,因为我们已经可以调用将对象传递给它的函数了:



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


Enter fullscreen mode Exit fullscreen mode

超级棒耶!

我们刚刚证明了通过使用函数局部范围、对象引用和参数传递,在 Bash 中实现第二个特性也是完全可能的。

因此,我们可以说我们可以在 Bash 中实现 OOP。好吧,虽然非常有限,但还是有可能的。

添加更多功能

现在,我们可以通过添加更多行为来释放 Bash 中 OOP 的强大功能。

必须实现一个deposit函数吗?没问题,现在应该非常简单:



deposit() {
  self=$1

  currentBalance=${self}_balance
  amount=$2

  export ${self}_balance=$(($currentBalance + $amount))
}


Enter fullscreen mode Exit fullscreen mode

进而:



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


Enter fullscreen mode Exit fullscreen mode

天哪,多么美好的一天!

将相关数据和行为分组

目前,我们有两个处于脚本范围的函数,没有语义含义

我们可以创建另一个函数来包装与“帐户”相关的属性和函数。那么,如何调用这样的函数呢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
}


Enter fullscreen mode Exit fullscreen mode

不可能,它看起来就像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


Enter fullscreen mode Exit fullscreen mode

太棒了!


结论

在本文中,我想通过使用 Bash 脚本来解释 OOP 中的两个重要特征。

这篇很棒的博文启发了我去探索在 Bash 中实现面向对象编程 (OOP)。我不是 Bash 专家,但尝试一下这件事确实很有趣也很愉快。

希望你喜欢这篇文章,欢迎留言评论。代码分享在 gist 中

更不用说,在TwitterLinkedin上关注我。

文章来源:https://dev.to/leandronsp/simulated-oop-in-bash-3mop
PREV
深入探究 Gin:Golang 的领先框架 简介 从一个小示例开始 HTTP 方法 创建引擎变量 注册路由回调函数 使用基数树加速路由检索 导入中间件处理函数 开始运行进程消息上下文处理恐慌
NEXT
Kubernetes 101,第八部分,网络基础知识