This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
rhaj/_archive/rhai_engine/rhaibook/patterns/oop.md
2025-04-04 08:28:07 +02:00

197 lines
5.3 KiB
Markdown

Object-Oriented Programming (OOP)
=================================
{{#include ../links.md}}
Rhai does not have _objects_ per se and is not object-oriented (in the traditional sense),
but it is possible to _simulate_ object-oriented programming.
```admonish question.small "To OOP or not to OOP, that is the question."
Regardless of whether object-oriented programming (OOP) should be treated as a pattern or
an _anti-pattern_ (the programming world is split 50-50 on this), there are always users who
would like to write Rhai in "the OOP way."
Rust itself is not object-oriented in the traditional sense; JavaScript also isn't, but that didn't
prevent generations of programmers trying to shoehorn a class-based inheritance system onto it.
So... as soon as Rhai gained in usage, way way before version 1.0, PR's started coming in to make
it possible to write Rhai in "the OOP way."
```
Use Object Maps to Simulate OOP
-------------------------------
Rhai's [object maps] has [special support for OOP]({{rootUrl}}/language/object-maps-oop.md).
| Rhai concept | Maps to OOP |
| ----------------------------------------------------- | :---------: |
| [Object maps] | objects |
| [Object map] properties holding values | properties |
| [Object map] properties that hold [function pointers] | methods |
When a property of an [object map] is called like a method function, and if it happens to hold a
valid [function pointer] (perhaps defined via an [anonymous function] or more commonly as a [closure]),
then the call will be dispatched to the actual function with `this` binding to the
[object map] itself.
Use Closures to Define Methods
------------------------------
[Closures] defined as values for [object map] properties take on a syntactic shape which resembles
very closely that of class methods in an OOP language.
[Closures] also _capture_ [variables] from the defining environment, which is a very common language
feature. It can be turned off via the [`no_closure`] feature.
```rust
let factor = 1;
// Define the object
let obj = #{
data: 0, // object field
increment: |x| this.data += x, // 'this' binds to 'obj'
update: |x| this.data = x * factor, // 'this' binds to 'obj', 'factor' is captured
action: || print(this.data) // 'this' binds to 'obj'
};
// Use the object
obj.increment(1);
obj.action(); // prints 1
obj.update(42);
obj.action(); // prints 42
factor = 2;
obj.update(42);
obj.action(); // prints 84
```
Simulating Inheritance with Polyfills
-------------------------------------
The `fill_with` method of [object maps] can be conveniently used to _polyfill_ default method
implementations from a _base class_, as per OOP lingo.
Do not use the `mixin` method because it _overwrites_ existing fields.
```rust
// Define base class
let BaseClass = #{
factor: 1,
data: 42,
get_data: || this.data * 2,
update: |x| this.data += x * this.factor
};
let obj = #{
// Override base class field
factor: 100,
// Override base class method
// Notice that the base class can also be accessed, if in scope
get_data: || this.call(BaseClass.get_data) * 999,
}
// Polyfill missing fields/methods
obj.fill_with(BaseClass);
// By this point, 'obj' has the following:
//
// #{
// factor: 100
// data: 42,
// get_data: || this.call(BaseClass.get_data) * 999,
// update: |x| this.data += x * this.factor
// }
// obj.get_data() => (this.data (42) * 2) * 999
obj.get_data() == 83916;
obj.update(1);
obj.data == 142
```
Prototypical Inheritance via Mixin
----------------------------------
Some languages like JavaScript has _prototypical_ inheritance, which bases inheritance on a
_prototype_ object.
It is possible to simulate this form of inheritance using [object maps], leveraging the fact that,
in Rhai, all values are cloned and there are no pointers. This significantly simplifies coding logic.
```rust
// Define prototype 'class'
const PrototypeClass = #{
field: 42,
get_field: || this.field,
set_field: |x| this.field = x
};
// Create instances of the 'class'
let obj1 = PrototypeClass; // a copy of 'PrototypeClass'
obj1.get_field() == 42;
let obj2 = PrototypeClass; // a copy of 'PrototypeClass'
obj2.mixin(#{ // override fields and methods
field: 1,
get_field: || this.field * 2
};
obj2.get_field() == 2;
let obj2 = PrototypeClass + #{ // compact syntax with '+'
field: 1,
get_field: || this.field * 2
};
obj2.get_field() == 2;
// Inheritance chain
const ParentClass = #{
field: 123,
new_field: 0,
action: || print(this.new_field * this.field)
};
const ChildClass = #{
action: || {
this.field = this.new_field;
this.new_field = ();
}
}
let obj3 = PrototypeClass + ParentClass + ChildClass;
// Alternate formulation
const ParentClass = PrototypeClass + #{
field: 123,
new_field: 0,
action: || print(this.new_field * this.field)
};
const ChildClass = ParentClass + #{
action: || {
this.field = this.new_field;
this.new_field = ();
}
}
let obj3 = ChildClass; // a copy of 'ChildClass'
```