Simulating OOP in Bash
Understand the fundamentals of OOP in a practical step-by-step guide
Everyone knows that OOP stands for "Object-oriented programming". But what is in fact OOP? Is it a Class? Is it inheritance and polymorphism like we learned across hundreds of tutorials?
Indeed, despite inheritance and polymorphism being important traits that complement OOP languages, none of them define the original concepts of OOP.
In this post, I'll try to demonstrate some elementary OOP concepts while we go together through a bit of history along with a very simple simulation of OOP in, you heard well, Bash script.
The OOP definition
According to the wikipedia:
OOP is a programming paradigm based on the concept of "objects", which can contain data and code.
In other words, OOP provides a structure where we can group related data or state, in the form of attributes; and code or behaviour, in the form of functions or methods.
For instance, we could think of an object that represents a Bank Account:
Everything related to a Bank Account could be grouped into this very single structure, an object.
How is OOP implemented?
In the early 60's, a bunch of projects such as Simula emerged with some implementations of "objects". Basically it allowed to hold state and behaviour into a single unit called object.
Later on the 70's, Simula concepts influenced Alan Kay to create Smalltalk, an oriented-object programming language which opened a wide range of OO-based programming languages that came in the upcoming decades.
Despite of many tutorials and courses prefer focusing on inheritance and polymorphism to explain OOP, I'd like to highlight 2 main traits that are exclusively elementary and enough for enabling OOP:
- objects must be able to hold a state (attributes)
- objects must be able to hold behaviour (functions) and execute/dispatch those functions dynamically at run time
Why Bash?
You may be wondering: why in this world one would choose Bash to implement OOP?
I wanted to use a language which is not OOP by design. Also, such a language must not support lexical scope, which is a very important characteristic in order to have the 2nd trait (dynamic dispatch) in place.
Bash is a command shell and script language that is implemented using simple grammar rules hence it does not support lexical scope.
Don't worry, we'll understand what is lexical scope later in this post. Let's start the implementation.
First trait: objects must hold state
Can we implement the first trait using Bash? Let's try.
Objects must follow some sort of template. Then, we're going to create an Object
function that will represent the object template.
Object() {
}
Pretty simple. Now, we have to call this function with some arguments. Suppose we want to create objects using the following syntax:
Object account leandroAccount name=Leandro balance=500
That would be great, wouldn't it? May we explain the function arguments:
$1
: the type of object, in this case,account
$2
: the reference to the object,leandroAccount
$3
+: a key-pair structure that should be parsed then saved into the object's internal state
Okay, time to implement the Object
function. In bash, there's no "internal state" of functions. Indeed, there's a local scope but it can't be used across different "objects" we may create.
What should we do then? Use global state.
It's weird, I know, but it's the only way to define an object's state in Bash, since it has no lexical scope.
Lexical scope is used to define a reserved area in memory for structures that may be evaluated with arbitrary arguments.
But we can do a trick, by prepending the object's reference at every attribute, for instance:
leandroAccount_name
refers to the leandroAccount's nameleandroAccount_balance
refers to the leandroAccount's balancecarlosAccount_name
refers to the carlosAccount's name
...and so on.
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
}
Super nice! Let's play a bit:
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
Yay! We just proved that it's possible to implement the first trait, holding an object's state, in pure Bash script, using global scope and object reference.
So far, so good, isn't it?
Second trait: objects must support dynamic dispatch
Looking at the wikipedia:
Dynamic dispatch is the process of selecting which implementation of a polymorphic operation (method or function) to call at run time. It is commonly employed in, and considered a prime characteristic of, object-oriented programming (OOP) languages and systems.
Can we implement this second trait, important for the object's behaviour, in Bash? Let's try.
As one could guess, we can implement behaviour using functions. Suppose we want to call functions as follows:
$leandroAccount_fn_display
Hello, Leandro. Your balance is 100
In order to allow saving the function name into the object's scope, we'd have to support lexical scope, which opens possibilities for computing techniques and structures such as late bindings and closures.
Unfortunately, Bash has no support for lexical scope due to its simple grammar rules. Remember that it's a script language, after all.
But we can do another trick as well. How about passing the scope (object) as an argument to the function? Something like:
$account_fn_display leandroAccount
Let's try it.
First, we have to define the display
function:
display() {
self=$1
name=${self}_name
balance=${self}_balance
echo "Hello, ${!name}. Your balance is ${!balance}"
}
Great. Now, time to create the object using the function as an argument:
## 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
And, of course, we have to parse the fn argument in the Object
function, by just adding the elif
clause:
# ... 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
At this time we are all set, as we can already call the function passing the object to it:
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
Super Yay!
We just proved that it's perfectly possible to implement the second trait in Bash too, by using function local scope, object reference and argument passing.
Thus, we can say we can implement OOP in Bash. Okay, very limited, but possible.
Adding more functions
Now, we can unlock the power of OOP in Bash by adding more behaviour as much as we want.
Must implement a deposit
function? No problem, that should be super easy at this moment:
deposit() {
self=$1
currentBalance=${self}_balance
amount=$2
export ${self}_balance=$(($currentBalance + $amount))
}
And then:
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
OMG, what a wonderful day!
Grouping related data and behaviour together
At this moment, we have two functions at the script scope, with no semantic meaning.
We can create another function that would wrap attributes and functions related to "accounts". What about calling such a function, 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
}
No way, it looks like a Class
we see in many Java/C++/Ruby tutorials!
But it's not a class in Bash. In the end, we're just simulating OOP.
However, since Bash relies in global scope because of its scripting language nature, we can organize our code using OOP, then allowing to interacting with objects like the following:
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
That's amazing!
Conclusion
In this article I wanted to explain 2 important traits in OOP by using Bash script.
This great blogpost inspired me to explore implementing OOP in Bash. I'm not an expert in Bash, but it was very fun and pleasant to play at this matter.
I hope you enjoyed the article, feel free to drop any comments. The code is shared in this gist.
Not to mention, follow me in Twitter, Linkedin and DEV Community.